diff --git a/android/app/build.gradle b/android/app/build.gradle index e200ccee..11f479be 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -73,6 +73,9 @@ flutter { dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' + implementation platform('com.google.firebase:firebase-bom:34.9.0') + implementation 'com.google.firebase:firebase-messaging' + implementation 'androidx.work:work-runtime:2.10.2' implementation 'com.otaliastudios:transcoder:0.11.0' implementation 'androidx.core:core-splashscreen:1.0.1' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8f648018..76b2cfaf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -71,6 +71,25 @@ android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService" tools:node="remove"> + + + + + + + + + diff --git a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt index 8b680aad..24205927 100644 --- a/android/app/src/main/kotlin/eu/twonly/MainActivity.kt +++ b/android/app/src/main/kotlin/eu/twonly/MainActivity.kt @@ -7,6 +7,7 @@ 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 android.content.Intent import io.crates.keyring.Keyring import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import android.os.Bundle @@ -16,6 +17,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.PickVisualMediaRequest import io.flutter.plugin.common.MethodChannel +import eu.twonly.notifications.NotificationTapChannel class MainActivity : FlutterFragmentActivity() { private val CHANNEL = "eu.twonly/photo_picker" @@ -25,6 +27,10 @@ class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() + + // Buffer a notification tap before the Flutter engine exists so the + // cold-start route is not lost. + NotificationTapChannel.handleIntent(intent) pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> if (uris.isNotEmpty()) { @@ -39,6 +45,12 @@ class MainActivity : FlutterFragmentActivity() { super.onCreate(savedInstanceState) } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + NotificationTapChannel.handleIntent(intent) + } + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) { eventSink!!.success(true) @@ -58,6 +70,8 @@ class MainActivity : FlutterFragmentActivity() { VideoCompressionChannel.configure(flutterEngine, applicationContext) + NotificationTapChannel.configure(flutterEngine, applicationContext) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "pickImages" -> { @@ -88,4 +102,9 @@ class MainActivity : FlutterFragmentActivity() { } } } + + override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { + NotificationTapChannel.detach() + super.cleanUpFlutterEngine(flutterEngine) + } } diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationBridge.kt b/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationBridge.kt new file mode 100644 index 00000000..574a4ed5 --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationBridge.kt @@ -0,0 +1,25 @@ +package eu.twonly.notifications + +internal object NativeNotificationBridge { + init { + System.loadLibrary("rust_lib_twonly") + } + + @JvmStatic + external fun process( + databaseDirectory: String, + dataDirectory: String, + locale: String, + deadlineMs: Long, + ): String + + @JvmStatic + external fun acknowledge(eventIdsJson: String): String + + @JvmStatic + external fun storeFcmToken( + databaseDirectory: String, + dataDirectory: String, + token: String, + ): String +} diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationModels.kt b/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationModels.kt new file mode 100644 index 00000000..56e5249f --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/notifications/NativeNotificationModels.kt @@ -0,0 +1,77 @@ +package eu.twonly.notifications + +import org.json.JSONObject + +internal data class NativeNotificationPresentation( + val title: String, + val body: String, +) + +internal data class NativeNotificationAddition( + val eventId: String, + val notificationId: String, + val conversationId: String?, + val senderId: Long, + val senderName: String, + val title: String, + val body: String, + val createdAt: Long, + val avatarPath: String?, +) + +internal data class NativeNotificationBatch( + val additions: List, + val removals: List, + val badgeCount: Long, + val completed: Boolean, +) + +internal data class NativeNotificationResponse( + val ok: Boolean, + val batch: NativeNotificationBatch?, + val fallback: NativeNotificationPresentation?, +) { + companion object { + fun parse(json: String): NativeNotificationResponse { + val root = JSONObject(json) + val batchJson = root.optJSONObject("batch") + val additions = batchJson?.optJSONArray("additions")?.let { array -> + List(array.length()) { index -> + val item = array.getJSONObject(index) + NativeNotificationAddition( + eventId = item.getString("event_id"), + notificationId = item.getString("notification_id"), + conversationId = item.nullableString("conversation_id"), + senderId = item.getLong("sender_id"), + senderName = item.getString("sender_name"), + title = item.getString("title"), + body = item.getString("body"), + createdAt = item.getLong("created_at"), + avatarPath = item.nullableString("avatar_path"), + ) + } + }.orEmpty() + val removals = batchJson?.optJSONArray("removals")?.let { array -> + List(array.length()) { index -> array.getString(index) } + }.orEmpty() + val batch = batchJson?.let { + NativeNotificationBatch( + additions = additions, + removals = removals, + badgeCount = it.optLong("badge_count"), + completed = it.optBoolean("completed"), + ) + } + val fallback = root.optJSONObject("fallback")?.let { + NativeNotificationPresentation( + title = it.getString("title"), + body = it.getString("body"), + ) + } + return NativeNotificationResponse(root.optBoolean("ok"), batch, fallback) + } + } +} + +private fun JSONObject.nullableString(key: String): String? = + if (isNull(key)) null else optString(key).takeIf(String::isNotEmpty) diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/NotificationTapChannel.kt b/android/app/src/main/kotlin/eu/twonly/notifications/NotificationTapChannel.kt new file mode 100644 index 00000000..ba11b8db --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/notifications/NotificationTapChannel.kt @@ -0,0 +1,83 @@ +package eu.twonly.notifications + +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationManagerCompat +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +internal fun nativeNotificationId(value: String): Int = value.hashCode() and Int.MAX_VALUE + +/** + * Forwards taps on natively rendered notifications into Flutter. + * + * Only the opaque conversation identifier travels across this channel; the + * route itself is built in Dart so Kotlin never duplicates Flutter routing. + */ +object NotificationTapChannel { + private const val CHANNEL = "eu.twonly/notificationTap" + const val EXTRA_CONVERSATION_ID = "conversation_id" + + private var channel: MethodChannel? = null + private var pendingConversationId: String? = null + private var pendingLaunch = false + + fun configure(flutterEngine: FlutterEngine, context: Context) { + val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call, result -> + when (call.method) { + "consumeInitialNotification" -> { + val launched = pendingLaunch + val conversationId = pendingConversationId + pendingLaunch = false + pendingConversationId = null + result.success( + if (launched) { + mapOf(EXTRA_CONVERSATION_ID to conversationId) + } else { + null + }, + ) + } + "cancelNotifications" -> { + val notificationIds = call.argument>("notification_ids").orEmpty() + val manager = NotificationManagerCompat.from(context.applicationContext) + notificationIds.forEach { manager.cancel(nativeNotificationId(it)) } + result.success(null) + } + else -> result.notImplemented() + } + } + this.channel = channel + } + + fun detach() { + channel?.setMethodCallHandler(null) + channel = null + } + + /** + * Records the launch intent before Flutter attaches. Called for both the + * cold-start intent and every `onNewIntent`; a live channel is notified + * immediately, otherwise the tap is buffered for `consumeInitialNotification`. + */ + fun handleIntent(intent: Intent?) { + if (intent?.hasExtra(EXTRA_CONVERSATION_ID) != true) return + val conversationId = + intent.getStringExtra(EXTRA_CONVERSATION_ID)?.takeIf(String::isNotEmpty) + // A tap must only route once, even if the activity is recreated with + // the same intent after a configuration change. + intent.removeExtra(EXTRA_CONVERSATION_ID) + + val channel = this.channel + if (channel == null) { + pendingLaunch = true + pendingConversationId = conversationId + return + } + channel.invokeMethod( + "onNotificationTapped", + mapOf(EXTRA_CONVERSATION_ID to conversationId), + ) + } +} diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyFirebaseMessagingService.kt b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyFirebaseMessagingService.kt new file mode 100644 index 00000000..c63e6bbd --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyFirebaseMessagingService.kt @@ -0,0 +1,44 @@ +package eu.twonly.notifications + +import android.util.Log +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +class TwonlyFirebaseMessagingService : FirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + if (message.data["kind"] != "message_wakeup" || message.data["version"] != "1") { + Log.w(TAG, "Ignoring unsupported opaque FCM payload") + return + } + + val request = OneTimeWorkRequestBuilder() + .setInputData(Data.Builder().putString(INPUT_MESSAGE_ID, message.messageId).build()) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + WorkManager.getInstance(applicationContext).enqueueUniqueWork( + UNIQUE_WORK, + ExistingWorkPolicy.APPEND_OR_REPLACE, + request, + ) + } + + override fun onNewToken(token: String) { + try { + val directory = applicationContext.filesDir.absolutePath + NativeNotificationBridge.storeFcmToken(directory, directory, token) + } catch (error: Throwable) { + Log.e(TAG, "Could not persist refreshed FCM token in Rust", error) + } + } + + private companion object { + const val TAG = "TwonlyFCM" + const val UNIQUE_WORK = "twonly-native-notification-drain" + const val INPUT_MESSAGE_ID = "fcm_message_id" + } +} diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt new file mode 100644 index 00000000..dc5f94d6 --- /dev/null +++ b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt @@ -0,0 +1,156 @@ +package eu.twonly.notifications + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat +import androidx.work.Worker +import androidx.work.WorkerParameters +import eu.twonly.MainActivity +import eu.twonly.R +import java.util.Locale +import org.json.JSONArray + +class TwonlyNotificationWorker( + appContext: Context, + params: WorkerParameters, +) : Worker(appContext, params) { + override fun doWork(): Result { + return try { + val directory = applicationContext.filesDir.absolutePath + val response = NativeNotificationResponse.parse( + NativeNotificationBridge.process( + directory, + directory, + Locale.getDefault().toLanguageTag(), + RUST_DEADLINE_MS, + ), + ) + ensureChannel() + val batch = response.batch + if (!response.ok || batch == null) { + // Rust could not reach the mailbox. Show the generic alert so a + // high-priority wake-up still produces a notification, and retry. + response.fallback?.let(::showFallback) + return if (runAttemptCount < MAX_RETRIES) Result.retry() else Result.success() + } + + val manager = NotificationManagerCompat.from(applicationContext) + batch.removals.forEach { manager.cancel(nativeNotificationId(it)) } + val delivered = batch.additions.filter { addition -> + showAddition(manager, addition) + }.map(NativeNotificationAddition::eventId) + if (delivered.isNotEmpty()) { + NativeNotificationBridge.acknowledge(JSONArray(delivered).toString()) + // Real notifications supersede a placeholder from an earlier attempt. + manager.cancel(FALLBACK_ID) + } + + // An empty batch is the normal outcome of a duplicate wake-up or of + // traffic that is not user visible, so it must not retry. Only an + // undrained mailbox is worth another attempt. + if (!batch.completed && runAttemptCount < MAX_RETRIES) { + Result.retry() + } else { + Result.success() + } + } catch (error: Throwable) { + Log.e(TAG, "Native notification processing failed", error) + if (runAttemptCount < MAX_RETRIES) Result.retry() else Result.failure() + } + } + + private fun showAddition( + manager: NotificationManagerCompat, + addition: NativeNotificationAddition, + ): Boolean { + val avatar = addition.avatarPath + ?.let(BitmapFactory::decodeFile) + ?.let(IconCompat::createWithBitmap) + val sender = Person.Builder() + .setName(addition.senderName) + .setKey(addition.senderId.toString()) + .setIcon(avatar) + .build() + val user = Person.Builder().setName(applicationLabel()).setKey("twonly-user").build() + val style = NotificationCompat.MessagingStyle(user) + .addMessage(addition.body, addition.createdAt * 1_000, sender) + val intent = Intent(applicationContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + // Only opaque identifiers cross into the activity; Dart owns routing. + putExtra( + NotificationTapChannel.EXTRA_CONVERSATION_ID, + addition.conversationId.orEmpty(), + ) + } + val pendingIntent = PendingIntent.getActivity( + applicationContext, + nativeNotificationId(addition.notificationId), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(addition.title) + .setContentText(addition.body) + .setStyle(style) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setGroup(addition.conversationId ?: addition.senderId.toString()) + .build() + return try { + manager.notify(nativeNotificationId(addition.notificationId), notification) + true + } catch (error: SecurityException) { + Log.w(TAG, "Notification permission is unavailable", error) + false + } + } + + private fun showFallback(presentation: NativeNotificationPresentation) { + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(presentation.title) + .setContentText(presentation.body) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .build() + try { + NotificationManagerCompat.from(applicationContext).notify(FALLBACK_ID, notification) + } catch (error: SecurityException) { + Log.w(TAG, "Notification permission is unavailable", error) + } + } + + private fun ensureChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val channel = NotificationChannel( + CHANNEL_ID, + applicationLabel(), + NotificationManager.IMPORTANCE_HIGH, + ) + applicationContext.getSystemService(NotificationManager::class.java) + .createNotificationChannel(channel) + } + + private fun applicationLabel(): String = + applicationContext.applicationInfo.loadLabel(applicationContext.packageManager).toString() + + private companion object { + const val TAG = "TwonlyNotification" + const val CHANNEL_ID = "twonly_messages_v2" + const val FALLBACK_ID = 0x74776F + const val MAX_RETRIES = 2 + const val RUST_DEADLINE_MS = 25_000L + } +} diff --git a/ios/NotificationService/NotificationService.entitlements b/ios/NotificationService/NotificationService.entitlements index 78e4d59c..e32e0256 100644 --- a/ios/NotificationService/NotificationService.entitlements +++ b/ios/NotificationService/NotificationService.entitlements @@ -2,6 +2,14 @@ + com.apple.developer.usernotifications.communication + + com.apple.developer.usernotifications.filtering + + com.apple.security.application-groups + + group.eu.twonly.runtime + keychain-access-groups $(AppIdentifierPrefix)eu.twonly.shared diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index ebf7b038..aaf6976b 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -1,326 +1,273 @@ -import CryptoKit import Foundation -import Security +import Intents import UserNotifications +import rust_lib_twonly -class NotificationService: UNNotificationServiceExtension { - - var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? - - override func didReceive( - _ request: UNNotificationRequest, - withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void - ) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - // Store the current timestamp in Keychain for iOS FCM messaging tracking - let nowMs = String(format: "%.0f", Date().timeIntervalSince1970 * 1000) - writeToKeychain(key: "last_fcm_message_timestamp", value: nowMs) - NSLog("Received APNs push notification, updated last_fcm_message_timestamp to \(nowMs)") - - if let bestAttemptContent = bestAttemptContent { - - guard bestAttemptContent.userInfo as? [String: Any] != nil, - let push_data = bestAttemptContent.userInfo["push_data"] as? String - else { - return contentHandler(bestAttemptContent) - } - - let data = getPushNotificationData(pushData: push_data) - - if data != nil { - if data!.title == "blocked" { - NSLog("Block message because user is blocked!") - // https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.usernotifications.filtering - return contentHandler(UNNotificationContent()) - } - bestAttemptContent.title = data!.title - bestAttemptContent.body = data!.body - bestAttemptContent.threadIdentifier = String(format: "%d", data!.notificationId) - } else { - NSLog("Could not decrypt message. Show default.") - bestAttemptContent.title = "\(bestAttemptContent.title)" - } - - contentHandler(bestAttemptContent) - } - } - - override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - contentHandler(bestAttemptContent) - } - } +private let runtimeAppGroup = "group.eu.twonly.runtime" +private struct NativeNotificationResponse: Decodable { + let ok: Bool + let batch: NativeNotificationBatch? + let error: String? } -func getPushNotificationData(pushData: String) -> ( - title: String, body: String, notificationId: Int64 -)? { +private struct NativeNotificationBatch: Decodable { + let additions: [NativeNotificationAddition] + let removals: [String] + let badgeCount: Int64 + let completed: Bool - guard let data = Data(base64Encoded: pushData) else { - NSLog("Failed to decode base64 string") - return nil - } - - do { - let pushData = try EncryptedPushNotification(serializedBytes: data) - - var pushNotification: PushNotification? - var pushUser: PushUser? - - // Check the keyId - if pushData.keyID == 0 { - let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)! - pushNotification = tryDecryptMessage(key: key, pushData: pushData) - } else { - let pushUsers = getPushUsers() - if pushUsers != nil { - for tryPushUser in pushUsers! { - for pushKey in tryPushUser.pushKeys { - if pushKey.id == pushData.keyID { - pushNotification = tryDecryptMessage( - key: pushKey.key, pushData: pushData) - if pushNotification != nil { - pushUser = tryPushUser - if isUUIDNewer(pushUser!.lastMessageID, pushNotification!.messageID) - { - //return ("blocked", "blocked", 0) - } - break - } - } - } - if pushUser != nil { break } - } - } else { - NSLog("pushKeys are empty") - } - } - - if pushUser?.blocked == true { - return ("blocked", "blocked", 0) - } - - // Handle the push notification based on the pushKind - if let pushNotification = pushNotification { - - if pushNotification.kind == .testNotification { - return ("Test Notification", "This is a test notification.", 0) - } else if pushUser != nil { - return ( - pushUser!.displayName, - getPushNotificationText(pushNotification: pushNotification, userKnown: true).0, pushUser!.userID - ) - } else { - let content = getPushNotificationText(pushNotification: pushNotification, userKnown: false) - return ( - content.1, content.0, 1 - ) - } - - } else { - NSLog("Failed to decrypt message or pushKind is nil") - } - return nil - } catch { - NSLog("Error decoding JSON: \(error)") - return nil - } + enum CodingKeys: String, CodingKey { + case additions, removals, completed + case badgeCount = "badge_count" + } } -func isUUIDNewer(_ uuid1: String, _ uuid2: String) -> Bool { - guard uuid1.count >= 8, uuid2.count >= 8 else { return true } - let hex1 = String(uuid1.prefix(8)) - let hex2 = String(uuid2.prefix(8)) - guard let timestamp1 = UInt32(hex1, radix: 16), - let timestamp2 = UInt32(hex2, radix: 16) - else { return true } - return timestamp1 > timestamp2 +private struct NativeNotificationAddition: Decodable { + let eventId: String + let notificationId: String + let conversationId: String? + let senderId: Int64 + let senderName: String + let title: String + let body: String + let conversationName: String? + let isGroup: Bool + let messageId: String? + let kind: String + let content: String? + let createdAt: Int64 + let avatarPath: String? + + enum CodingKeys: String, CodingKey { + case kind, content, title, body + case eventId = "event_id" + case notificationId = "notification_id" + case conversationId = "conversation_id" + case senderId = "sender_id" + case senderName = "sender_name" + case conversationName = "conversation_name" + case isGroup = "is_group" + case messageId = "message_id" + case createdAt = "created_at" + case avatarPath = "avatar_path" + } } -func tryDecryptMessage(key: Data, pushData: EncryptedPushNotification) -> PushNotification? { +final class NotificationService: UNNotificationServiceExtension { + private let finishLock = NSLock() + private var hasFinished = false + private var contentHandler: ((UNNotificationContent) -> Void)? - do { - // Create a nonce for ChaChaPoly - let nonce = try ChaChaPoly.Nonce(data: pushData.nonce) + override func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + self.contentHandler = contentHandler - // Create a sealed box for ChaChaPoly - let sealedBox = try ChaChaPoly.SealedBox( - nonce: nonce, - ciphertext: pushData.ciphertext, - tag: pushData.mac - ) - - // Decrypt the data using the key - let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: key)) - - // Here you can determine the PushKind based on the decrypted message - return try PushNotification(serializedBytes: decryptedData) - } catch { - NSLog("Decryption failed: \(error)") + guard let runtimeDirectory = Self.runtimeDirectory() else { + suppress(reason: "shared runtime directory is unavailable") + return } - return nil -} - -func getPushUsers() -> [PushUser]? { - // Retrieve the data from secure storage (Keychain) - guard let pushUsersB64 = readFromKeychain(key: "push_keys_receiving") else { - NSLog("No data found for key: push_keys_receiving") - return nil - } - guard let pushUsersBytes = Data(base64Encoded: pushUsersB64) else { - NSLog("Failed to decode base64 push users") - return nil - } - - do { - let pushUsers = try PushUsers(serializedBytes: pushUsersBytes) - return pushUsers.users - } catch { - NSLog("Error decoding JSON: \(error)") - return nil - } -} - -// Helper function to read from Keychain -func readFromKeychain(key: String) -> String? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecAttrService as String: "flutter_secure_storage_service", - kSecReturnData as String: kCFBooleanTrue!, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared", // Use your access group - ] - - var dataTypeRef: AnyObject? = nil - let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) - - if status == errSecSuccess { - if let data = dataTypeRef as? Data { - return String(data: data, encoding: .utf8) - } - } - - return nil -} - -// Helper function to write to Keychain -func writeToKeychain(key: String, value: String) { - guard let data = value.data(using: .utf8) else { - NSLog("Failed to convert value to data for keychain key: \(key)") + DispatchQueue.global(qos: .userInitiated).async { + guard let response = Self.processWakeup(runtimeDirectory: runtimeDirectory) else { + self.suppress(reason: "notification worker returned no response") return + } + guard response.ok, + let batch = response.batch, + !batch.additions.isEmpty + else { + self.suppress(reason: response.error ?? "notification worker returned no messages") + return + } + self.render(batch: batch, original: request.content) } + } - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecAttrService as String: "flutter_secure_storage_service", - kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" - ] + override func serviceExtensionTimeWillExpire() { + suppress(reason: "notification service extension timed out") + } - // Delete existing item first to ensure a clean overwrite - SecItemDelete(query as CFDictionary) + private func render(batch: NativeNotificationBatch, original: UNNotificationContent) { + let center = UNUserNotificationCenter.current() + let group = DispatchGroup() + let stateLock = NSLock() + var deliveredEventIds: [String] = [] + var finalContent: UNNotificationContent = original - // Add the new item with background-compatible accessibility - var addQuery = query - addQuery[kSecValueData as String] = data - addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock - - let status = SecItemAdd(addQuery as CFDictionary, nil) - if status != errSecSuccess { - NSLog("Failed to write keychain item for key \(key): \(status)") - } else { - NSLog("Successfully wrote keychain item for key: \(key)") - } -} - -func getPushNotificationText(pushNotification: PushNotification, userKnown: Bool) -> (String, String) { - let systemLanguage = Locale.current.language.languageCode?.identifier ?? "en" // Get the current system language - - var pushNotificationText: [PushKind: String] = [:] - var title = "You" - var noTranslationFoundTitle = "You have a new message." - var noTranslationFoundBody = "Open twonly to learn more." - - // Define the messages based on the system language - if systemLanguage.contains("de") { // German - title = "Du" - noTranslationFoundTitle = "Du hast eine neue Nachricht." - noTranslationFoundBody = "Öffne twonly um mehr zu erfahren." - if (userKnown) { - pushNotificationText = [ - .text: "hat eine Nachricht{inGroup} gesendet.", - .twonly: "hat ein twonly{inGroup} gesendet.", - .video: "hat ein Video{inGroup} gesendet.", - .image: "hat ein Bild{inGroup} gesendet.", - .audio: "hat eine Sprachnachricht{inGroup} gesendet.", - .contactRequest: "möchte sich mit dir vernetzen.", - .acceptRequest: "ist jetzt mit dir vernetzt.", - .storedMediaFile: "hat dein Bild gespeichert.", - .reaction: "hat auf dein Bild reagiert.", - .testNotification: "Das ist eine Testbenachrichtigung.", - .reopenedMedia: "hat dein Bild erneut geöffnet.", - .reactionToVideo: "hat mit {{content}} auf dein Video reagiert.", - .reactionToText: "hat mit {{content}} auf deinen Text reagiert.", - .reactionToImage: "hat mit {{content}} auf dein Bild reagiert.", - .reactionToAudio: "hat mit {{content}} auf deine Sprachnachricht reagiert.", - .response: "hat dir{inGroup} geantwortet.", - .addedToGroup: "hat dich zu \"{{content}}\" hinzugefügt.", - ] - } else { - pushNotificationText = [ - .contactRequest: "hast eine neue Kontaktanfrage erhalten.", - ] + for (index, addition) in batch.additions.enumerated() { + group.enter() + communicationContent(for: addition, badgeCount: batch.badgeCount, original: original) { + content in + let isFinal = index == batch.additions.count - 1 + if isFinal { + stateLock.lock() + finalContent = content + deliveredEventIds.append(addition.eventId) + stateLock.unlock() + group.leave() + return } - } else { - if (userKnown) { - pushNotificationText = [ - .text: "sent a message{inGroup}.", - .twonly: "sent a twonly{inGroup}.", - .video: "sent a video{inGroup}.", - .image: "sent an image{inGroup}.", - .audio: "sent a voice message{inGroup}.", - .contactRequest: "wants to connect with you.", - .acceptRequest: "is now connected with you.", - .storedMediaFile: "has stored your image.", - .reaction: "has reacted to your image.", - .testNotification: "This is a test notification.", - .reopenedMedia: "has reopened your image.", - .reactionToVideo: "has reacted with {{content}} to your video.", - .reactionToText: "has reacted with {{content}} to your text.", - .reactionToImage: "has reacted with {{content}} to your image.", - .reactionToAudio: "has reacted with {{content}} to your voice message.", - .response: "has responded{inGroup}.", - .addedToGroup: "has added you to \"{{content}}\"", - ] - } else { - pushNotificationText = [ - .contactRequest: "have received a new contact request.", - ] + + let request = UNNotificationRequest( + identifier: addition.notificationId, + content: content, + trigger: nil + ) + center.add(request) { error in + if let error { + NSLog("Could not schedule Twonly notification: \(error)") + } else { + stateLock.lock() + deliveredEventIds.append(addition.eventId) + stateLock.unlock() + } + group.leave() } + } } - var content = pushNotificationText[pushNotification.kind] ?? "" - if (content == "") { - title = noTranslationFoundTitle - content = noTranslationFoundBody + group.notify(queue: .global(qos: .userInitiated)) { [weak self] in + stateLock.lock() + let eventIds = deliveredEventIds + let content = finalContent + stateLock.unlock() + Self.acknowledge(eventIds: eventIds) + self?.finish(with: content) } + } - if pushNotification.hasAdditionalContent { - content.replace("{{content}}", with: pushNotification.additionalContent) - content.replace("{inGroup}", with: " in {inGroup}") - content.replace("{inGroup}", with: pushNotification.additionalContent) - } else { - content.replace("{inGroup}", with: "") + private func communicationContent( + for addition: NativeNotificationAddition, + badgeCount: Int64, + original: UNNotificationContent, + completion: @escaping (UNNotificationContent) -> Void + ) { + let mutable = (original.mutableCopy() as? UNMutableNotificationContent) + ?? UNMutableNotificationContent() + mutable.title = addition.title + mutable.body = addition.body + mutable.threadIdentifier = addition.conversationId ?? String(addition.senderId) + mutable.badge = NSNumber(value: badgeCount) + mutable.sound = .default + var userInfo = mutable.userInfo + if let conversationId = addition.conversationId { + userInfo["conversation_id"] = conversationId } + userInfo["notification_id"] = addition.notificationId + mutable.userInfo = userInfo + + let avatar = addition.avatarPath + .flatMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) } + .map(INImage.init(imageData:)) + let sender = INPerson( + personHandle: INPersonHandle(value: String(addition.senderId), type: .unknown), + nameComponents: nil, + displayName: addition.senderName, + image: avatar, + contactIdentifier: nil, + customIdentifier: String(addition.senderId) + ) + let groupName = addition.isGroup + ? addition.conversationName.map(INSpeakableString.init(spokenPhrase:)) + : nil + let intent = INSendMessageIntent( + recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: addition.body, + speakableGroupName: groupName, + conversationIdentifier: addition.conversationId ?? String(addition.senderId), + serviceName: "Twonly", + sender: sender, + attachments: nil + ) + let interaction = INInteraction(intent: intent, response: nil) + interaction.direction = .incoming + interaction.donate { error in + if let error { + NSLog("Could not donate Twonly communication intent: \(error)") + completion(mutable) + return + } + do { + completion(try mutable.updating(from: intent)) + } catch { + NSLog("Could not create Twonly communication notification: \(error)") + completion(mutable) + } + } + } + + private func finish(with content: UNNotificationContent) { + finishLock.lock() + guard !hasFinished else { + finishLock.unlock() + return + } + hasFinished = true + let handler = contentHandler + contentHandler = nil + finishLock.unlock() + handler?(content) + } + + private func suppress(reason: String) { + NSLog("Suppressing Twonly wake-up notification: \(reason)") + finish(with: UNNotificationContent()) + } + + private static func runtimeDirectory() -> String? { + guard + let container = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: runtimeAppGroup + ) + else { return nil } + let directory = container.appendingPathComponent("runtime", isDirectory: true) + do { + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try (directory as NSURL).setResourceValue( + URLFileProtection.completeUntilFirstUserAuthentication, + forKey: .fileProtectionKey + ) + return directory.path + } catch { + NSLog("Could not open Twonly runtime directory: \(error)") + return nil + } + } + + private static func processWakeup(runtimeDirectory: String) -> NativeNotificationResponse? { + let locale = Locale.current.identifier + let pointer = runtimeDirectory.withCString { databaseDirectory in + runtimeDirectory.withCString { dataDirectory in + locale.withCString { locale in + twonly_notification_process(databaseDirectory, dataDirectory, locale, 24_000) + } + } + } + guard let pointer else { return nil } + defer { twonly_notification_string_free(pointer) } + let json = String(cString: pointer) + do { + return try JSONDecoder().decode( + NativeNotificationResponse.self, + from: Data(json.utf8) + ) + } catch { + NSLog("Could not decode Twonly notification worker response: \(error)") + return nil + } + } + + private static func acknowledge(eventIds: [String]) { + guard !eventIds.isEmpty, let data = try? JSONEncoder().encode(eventIds), + let json = String(data: data, encoding: .utf8) + else { return } + let pointer = json.withCString { twonly_notification_acknowledge($0) } + guard let pointer else { return } + twonly_notification_string_free(pointer) + } - // Return the corresponding message or an empty string if not found - return (content, title) } diff --git a/ios/NotificationService/push_notification.pb.swift b/ios/NotificationService/push_notification.pb.swift deleted file mode 100644 index 09b28f19..00000000 --- a/ios/NotificationService/push_notification.pb.swift +++ /dev/null @@ -1,447 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: push_notification.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate nonisolated struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -nonisolated enum PushKind: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case reaction // = 0 - case response // = 1 - case text // = 2 - case video // = 3 - case twonly // = 4 - case image // = 5 - case contactRequest // = 6 - case acceptRequest // = 7 - case storedMediaFile // = 8 - case testNotification // = 9 - case reopenedMedia // = 10 - case reactionToVideo // = 11 - case reactionToText // = 12 - case reactionToImage // = 13 - case reactionToAudio // = 14 - case addedToGroup // = 15 - case audio // = 16 - case UNRECOGNIZED(Int) - - init() { - self = .reaction - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .reaction - case 1: self = .response - case 2: self = .text - case 3: self = .video - case 4: self = .twonly - case 5: self = .image - case 6: self = .contactRequest - case 7: self = .acceptRequest - case 8: self = .storedMediaFile - case 9: self = .testNotification - case 10: self = .reopenedMedia - case 11: self = .reactionToVideo - case 12: self = .reactionToText - case 13: self = .reactionToImage - case 14: self = .reactionToAudio - case 15: self = .addedToGroup - case 16: self = .audio - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .reaction: return 0 - case .response: return 1 - case .text: return 2 - case .video: return 3 - case .twonly: return 4 - case .image: return 5 - case .contactRequest: return 6 - case .acceptRequest: return 7 - case .storedMediaFile: return 8 - case .testNotification: return 9 - case .reopenedMedia: return 10 - case .reactionToVideo: return 11 - case .reactionToText: return 12 - case .reactionToImage: return 13 - case .reactionToAudio: return 14 - case .addedToGroup: return 15 - case .audio: return 16 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [PushKind] = [ - .reaction, - .response, - .text, - .video, - .twonly, - .image, - .contactRequest, - .acceptRequest, - .storedMediaFile, - .testNotification, - .reopenedMedia, - .reactionToVideo, - .reactionToText, - .reactionToImage, - .reactionToAudio, - .addedToGroup, - .audio, - ] - -} - -nonisolated struct EncryptedPushNotification: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var keyID: Int64 = 0 - - var nonce: Data = Data() - - var ciphertext: Data = Data() - - var mac: Data = Data() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -nonisolated struct PushNotification: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var kind: PushKind = .reaction - - var messageID: String { - get {_messageID ?? String()} - set {_messageID = newValue} - } - /// Returns true if `messageID` has been explicitly set. - var hasMessageID: Bool {self._messageID != nil} - /// Clears the value of `messageID`. Subsequent reads from it will return its default value. - mutating func clearMessageID() {self._messageID = nil} - - var additionalContent: String { - get {_additionalContent ?? String()} - set {_additionalContent = newValue} - } - /// Returns true if `additionalContent` has been explicitly set. - var hasAdditionalContent: Bool {self._additionalContent != nil} - /// Clears the value of `additionalContent`. Subsequent reads from it will return its default value. - mutating func clearAdditionalContent() {self._additionalContent = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _messageID: String? = nil - fileprivate var _additionalContent: String? = nil -} - -nonisolated struct PushUsers: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var users: [PushUser] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -nonisolated struct PushUser: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var userID: Int64 = 0 - - var displayName: String = String() - - var blocked: Bool = false - - var lastMessageID: String { - get {_lastMessageID ?? String()} - set {_lastMessageID = newValue} - } - /// Returns true if `lastMessageID` has been explicitly set. - var hasLastMessageID: Bool {self._lastMessageID != nil} - /// Clears the value of `lastMessageID`. Subsequent reads from it will return its default value. - mutating func clearLastMessageID() {self._lastMessageID = nil} - - var pushKeys: [PushKey] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _lastMessageID: String? = nil -} - -nonisolated struct PushKey: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var id: Int64 = 0 - - var key: Data = Data() - - var createdAtUnixTimestamp: Int64 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -nonisolated extension PushKind: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0REACTION\0\u{1}RESPONSE\0\u{1}TEXT\0\u{1}VIDEO\0\u{1}TWONLY\0\u{1}IMAGE\0\u{1}CONTACT_REQUEST\0\u{1}ACCEPT_REQUEST\0\u{1}STORED_MEDIA_FILE\0\u{1}TEST_NOTIFICATION\0\u{1}REOPENED_MEDIA\0\u{1}REACTION_TO_VIDEO\0\u{1}REACTION_TO_TEXT\0\u{1}REACTION_TO_IMAGE\0\u{1}REACTION_TO_AUDIO\0\u{1}ADDED_TO_GROUP\0\u{1}AUDIO\0") -} - -nonisolated extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "EncryptedPushNotification" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0") - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt64Field(value: &self.keyID) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.nonce) }() - case 3: try { try decoder.decodeSingularBytesField(value: &self.ciphertext) }() - case 4: try { try decoder.decodeSingularBytesField(value: &self.mac) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.keyID != 0 { - try visitor.visitSingularInt64Field(value: self.keyID, fieldNumber: 1) - } - if !self.nonce.isEmpty { - try visitor.visitSingularBytesField(value: self.nonce, fieldNumber: 2) - } - if !self.ciphertext.isEmpty { - try visitor.visitSingularBytesField(value: self.ciphertext, fieldNumber: 3) - } - if !self.mac.isEmpty { - try visitor.visitSingularBytesField(value: self.mac, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: EncryptedPushNotification, rhs: EncryptedPushNotification) -> Bool { - if lhs.keyID != rhs.keyID {return false} - if lhs.nonce != rhs.nonce {return false} - if lhs.ciphertext != rhs.ciphertext {return false} - if lhs.mac != rhs.mac {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -nonisolated extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "PushNotification" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0") - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }() - case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }() - case 3: try { try decoder.decodeSingularStringField(value: &self._additionalContent) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.kind != .reaction { - try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1) - } - try { if let v = self._messageID { - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - } }() - try { if let v = self._additionalContent { - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: PushNotification, rhs: PushNotification) -> Bool { - if lhs.kind != rhs.kind {return false} - if lhs._messageID != rhs._messageID {return false} - if lhs._additionalContent != rhs._additionalContent {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -nonisolated extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "PushUsers" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}users\0") - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.users) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.users.isEmpty { - try visitor.visitRepeatedMessageField(value: self.users, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: PushUsers, rhs: PushUsers) -> Bool { - if lhs.users != rhs.users {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -nonisolated extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "PushUser" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0") - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }() - case 4: try { try decoder.decodeSingularStringField(value: &self._lastMessageID) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.userID != 0 { - try visitor.visitSingularInt64Field(value: self.userID, fieldNumber: 1) - } - if !self.displayName.isEmpty { - try visitor.visitSingularStringField(value: self.displayName, fieldNumber: 2) - } - if self.blocked != false { - try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3) - } - try { if let v = self._lastMessageID { - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - } }() - if !self.pushKeys.isEmpty { - try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: PushUser, rhs: PushUser) -> Bool { - if lhs.userID != rhs.userID {return false} - if lhs.displayName != rhs.displayName {return false} - if lhs.blocked != rhs.blocked {return false} - if lhs._lastMessageID != rhs._lastMessageID {return false} - if lhs.pushKeys != rhs.pushKeys {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -nonisolated extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "PushKey" - static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0") - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt64Field(value: &self.id) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.key) }() - case 3: try { try decoder.decodeSingularInt64Field(value: &self.createdAtUnixTimestamp) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.id != 0 { - try visitor.visitSingularInt64Field(value: self.id, fieldNumber: 1) - } - if !self.key.isEmpty { - try visitor.visitSingularBytesField(value: self.key, fieldNumber: 2) - } - if self.createdAtUnixTimestamp != 0 { - try visitor.visitSingularInt64Field(value: self.createdAtUnixTimestamp, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: PushKey, rhs: PushKey) -> Bool { - if lhs.id != rhs.id {return false} - if lhs.key != rhs.key {return false} - if lhs.createdAtUnixTimestamp != rhs.createdAtUnixTimestamp {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/ios/Podfile b/ios/Podfile index 0ed93e4f..34ad70b0 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -97,6 +97,5 @@ post_install do |installer| end target 'NotificationService' do - pod 'SwiftProtobuf' - # pod 'Firebase/Messaging' + pod 'rust_lib_twonly', :path => '.symlinks/plugins/rust_lib_twonly/ios' end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 04f4a14f..b9c4642b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -93,8 +93,7 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - PromisesObjC (2.4.0) - - rust_lib_twonly (0.0.1): - - Flutter + - rust_lib_twonly (0.0.1) - screen_protector (1.5.1): - Flutter - ScreenProtectorKit (= 1.5.1) @@ -199,7 +198,7 @@ SPEC CHECKSUMS: nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520 + rust_lib_twonly: 6586fdf02e31cd8a3ad9f1ee84796da6bbf289bb screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150 ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71 SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf @@ -207,6 +206,6 @@ SPEC CHECKSUMS: SwiftProtobuf: d724b5145bfc609d9a49c1e3e3a3dabb07273ffb workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 -PODFILE CHECKSUM: 245e6d5f26c858edb6b99a7d972cc93ead4d55cf +PODFILE CHECKSUM: f83bbaaed0b8c29b006472e50864d40e54617e24 COCOAPODS: 1.17.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 60389e6f..054601de 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -14,6 +14,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -23,7 +24,6 @@ D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D2B2E0FF2F63819600E729C1 /* VideoCompressionChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2B2E0FE2F63819600E729C1 /* VideoCompressionChannel.swift */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,6 +91,7 @@ 70E8A5E1DA4031C0E3F86C77 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7FC59147CD9A45BFAC98EA05 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -115,7 +116,6 @@ E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -311,9 +311,6 @@ productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -335,6 +332,9 @@ D25D4D792EFF41DB0029F805 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -385,9 +385,6 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -419,6 +416,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -1319,12 +1319,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3a5a88ff..e6556149 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" + "revision" : "346daa9f46316aa372b35b317e18224acc2e9063", + "version" : "12.18.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", - "version" : "3.6.0" + "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", + "version" : "3.6.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" + "revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5", + "version" : "12.18.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" + "revision" : "ba3358d3c3dbae8ef230b58a46b97ad65e84e974", + "version" : "10.1.1" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" + "revision" : "92c8f6dc3ac375d6febdfcb3db68bc3d10633db3", + "version" : "8.1.3" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", - "version" : "5.3.0" + "revision" : "724a52eea6329b7e12d3ad8300d76ca9f3895fcc", + "version" : "5.3.1" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/nanopb.git", "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" } }, { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 88277029..d5bb7aef 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -22,6 +22,9 @@ import workmanager_apple WorkmanagerPlugin.setPluginRegistrantCallback { registry in GeneratedPluginRegistrant.register(with: registry) + // Background tasks call AppEnvironment.init() too, so this engine needs + // the runtime storage channel just as much as the implicit one. + RuntimeStorageChannel.register(with: registry) } WorkmanagerPlugin.registerPeriodicTask( @@ -51,6 +54,8 @@ import workmanager_apple func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + RuntimeStorageChannel.register(with: engineBridge.pluginRegistry) + NativeNotificationChannel.register(with: engineBridge.pluginRegistry) } override func userNotificationCenter( @@ -70,23 +75,139 @@ import workmanager_apple ) { NSLog("userNotificationCenter:willPresent") - /* - debugging NotificationService - let pushKeys = getPushKey(); - print(pushKeys) - - let bestAttemptContent = notification.request.content - - guard let _userInfo = bestAttemptContent.userInfo as? [String: Any], - let push_data = bestAttemptContent.userInfo["push_data"] as? String else { - return completionHandler([.alert, .sound]) - } - - let data = getPushNotificationData(pushDataJson: push_data) - print(data) - */ - completionHandler([.alert, .sound]) } -} \ No newline at end of file +} + +/// Withdraws only the native notifications whose message IDs Dart reports as +/// opened. The notification service extension's final alert keeps APNs' request +/// identifier, so both the identifier and our `notification_id` user-info field +/// have to be considered. +class NativeNotificationChannel { + private static let channelName = "eu.twonly/notificationTap" + private static let notificationIdsKey = "notification_ids" + + static func register(with registry: FlutterPluginRegistry) { + guard let registrar = registry.registrar(forPlugin: "TwonlyNativeNotifications") else { + return + } + let channel = FlutterMethodChannel( + name: channelName, + binaryMessenger: registrar.messenger() + ) + channel.setMethodCallHandler { call, result in + guard call.method == "cancelNotifications" else { + result(FlutterMethodNotImplemented) + return + } + guard + let arguments = call.arguments as? [String: Any], + let values = arguments[notificationIdsKey] as? [String] + else { + result( + FlutterError( + code: "invalid_notification_ids", + message: "notification_ids must be a list of strings", + details: nil + )) + return + } + removeNotifications(Set(values), completion: result) + } + } + + private static func removeNotifications( + _ notificationIds: Set, + completion: @escaping FlutterResult + ) { + guard !notificationIds.isEmpty else { + completion(nil) + return + } + let center = UNUserNotificationCenter.current() + center.getDeliveredNotifications { delivered in + let requestIds = delivered.compactMap { notification -> String? in + let request = notification.request + let notificationId = request.content.userInfo["notification_id"] as? String + return notificationIds.contains(request.identifier) + || notificationId.map(notificationIds.contains) == true + ? request.identifier + : nil + } + center.removeDeliveredNotifications(withIdentifiers: requestIds) + center.getPendingNotificationRequests { pending in + let pendingIds = pending.compactMap { request -> String? in + let notificationId = request.content.userInfo["notification_id"] as? String + return notificationIds.contains(request.identifier) + || notificationId.map(notificationIds.contains) == true + ? request.identifier + : nil + } + center.removePendingNotificationRequests(withIdentifiers: pendingIds) + DispatchQueue.main.async { + completion(nil) + } + } + } + } +} + +/// Hands Dart the App Group container shared between the app and its +/// extensions. Must be registered on every Flutter engine that runs +/// `AppEnvironment.init()`, not just the implicit one. +class RuntimeStorageChannel { + private static let appGroupIdentifier = "group.eu.twonly.runtime" + + static func register(with registry: FlutterPluginRegistry) { + guard let registrar = registry.registrar(forPlugin: "TwonlyRuntimeStorage") else { + return + } + register(with: registrar.messenger()) + } + + static func register(with messenger: FlutterBinaryMessenger) { + let channel = FlutterMethodChannel( + name: "eu.twonly/runtime_storage", + binaryMessenger: messenger + ) + channel.setMethodCallHandler { call, result in + guard call.method == "runtimeSupportDirectory" else { + result(FlutterMethodNotImplemented) + return + } + guard + let container = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupIdentifier + ) + else { + result( + FlutterError( + code: "runtime_app_group_unavailable", + message: "Could not open \(appGroupIdentifier)", + details: nil + )) + return + } + let runtimeDirectory = container.appendingPathComponent("runtime", isDirectory: true) + do { + try FileManager.default.createDirectory( + at: runtimeDirectory, + withIntermediateDirectories: true + ) + try (runtimeDirectory as NSURL).setResourceValue( + URLFileProtection.completeUntilFirstUserAuthentication, + forKey: .fileProtectionKey + ) + result(runtimeDirectory.path) + } catch { + result( + FlutterError( + code: "runtime_directory_failed", + message: error.localizedDescription, + details: nil + )) + } + } + } +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 96bed792..8e45f74f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -47,6 +47,10 @@ LSRequiresIPhoneOS + NSUserActivityTypes + + INSendMessageIntent + NSCameraUsageDescription Use your camera to make photos or videos and share them encrypted with your friends. NSFaceIDUsageDescription diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index dc9d4ccf..2c5fb61b 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,9 +8,12 @@ applinks:me.twonly.eu + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.eu.twonly.shareIntent + group.eu.twonly.runtime keychain-access-groups diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements index dc9d4ccf..2c5fb61b 100644 --- a/ios/Runner/RunnerDebug.entitlements +++ b/ios/Runner/RunnerDebug.entitlements @@ -8,9 +8,12 @@ applinks:me.twonly.eu + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.eu.twonly.shareIntent + group.eu.twonly.runtime keychain-access-groups diff --git a/ios/Runner/RunnerRelease.entitlements b/ios/Runner/RunnerRelease.entitlements index dc9d4ccf..2c5fb61b 100644 --- a/ios/Runner/RunnerRelease.entitlements +++ b/ios/Runner/RunnerRelease.entitlements @@ -8,9 +8,12 @@ applinks:me.twonly.eu + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.eu.twonly.shareIntent + group.eu.twonly.runtime keychain-access-groups diff --git a/lib/core/bridge.dart b/lib/core/bridge.dart index acd01aeb..e479d417 100644 --- a/lib/core/bridge.dart +++ b/lib/core/bridge.dart @@ -8,6 +8,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)]`: `AnnouncedUser`, `OtherPromotion` +// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `initialize_twonly_notification` Future initializeTwonlyFlutter({required InitConfig config}) => RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config); diff --git a/lib/core/bridge/wrapper/app_database.dart b/lib/core/bridge/wrapper/app_database.dart index 9cd4341e..b218f54e 100644 --- a/lib/core/bridge/wrapper/app_database.dart +++ b/lib/core/bridge/wrapper/app_database.dart @@ -52,6 +52,19 @@ class LegacyTableMigrationCount { class RustAppDatabase { const RustAppDatabase(); + /// Streams the tables Rust has committed to. + /// + /// Rust owns the connection, so writes it makes on its own never pass + /// through the Drift compatibility executor and cannot invalidate Drift's + /// query streams. Dart forwards each batch into `notifyUpdates` so + /// `watch()` keeps reflecting Rust-side writes. + /// + /// An empty list means "assume every table changed". It is sent right + /// after (re)subscribing, and whenever the broadcast channel drops + /// notifications, so Dart never silently keeps stale rows on screen. + static Stream> changes() => RustLib.instance.api + .crateBridgeWrapperAppDatabaseRustAppDatabaseChanges(); + static Future execute({ required String statement, required List arguments, diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index b4835415..c681f6ec 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -84,7 +84,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -1286774374; + int get rustContentHash => -1780439173; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -409,6 +409,8 @@ abstract class RustLibApi extends BaseApi { required List prekeys, }); + Stream> crateBridgeWrapperAppDatabaseRustAppDatabaseChanges(); + Future crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({ required String statement, @@ -3283,6 +3285,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ], ); + @override + Stream> crateBridgeWrapperAppDatabaseRustAppDatabaseChanges() { + final sink = RustStreamSink>(); + unawaited( + handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_StreamSink_list_String_Sse(sink, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 77, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperAppDatabaseRustAppDatabaseChangesConstMeta, + argValues: [sink], + apiImpl: this, + ), + ), + ); + return sink.stream; + } + + TaskConstMeta + get kCrateBridgeWrapperAppDatabaseRustAppDatabaseChangesConstMeta => + const TaskConstMeta( + debugName: "rust_app_database_changes", + argNames: ["sink"], + ); + @override Future crateBridgeWrapperAppDatabaseRustAppDatabaseExecute({ @@ -3298,7 +3337,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -3331,7 +3370,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -3364,7 +3403,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -3401,7 +3440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -3433,7 +3472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -3466,7 +3505,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 83, port: port_, ); }, @@ -3501,7 +3540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 84, port: port_, ); }, @@ -3533,7 +3572,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 85, port: port_, ); }, @@ -3571,7 +3610,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 86, port: port_, ); }, @@ -3604,7 +3643,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 87, port: port_, ); }, @@ -3642,7 +3681,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 88, port: port_, ); }, @@ -3679,7 +3718,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 89, port: port_, ); }, @@ -3716,7 +3755,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 90, port: port_, ); }, @@ -3754,7 +3793,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 91, port: port_, ); }, @@ -3792,7 +3831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 92, port: port_, ); }, @@ -3824,7 +3863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 93, port: port_, ); }, @@ -3857,7 +3896,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 94, port: port_, ); }, @@ -3889,7 +3928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 95, port: port_, ); }, @@ -3924,7 +3963,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 96, port: port_, ); }, @@ -3961,7 +4000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 97, port: port_, ); }, @@ -3993,7 +4032,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 98, port: port_, ); }, @@ -4025,7 +4064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 99, port: port_, ); }, @@ -4060,7 +4099,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 100, port: port_, ); }, @@ -4099,7 +4138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 101, port: port_, ); }, @@ -4136,7 +4175,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 102, port: port_, ); }, @@ -4166,7 +4205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 103, port: port_, ); }, @@ -4198,7 +4237,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 104, port: port_, ); }, @@ -4233,7 +4272,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 105, port: port_, ); }, @@ -4265,7 +4304,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 106, port: port_, ); }, @@ -4303,7 +4342,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 107, port: port_, ); }, @@ -4342,7 +4381,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 108, port: port_, ); }, @@ -4377,7 +4416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 109, port: port_, ); }, @@ -4412,7 +4451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 110, port: port_, ); }, @@ -4447,7 +4486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 111, port: port_, ); }, @@ -4480,7 +4519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 112, )!; }, codec: SseCodec( @@ -4520,7 +4559,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 113, port: port_, ); }, @@ -4565,7 +4604,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 114, port: port_, ); }, @@ -4595,7 +4634,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 115, port: port_, ); }, @@ -4628,7 +4667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 116, port: port_, ); }, @@ -4663,7 +4702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 117, port: port_, ); }, @@ -4995,6 +5034,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(); } + @protected + RustStreamSink> dco_decode_StreamSink_list_String_Sse( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + @protected String dco_decode_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5782,8 +5829,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { UserConfig dco_decode_user_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 58) - throw Exception('unexpected arr length: expect 58 but see ${arr.length}'); + if (arr.length != 59) + throw Exception('unexpected arr length: expect 59 but see ${arr.length}'); return UserConfig( userId: dco_decode_i_64(arr[0]), username: dco_decode_String(arr[1]), @@ -5845,9 +5892,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { passwordLessRecovery: dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]), fcmToken: dco_decode_opt_String(arr[54]), - currentSetupPage: dco_decode_opt_String(arr[55]), - skipSetupPages: dco_decode_bool(arr[56]), - hasZoomed: dco_decode_bool(arr[57]), + lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[55]), + currentSetupPage: dco_decode_opt_String(arr[56]), + skipSetupPages: dco_decode_bool(arr[57]), + hasZoomed: dco_decode_bool(arr[58]), ); } @@ -5912,6 +5960,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError('Unreachable ()'); } + @protected + RustStreamSink> sse_decode_StreamSink_list_String_Sse( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + throw UnimplementedError('Unreachable ()'); + } + @protected String sse_decode_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6938,6 +6994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_passwordLessRecovery = sse_decode_opt_box_autoadd_passwordless_recovery_config(deserializer); var var_fcmToken = sse_decode_opt_String(deserializer); + var var_lastFcmWakeupAt = sse_decode_opt_box_autoadd_i_64(deserializer); var var_currentSetupPage = sse_decode_opt_String(deserializer); var var_skipSetupPages = sse_decode_bool(deserializer); var var_hasZoomed = sse_decode_bool(deserializer); @@ -7000,6 +7057,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { isBackupEnabled: var_isBackupEnabled, passwordLessRecovery: var_passwordLessRecovery, fcmToken: var_fcmToken, + lastFcmWakeupAt: var_lastFcmWakeupAt, currentSetupPage: var_currentSetupPage, skipSetupPages: var_skipSetupPages, hasZoomed: var_hasZoomed, @@ -7175,6 +7233,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void sse_encode_StreamSink_list_String_Sse( + RustStreamSink> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String( + self.setupAndSerialize( + codec: SseCodec( + decodeSuccessData: sse_decode_list_String, + decodeErrorData: sse_decode_AnyhowException, + ), + ), + serializer, + ); + } + @protected void sse_encode_String(String self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -8112,6 +8187,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer, ); sse_encode_opt_String(self.fcmToken, serializer); + sse_encode_opt_box_autoadd_i_64(self.lastFcmWakeupAt, serializer); sse_encode_opt_String(self.currentSetupPage, serializer); sse_encode_bool(self.skipSetupPages, serializer); sse_encode_bool(self.hasZoomed, serializer); diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index e94d702e..3d1c6a89 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -82,6 +82,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RustStreamSink dco_decode_StreamSink_api_event_Sse(dynamic raw); + @protected + RustStreamSink> dco_decode_StreamSink_list_String_Sse( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); @@ -405,6 +410,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + RustStreamSink> sse_decode_StreamSink_list_String_Sse( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -828,6 +838,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_StreamSink_list_String_Sse( + RustStreamSink> self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 6ab4ed9c..fa5079d9 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -84,6 +84,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RustStreamSink dco_decode_StreamSink_api_event_Sse(dynamic raw); + @protected + RustStreamSink> dco_decode_StreamSink_list_String_Sse( + dynamic raw, + ); + @protected String dco_decode_String(dynamic raw); @@ -407,6 +412,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + RustStreamSink> sse_decode_StreamSink_list_String_Sse( + SseDeserializer deserializer, + ); + @protected String sse_decode_String(SseDeserializer deserializer); @@ -830,6 +840,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_StreamSink_list_String_Sse( + RustStreamSink> self, + SseSerializer serializer, + ); + @protected void sse_encode_String(String self, SseSerializer serializer); diff --git a/lib/core/user_config.dart b/lib/core/user_config.dart index 43aff654..d3fd8192 100644 --- a/lib/core/user_config.dart +++ b/lib/core/user_config.dart @@ -158,6 +158,11 @@ class UserConfig { bool isBackupEnabled; PasswordlessRecoveryConfig? passwordLessRecovery; String? fcmToken; + + /// Unix seconds of the last opaque FCM/APNs wake-up that reached the native + /// notification worker. Recorded in Rust because Flutter is no longer + /// started for background delivery on either platform. + PlatformInt64? lastFcmWakeupAt; String? currentSetupPage; bool skipSetupPages; bool hasZoomed; @@ -218,6 +223,7 @@ class UserConfig { required this.isBackupEnabled, this.passwordLessRecovery, this.fcmToken, + this.lastFcmWakeupAt, this.currentSetupPage, required this.skipSetupPages, required this.hasZoomed, @@ -280,6 +286,7 @@ class UserConfig { isBackupEnabled.hashCode ^ passwordLessRecovery.hashCode ^ fcmToken.hashCode ^ + lastFcmWakeupAt.hashCode ^ currentSetupPage.hashCode ^ skipSetupPages.hashCode ^ hasZoomed.hashCode; @@ -351,6 +358,7 @@ class UserConfig { isBackupEnabled == other.isBackupEnabled && passwordLessRecovery == other.passwordLessRecovery && fcmToken == other.fcmToken && + lastFcmWakeupAt == other.lastFcmWakeupAt && currentSetupPage == other.currentSetupPage && skipSetupPages == other.skipSetupPages && hasZoomed == other.hasZoomed; diff --git a/lib/globals.dart b/lib/globals.dart index 4b476b6b..45324b38 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'package:camera/camera.dart'; +import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:twonly/src/utils/log.dart'; final int isolateCallbackId = Random().nextInt(0x7FFFFFFF); class AppEnvironment { + static const _runtimeChannel = MethodChannel('eu.twonly/runtime_storage'); static late String cacheDir; static late String supportDir; @@ -18,11 +21,66 @@ class AppEnvironment { static Future init() async { if (_isInitialized) return; cacheDir = (await getApplicationCacheDirectory()).path; - supportDir = (await getApplicationSupportDirectory()).path; + final privateSupportDir = (await getApplicationSupportDirectory()).path; + if (Platform.isIOS) { + final sharedSupportDir = await _runtimeChannel.invokeMethod( + 'runtimeSupportDirectory', + ); + if (sharedSupportDir == null || sharedSupportDir.isEmpty) { + throw StateError('The iOS runtime App Group is unavailable.'); + } + await _migrateToSharedSupportDirectory( + Directory(privateSupportDir), + Directory(sharedSupportDir), + ); + supportDir = sharedSupportDir; + } else { + supportDir = privateSupportDir; + } Log.init(); _isInitialized = true; } + static Future _migrateToSharedSupportDirectory( + Directory source, + Directory destination, + ) async { + await destination.create(recursive: true); + final marker = File('${destination.path}/.runtime_storage_migrated_v1'); + if (await marker.exists() || !await source.exists()) return; + + await for (final entity in source.list(followLinks: false)) { + await _copyEntity(entity, destination.path); + } + await marker.writeAsString( + DateTime.now().toUtc().toIso8601String(), + flush: true, + ); + } + + static Future _copyEntity( + FileSystemEntity entity, + String destinationDirectory, + ) async { + final name = entity.uri.pathSegments.where((value) => value.isNotEmpty).last; + final destinationPath = '$destinationDirectory/$name'; + if (entity is Directory) { + final destination = Directory(destinationPath); + await destination.create(recursive: true); + await for (final child in entity.list(followLinks: false)) { + await _copyEntity(child, destination.path); + } + return; + } + if (entity is! File) return; + + final destination = File(destinationPath); + if (await destination.exists()) return; + final temporary = File('$destinationPath.migrating'); + await entity.copy(temporary.path); + await temporary.rename(destination.path); + } + static void initTesting({String? customCacheDir, String? customSupportDir}) { cacheDir = customCacheDir ?? '/tmp/twonly_cache'; supportDir = customSupportDir ?? '/tmp/twonly_support'; diff --git a/lib/main.dart b/lib/main.dart index cc8fdfbf..c24a9988 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,7 @@ import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/memories/memories.service.dart'; import 'package:twonly/src/services/migrations.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; +import 'package:twonly/src/services/notifications/native.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart'; @@ -102,6 +103,7 @@ void main() async { var storageError = await twonlyMinimumInitialization(); await FcmNotificationService.initStartup(); await setupPushNotification(); + NativeNotificationService.init(); var userExists = false; diff --git a/lib/src/callbacks/logging.callbacks.dart b/lib/src/callbacks/logging.callbacks.dart index 6b1a9c67..8f5a3c40 100644 --- a/lib/src/callbacks/logging.callbacks.dart +++ b/lib/src/callbacks/logging.callbacks.dart @@ -1,34 +1,73 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:logging/logging.dart'; import 'package:twonly/src/utils/log.dart'; +/// Matches ANSI escape sequences (CSI sequences, caret notation like ^[[3m or \^[[3m). +final _ansiRegex = RegExp(r'(?:\x1B|\\?\^\[)\[[0-?]*[ -/]*[@-~]'); + +/// Matches the plain `ShortEventFormatter` output from `rust/src/log.rs`: +/// `HH:MM:SS LEVEL dir/file.rs:12 `. +final _rustLogLine = RegExp( + r'^\d{2}:\d{2}:\d{2} (TRACE|DEBUG|INFO|WARN|ERROR) +(\S+:\d+) ?(.*)$', + dotAll: true, +); + +/// Rust levels mapped onto the `logging` levels the Dart side already prints. +/// `SHOUT` is what [Log.error] uses, so a Rust `ERROR` reads the same as a Dart +/// one. +const Map _levels = { + 'TRACE': Level.FINEST, + 'DEBUG': Level.FINE, + 'INFO': Level.INFO, + 'WARN': Level.WARNING, + 'ERROR': Level.SHOUT, +}; + class LoggingCallbacks { static Future> getStreamSink() async { final dartLogSink = RustStreamSink(); + // `stream` throws until flutter_rust_bridge has serialized the sink for + // Rust, which only happens once this function has returned. Poll for it + // instead of racing; buffered events are replayed on the first listen. + var attempts = 0; Timer.periodic(const Duration(milliseconds: 100), (timer) { + attempts++; try { - dartLogSink.stream.listen( - (log) { - if (log.contains('INFO ')) { - Log.info(log.split('INFO ')[1]); - } else if (log.contains('DEBUG ')) { - Log.info(log.split('DEBUG ')[1]); - } else if (kDebugMode && - !Platform.environment.containsKey('FLUTTER_TEST')) { - // ignore: avoid_print - print(log); - } - }, - ); + dartLogSink.stream.listen(_handleRustLog); timer.cancel(); - } catch (e) { - // stream not yet initialized + } catch (_) { + // Stream not yet initialized. + if (attempts >= 100) { + timer.cancel(); + Log.warn('Rust log sink never became available.'); + } } }); return dartLogSink; } + + @visibleForTesting + static void handleRustLog(String log) => _handleRustLog(log); + + static void _handleRustLog(String log) { + final sanitizedLog = log.replaceAll(_ansiRegex, ''); + final match = _rustLogLine.firstMatch(sanitizedLog); + if (match == null) { + // Not a formatted event (panic output, a continuation line, ...). + Log.warn(sanitizedLog); + return; + } + + // The level and the `file.rs:line` origin belong in the record itself, not + // repeated inside the message: the Dart call site here says nothing useful. + Log.forward( + level: _levels[match.group(1)] ?? Level.INFO, + source: match.group(2)!, + messageInput: match.group(3), + ); + } } diff --git a/lib/src/constants/secure_storage.keys.dart b/lib/src/constants/secure_storage.keys.dart index d8020454..a6da571c 100644 --- a/lib/src/constants/secure_storage.keys.dart +++ b/lib/src/constants/secure_storage.keys.dart @@ -10,9 +10,6 @@ class SecureStorageKeys { static const String userData = 'userData'; // Not required for backup... - static const String receivingPushKeys = 'push_keys_receiving'; - static const String sendingPushKeys = 'push_keys_sending'; - static const String lastFcmMessageTimestamp = 'last_fcm_message_timestamp'; static const String lastServerMessageTimestamp = 'last_server_message_timestamp'; } diff --git a/lib/src/database/rust_change_notifier.dart b/lib/src/database/rust_change_notifier.dart new file mode 100644 index 00000000..28705ffb --- /dev/null +++ b/lib/src/database/rust_change_notifier.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:twonly/core/bridge/wrapper/app_database.dart'; +import 'package:twonly/src/utils/log.dart'; + +/// Bridges Rust-side commits into Drift's stream query invalidation. +/// +/// Rust owns the SQLite connection. Statements Drift issues go through +/// `openRustAppDatabase` and invalidate its query streams as usual, but every +/// write Rust performs on its own bypasses that executor entirely — Drift never +/// learns about it, so `watch()` keeps serving stale rows and the UI does not +/// update until something else happens to touch the same table. +/// +/// Rust already broadcasts the tables it commits to (`notify_committed`); this +/// forwards those batches into [GeneratedDatabase.notifyUpdates]. +StreamSubscription> listenToRustDatabaseChanges( + GeneratedDatabase db, +) { + return RustAppDatabase.changes().listen( + (tables) { + // An empty batch means Rust could not tell us precisely what changed + // (fresh subscription, or dropped notifications). Invalidate everything + // rather than leaving the UI stale. + final updates = tables.isEmpty + ? db.allTables.map(TableUpdate.onTable).toSet() + : tables.map(TableUpdate.new).toSet(); + db.notifyUpdates(updates); + }, + onError: (Object error, StackTrace stackTrace) { + Log.error( + 'Rust database change stream failed', + error: error, + stackTrace: stackTrace, + ); + }, + ); +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 40eea8f7..74d0c6ce 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; @@ -9,6 +11,7 @@ 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/rust_change_notifier.dart'; import 'package:twonly/src/database/rust_query_executor.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; @@ -64,14 +67,27 @@ part 'twonly.db.g.dart'; ], ) class TwonlyDB extends _$TwonlyDB { - TwonlyDB([QueryExecutor? e]) - : super( - e ?? openRustAppDatabase(), - ); + TwonlyDB([QueryExecutor? e]) : super(e ?? openRustAppDatabase()) { + // Only the Rust-backed connection needs external change notifications. + // An explicit executor is a plain Drift-owned database (tests, and the + // legacy import in main.dart), where Drift already sees every write. + if (e == null) { + _rustChanges = listenToRustDatabaseChanges(this); + } + } // ignore: matching_super_parameters TwonlyDB.forTesting(DatabaseConnection super.connection); + StreamSubscription>? _rustChanges; + + @override + Future close() async { + await _rustChanges?.cancel(); + _rustChanges = null; + return super.close(); + } + @override int get schemaVersion => 25; diff --git a/lib/src/model/protobuf/client/generated/http_requests.pb.dart b/lib/src/model/protobuf/client/generated/http_requests.pb.dart index 768ce32b..633eca93 100644 --- a/lib/src/model/protobuf/client/generated/http_requests.pb.dart +++ b/lib/src/model/protobuf/client/generated/http_requests.pb.dart @@ -21,12 +21,12 @@ class TextMessage extends $pb.GeneratedMessage { factory TextMessage({ $fixnum.Int64? userId, $core.List<$core.int>? body, - $core.List<$core.int>? pushData, + $core.bool? wakeReceiver, }) { final result = create(); if (userId != null) result.userId = userId; if (body != null) result.body = body; - if (pushData != null) result.pushData = pushData; + if (wakeReceiver != null) result.wakeReceiver = wakeReceiver; return result; } @@ -46,8 +46,7 @@ class TextMessage extends $pb.GeneratedMessage { ..aInt64(1, _omitFieldNames ? '' : 'userId') ..a<$core.List<$core.int>>( 2, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY) - ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY) + ..aOB(4, _omitFieldNames ? '' : 'wakeReceiver') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -87,14 +86,14 @@ class TextMessage extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearBody() => $_clearField(2); - @$pb.TagNumber(3) - $core.List<$core.int> get pushData => $_getN(2); - @$pb.TagNumber(3) - set pushData($core.List<$core.int> value) => $_setBytes(2, value); - @$pb.TagNumber(3) - $core.bool hasPushData() => $_has(2); - @$pb.TagNumber(3) - void clearPushData() => $_clearField(3); + @$pb.TagNumber(4) + $core.bool get wakeReceiver => $_getBF(2); + @$pb.TagNumber(4) + set wakeReceiver($core.bool value) => $_setBool(2, value); + @$pb.TagNumber(4) + $core.bool hasWakeReceiver() => $_has(2); + @$pb.TagNumber(4) + void clearWakeReceiver() => $_clearField(4); } class UploadRequest extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/http_requests.pbjson.dart b/lib/src/model/protobuf/client/generated/http_requests.pbjson.dart index eef1036e..b606a565 100644 --- a/lib/src/model/protobuf/client/generated/http_requests.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/http_requests.pbjson.dart @@ -21,25 +21,17 @@ const TextMessage$json = { '2': [ {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, {'1': 'body', '3': 2, '4': 1, '5': 12, '10': 'body'}, - { - '1': 'push_data', - '3': 3, - '4': 1, - '5': 12, - '9': 0, - '10': 'pushData', - '17': true - }, + {'1': 'wake_receiver', '3': 4, '4': 1, '5': 8, '10': 'wakeReceiver'}, ], - '8': [ - {'1': '_push_data'}, + '9': [ + {'1': 3, '2': 4}, ], }; /// Descriptor for `TextMessage`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List textMessageDescriptor = $convert.base64Decode( 'CgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgCIAEoDFIEYm' - '9keRIgCglwdXNoX2RhdGEYAyABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YQ=='); + '9keRIjCg13YWtlX3JlY2VpdmVyGAQgASgIUgx3YWtlUmVjZWl2ZXJKBAgDEAQ='); @$core.Deprecated('Use uploadRequestDescriptor instead') const UploadRequest$json = { diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 6c7043fc..56305f9e 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -1140,98 +1140,6 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage { void clearDisplayName() => $_clearField(4); } -class EncryptedContent_PushKeys extends $pb.GeneratedMessage { - factory EncryptedContent_PushKeys({ - EncryptedContent_PushKeys_Type? type, - $fixnum.Int64? keyId, - $core.List<$core.int>? key, - $fixnum.Int64? createdAt, - }) { - final result = create(); - if (type != null) result.type = type; - if (keyId != null) result.keyId = keyId; - if (key != null) result.key = key; - if (createdAt != null) result.createdAt = createdAt; - return result; - } - - EncryptedContent_PushKeys._(); - - factory EncryptedContent_PushKeys.fromBuffer($core.List<$core.int> data, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromBuffer(data, registry); - factory EncryptedContent_PushKeys.fromJson($core.String json, - [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => - create()..mergeFromJson(json, registry); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'EncryptedContent.PushKeys', - createEmptyInstance: create) - ..aE(1, _omitFieldNames ? '' : 'type', - enumValues: EncryptedContent_PushKeys_Type.values) - ..aInt64(2, _omitFieldNames ? '' : 'keyId') - ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY) - ..aInt64(4, _omitFieldNames ? '' : 'createdAt') - ..hasRequiredFields = false; - - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - EncryptedContent_PushKeys clone() => deepCopy(); - @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') - EncryptedContent_PushKeys copyWith( - void Function(EncryptedContent_PushKeys) updates) => - super.copyWith((message) => updates(message as EncryptedContent_PushKeys)) - as EncryptedContent_PushKeys; - - @$core.override - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static EncryptedContent_PushKeys create() => EncryptedContent_PushKeys._(); - @$core.override - EncryptedContent_PushKeys createEmptyInstance() => create(); - @$core.pragma('dart2js:noInline') - static EncryptedContent_PushKeys getDefault() => _defaultInstance ??= - $pb.GeneratedMessage.$_defaultFor(create); - static EncryptedContent_PushKeys? _defaultInstance; - - @$pb.TagNumber(1) - EncryptedContent_PushKeys_Type get type => $_getN(0); - @$pb.TagNumber(1) - set type(EncryptedContent_PushKeys_Type value) => $_setField(1, value); - @$pb.TagNumber(1) - $core.bool hasType() => $_has(0); - @$pb.TagNumber(1) - void clearType() => $_clearField(1); - - @$pb.TagNumber(2) - $fixnum.Int64 get keyId => $_getI64(1); - @$pb.TagNumber(2) - set keyId($fixnum.Int64 value) => $_setInt64(1, value); - @$pb.TagNumber(2) - $core.bool hasKeyId() => $_has(1); - @$pb.TagNumber(2) - void clearKeyId() => $_clearField(2); - - @$pb.TagNumber(3) - $core.List<$core.int> get key => $_getN(2); - @$pb.TagNumber(3) - set key($core.List<$core.int> value) => $_setBytes(2, value); - @$pb.TagNumber(3) - $core.bool hasKey() => $_has(2); - @$pb.TagNumber(3) - void clearKey() => $_clearField(3); - - @$pb.TagNumber(4) - $fixnum.Int64 get createdAt => $_getI64(3); - @$pb.TagNumber(4) - set createdAt($fixnum.Int64 value) => $_setInt64(3, value); - @$pb.TagNumber(4) - $core.bool hasCreatedAt() => $_has(3); - @$pb.TagNumber(4) - void clearCreatedAt() => $_clearField(4); -} - class EncryptedContent_FlameSync extends $pb.GeneratedMessage { factory EncryptedContent_FlameSync({ $fixnum.Int64? flameCounter, @@ -1724,7 +1632,6 @@ class EncryptedContent extends $pb.GeneratedMessage { EncryptedContent_ContactUpdate? contactUpdate, EncryptedContent_ContactRequest? contactRequest, EncryptedContent_FlameSync? flameSync, - EncryptedContent_PushKeys? pushKeys, EncryptedContent_Reaction? reaction, EncryptedContent_TextMessage? textMessage, EncryptedContent_GroupCreate? groupCreate, @@ -1754,7 +1661,6 @@ class EncryptedContent extends $pb.GeneratedMessage { if (contactUpdate != null) result.contactUpdate = contactUpdate; if (contactRequest != null) result.contactRequest = contactRequest; if (flameSync != null) result.flameSync = flameSync; - if (pushKeys != null) result.pushKeys = pushKeys; if (reaction != null) result.reaction = reaction; if (textMessage != null) result.textMessage = textMessage; if (groupCreate != null) result.groupCreate = groupCreate; @@ -1813,8 +1719,6 @@ class EncryptedContent extends $pb.GeneratedMessage { subBuilder: EncryptedContent_ContactRequest.create) ..aOM(10, _omitFieldNames ? '' : 'flameSync', subBuilder: EncryptedContent_FlameSync.create) - ..aOM(11, _omitFieldNames ? '' : 'pushKeys', - subBuilder: EncryptedContent_PushKeys.create) ..aOM(12, _omitFieldNames ? '' : 'reaction', subBuilder: EncryptedContent_Reaction.create) ..aOM( @@ -1976,208 +1880,197 @@ class EncryptedContent extends $pb.GeneratedMessage { @$pb.TagNumber(10) EncryptedContent_FlameSync ensureFlameSync() => $_ensure(8); - @$pb.TagNumber(11) - EncryptedContent_PushKeys get pushKeys => $_getN(9); - @$pb.TagNumber(11) - set pushKeys(EncryptedContent_PushKeys value) => $_setField(11, value); - @$pb.TagNumber(11) - $core.bool hasPushKeys() => $_has(9); - @$pb.TagNumber(11) - void clearPushKeys() => $_clearField(11); - @$pb.TagNumber(11) - EncryptedContent_PushKeys ensurePushKeys() => $_ensure(9); - @$pb.TagNumber(12) - EncryptedContent_Reaction get reaction => $_getN(10); + EncryptedContent_Reaction get reaction => $_getN(9); @$pb.TagNumber(12) set reaction(EncryptedContent_Reaction value) => $_setField(12, value); @$pb.TagNumber(12) - $core.bool hasReaction() => $_has(10); + $core.bool hasReaction() => $_has(9); @$pb.TagNumber(12) void clearReaction() => $_clearField(12); @$pb.TagNumber(12) - EncryptedContent_Reaction ensureReaction() => $_ensure(10); + EncryptedContent_Reaction ensureReaction() => $_ensure(9); @$pb.TagNumber(13) - EncryptedContent_TextMessage get textMessage => $_getN(11); + EncryptedContent_TextMessage get textMessage => $_getN(10); @$pb.TagNumber(13) set textMessage(EncryptedContent_TextMessage value) => $_setField(13, value); @$pb.TagNumber(13) - $core.bool hasTextMessage() => $_has(11); + $core.bool hasTextMessage() => $_has(10); @$pb.TagNumber(13) void clearTextMessage() => $_clearField(13); @$pb.TagNumber(13) - EncryptedContent_TextMessage ensureTextMessage() => $_ensure(11); + EncryptedContent_TextMessage ensureTextMessage() => $_ensure(10); @$pb.TagNumber(14) - EncryptedContent_GroupCreate get groupCreate => $_getN(12); + EncryptedContent_GroupCreate get groupCreate => $_getN(11); @$pb.TagNumber(14) set groupCreate(EncryptedContent_GroupCreate value) => $_setField(14, value); @$pb.TagNumber(14) - $core.bool hasGroupCreate() => $_has(12); + $core.bool hasGroupCreate() => $_has(11); @$pb.TagNumber(14) void clearGroupCreate() => $_clearField(14); @$pb.TagNumber(14) - EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(12); + EncryptedContent_GroupCreate ensureGroupCreate() => $_ensure(11); @$pb.TagNumber(15) - EncryptedContent_GroupJoin get groupJoin => $_getN(13); + EncryptedContent_GroupJoin get groupJoin => $_getN(12); @$pb.TagNumber(15) set groupJoin(EncryptedContent_GroupJoin value) => $_setField(15, value); @$pb.TagNumber(15) - $core.bool hasGroupJoin() => $_has(13); + $core.bool hasGroupJoin() => $_has(12); @$pb.TagNumber(15) void clearGroupJoin() => $_clearField(15); @$pb.TagNumber(15) - EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(13); + EncryptedContent_GroupJoin ensureGroupJoin() => $_ensure(12); @$pb.TagNumber(16) - EncryptedContent_GroupUpdate get groupUpdate => $_getN(14); + EncryptedContent_GroupUpdate get groupUpdate => $_getN(13); @$pb.TagNumber(16) set groupUpdate(EncryptedContent_GroupUpdate value) => $_setField(16, value); @$pb.TagNumber(16) - $core.bool hasGroupUpdate() => $_has(14); + $core.bool hasGroupUpdate() => $_has(13); @$pb.TagNumber(16) void clearGroupUpdate() => $_clearField(16); @$pb.TagNumber(16) - EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(14); + EncryptedContent_GroupUpdate ensureGroupUpdate() => $_ensure(13); @$pb.TagNumber(17) - EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(15); + EncryptedContent_ResendGroupPublicKey get resendGroupPublicKey => $_getN(14); @$pb.TagNumber(17) set resendGroupPublicKey(EncryptedContent_ResendGroupPublicKey value) => $_setField(17, value); @$pb.TagNumber(17) - $core.bool hasResendGroupPublicKey() => $_has(15); + $core.bool hasResendGroupPublicKey() => $_has(14); @$pb.TagNumber(17) void clearResendGroupPublicKey() => $_clearField(17); @$pb.TagNumber(17) EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() => - $_ensure(15); + $_ensure(14); @$pb.TagNumber(18) - EncryptedContent_ErrorMessages get errorMessages => $_getN(16); + EncryptedContent_ErrorMessages get errorMessages => $_getN(15); @$pb.TagNumber(18) set errorMessages(EncryptedContent_ErrorMessages value) => $_setField(18, value); @$pb.TagNumber(18) - $core.bool hasErrorMessages() => $_has(16); + $core.bool hasErrorMessages() => $_has(15); @$pb.TagNumber(18) void clearErrorMessages() => $_clearField(18); @$pb.TagNumber(18) - EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16); + EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(15); @$pb.TagNumber(19) EncryptedContent_AdditionalDataMessage get additionalDataMessage => - $_getN(17); + $_getN(16); @$pb.TagNumber(19) set additionalDataMessage(EncryptedContent_AdditionalDataMessage value) => $_setField(19, value); @$pb.TagNumber(19) - $core.bool hasAdditionalDataMessage() => $_has(17); + $core.bool hasAdditionalDataMessage() => $_has(16); @$pb.TagNumber(19) void clearAdditionalDataMessage() => $_clearField(19); @$pb.TagNumber(19) EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() => - $_ensure(17); + $_ensure(16); @$pb.TagNumber(20) - EncryptedContent_TypingIndicator get typingIndicator => $_getN(18); + EncryptedContent_TypingIndicator get typingIndicator => $_getN(17); @$pb.TagNumber(20) set typingIndicator(EncryptedContent_TypingIndicator value) => $_setField(20, value); @$pb.TagNumber(20) - $core.bool hasTypingIndicator() => $_has(18); + $core.bool hasTypingIndicator() => $_has(17); @$pb.TagNumber(20) void clearTypingIndicator() => $_clearField(20); @$pb.TagNumber(20) - EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18); + EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(17); @$pb.TagNumber(21) - $core.List<$core.int> get senderUserDiscoveryVersion => $_getN(19); + $core.List<$core.int> get senderUserDiscoveryVersion => $_getN(18); @$pb.TagNumber(21) set senderUserDiscoveryVersion($core.List<$core.int> value) => - $_setBytes(19, value); + $_setBytes(18, value); @$pb.TagNumber(21) - $core.bool hasSenderUserDiscoveryVersion() => $_has(19); + $core.bool hasSenderUserDiscoveryVersion() => $_has(18); @$pb.TagNumber(21) void clearSenderUserDiscoveryVersion() => $_clearField(21); @$pb.TagNumber(22) - EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(20); + EncryptedContent_UserDiscoveryRequest get userDiscoveryRequest => $_getN(19); @$pb.TagNumber(22) set userDiscoveryRequest(EncryptedContent_UserDiscoveryRequest value) => $_setField(22, value); @$pb.TagNumber(22) - $core.bool hasUserDiscoveryRequest() => $_has(20); + $core.bool hasUserDiscoveryRequest() => $_has(19); @$pb.TagNumber(22) void clearUserDiscoveryRequest() => $_clearField(22); @$pb.TagNumber(22) EncryptedContent_UserDiscoveryRequest ensureUserDiscoveryRequest() => - $_ensure(20); + $_ensure(19); @$pb.TagNumber(23) - EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(21); + EncryptedContent_UserDiscoveryUpdate get userDiscoveryUpdate => $_getN(20); @$pb.TagNumber(23) set userDiscoveryUpdate(EncryptedContent_UserDiscoveryUpdate value) => $_setField(23, value); @$pb.TagNumber(23) - $core.bool hasUserDiscoveryUpdate() => $_has(21); + $core.bool hasUserDiscoveryUpdate() => $_has(20); @$pb.TagNumber(23) void clearUserDiscoveryUpdate() => $_clearField(23); @$pb.TagNumber(23) EncryptedContent_UserDiscoveryUpdate ensureUserDiscoveryUpdate() => - $_ensure(21); + $_ensure(20); @$pb.TagNumber(24) - EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(22); + EncryptedContent_KeyVerificationProof get keyVerificationProof => $_getN(21); @$pb.TagNumber(24) set keyVerificationProof(EncryptedContent_KeyVerificationProof value) => $_setField(24, value); @$pb.TagNumber(24) - $core.bool hasKeyVerificationProof() => $_has(22); + $core.bool hasKeyVerificationProof() => $_has(21); @$pb.TagNumber(24) void clearKeyVerificationProof() => $_clearField(24); @$pb.TagNumber(24) EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() => - $_ensure(22); + $_ensure(21); @$pb.TagNumber(25) - $core.bool get askForFriendPromotions => $_getBF(23); + $core.bool get askForFriendPromotions => $_getBF(22); @$pb.TagNumber(25) - set askForFriendPromotions($core.bool value) => $_setBool(23, value); + set askForFriendPromotions($core.bool value) => $_setBool(22, value); @$pb.TagNumber(25) - $core.bool hasAskForFriendPromotions() => $_has(23); + $core.bool hasAskForFriendPromotions() => $_has(22); @$pb.TagNumber(25) void clearAskForFriendPromotions() => $_clearField(25); @$pb.TagNumber(26) - EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(24); + EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(23); @$pb.TagNumber(26) set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) => $_setField(26, value); @$pb.TagNumber(26) - $core.bool hasPasswordlessRecovery() => $_has(24); + $core.bool hasPasswordlessRecovery() => $_has(23); @$pb.TagNumber(26) void clearPasswordlessRecovery() => $_clearField(26); @$pb.TagNumber(26) EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() => - $_ensure(24); + $_ensure(23); @$pb.TagNumber(27) EncryptedContent_PasswordLessRecoveryHeartbeat - get passwordlessRecoveryHeartbeat => $_getN(25); + get passwordlessRecoveryHeartbeat => $_getN(24); @$pb.TagNumber(27) set passwordlessRecoveryHeartbeat( EncryptedContent_PasswordLessRecoveryHeartbeat value) => $_setField(27, value); @$pb.TagNumber(27) - $core.bool hasPasswordlessRecoveryHeartbeat() => $_has(25); + $core.bool hasPasswordlessRecoveryHeartbeat() => $_has(24); @$pb.TagNumber(27) void clearPasswordlessRecoveryHeartbeat() => $_clearField(27); @$pb.TagNumber(27) EncryptedContent_PasswordLessRecoveryHeartbeat - ensurePasswordlessRecoveryHeartbeat() => $_ensure(25); + ensurePasswordlessRecoveryHeartbeat() => $_ensure(24); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/messages.pbenum.dart b/lib/src/model/protobuf/client/generated/messages.pbenum.dart index 7ff4b706..d98c74c6 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbenum.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbenum.dart @@ -169,25 +169,5 @@ class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum { const EncryptedContent_ContactUpdate_Type._(super.value, super.name); } -class EncryptedContent_PushKeys_Type extends $pb.ProtobufEnum { - static const EncryptedContent_PushKeys_Type REQUEST = - EncryptedContent_PushKeys_Type._(0, _omitEnumNames ? '' : 'REQUEST'); - static const EncryptedContent_PushKeys_Type UPDATE = - EncryptedContent_PushKeys_Type._(1, _omitEnumNames ? '' : 'UPDATE'); - - static const $core.List values = - [ - REQUEST, - UPDATE, - ]; - - static final $core.List _byValue = - $pb.ProtobufEnum.$_initByValueList(values, 1); - static EncryptedContent_PushKeys_Type? valueOf($core.int value) => - value < 0 || value >= _byValue.length ? null : _byValue[value]; - - const EncryptedContent_PushKeys_Type._(super.value, super.name); -} - const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 51481a50..41adb98a 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -124,23 +124,13 @@ const EncryptedContent$json = { '10': 'flameSync', '17': true }, - { - '1': 'push_keys', - '3': 11, - '4': 1, - '5': 11, - '6': '.EncryptedContent.PushKeys', - '9': 11, - '10': 'pushKeys', - '17': true - }, { '1': 'reaction', '3': 12, '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', - '9': 12, + '9': 11, '10': 'reaction', '17': true }, @@ -150,7 +140,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', - '9': 13, + '9': 12, '10': 'textMessage', '17': true }, @@ -160,7 +150,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupCreate', - '9': 14, + '9': 13, '10': 'groupCreate', '17': true }, @@ -170,7 +160,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupJoin', - '9': 15, + '9': 14, '10': 'groupJoin', '17': true }, @@ -180,7 +170,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupUpdate', - '9': 16, + '9': 15, '10': 'groupUpdate', '17': true }, @@ -190,7 +180,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ResendGroupPublicKey', - '9': 17, + '9': 16, '10': 'resendGroupPublicKey', '17': true }, @@ -200,7 +190,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ErrorMessages', - '9': 18, + '9': 17, '10': 'errorMessages', '17': true }, @@ -210,7 +200,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.AdditionalDataMessage', - '9': 19, + '9': 18, '10': 'additionalDataMessage', '17': true }, @@ -220,7 +210,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.TypingIndicator', - '9': 20, + '9': 19, '10': 'typingIndicator', '17': true }, @@ -230,7 +220,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.UserDiscoveryRequest', - '9': 21, + '9': 20, '10': 'userDiscoveryRequest', '17': true }, @@ -240,7 +230,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.UserDiscoveryUpdate', - '9': 22, + '9': 21, '10': 'userDiscoveryUpdate', '17': true }, @@ -250,7 +240,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.KeyVerificationProof', - '9': 23, + '9': 22, '10': 'keyVerificationProof', '17': true }, @@ -260,7 +250,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.PasswordLessRecovery', - '9': 24, + '9': 23, '10': 'passwordlessRecovery', '17': true }, @@ -270,7 +260,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.PasswordLessRecoveryHeartbeat', - '9': 25, + '9': 24, '10': 'passwordlessRecoveryHeartbeat', '17': true }, @@ -289,7 +279,6 @@ const EncryptedContent$json = { EncryptedContent_MediaUpdate$json, EncryptedContent_ContactRequest$json, EncryptedContent_ContactUpdate$json, - EncryptedContent_PushKeys$json, EncryptedContent_FlameSync$json, EncryptedContent_TypingIndicator$json, EncryptedContent_UserDiscoveryRequest$json, @@ -310,7 +299,6 @@ const EncryptedContent$json = { {'1': '_contact_update'}, {'1': '_contact_request'}, {'1': '_flame_sync'}, - {'1': '_push_keys'}, {'1': '_reaction'}, {'1': '_text_message'}, {'1': '_group_create'}, @@ -757,47 +745,6 @@ const EncryptedContent_ContactUpdate_Type$json = { ], }; -@$core.Deprecated('Use encryptedContentDescriptor instead') -const EncryptedContent_PushKeys$json = { - '1': 'PushKeys', - '2': [ - { - '1': 'type', - '3': 1, - '4': 1, - '5': 14, - '6': '.EncryptedContent.PushKeys.Type', - '10': 'type' - }, - {'1': 'key_id', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true}, - {'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true}, - { - '1': 'created_at', - '3': 4, - '4': 1, - '5': 3, - '9': 2, - '10': 'createdAt', - '17': true - }, - ], - '4': [EncryptedContent_PushKeys_Type$json], - '8': [ - {'1': '_key_id'}, - {'1': '_key'}, - {'1': '_created_at'}, - ], -}; - -@$core.Deprecated('Use encryptedContentDescriptor instead') -const EncryptedContent_PushKeys_Type$json = { - '1': 'Type', - '2': [ - {'1': 'REQUEST', '2': 0}, - {'1': 'UPDATE', '2': 1}, - ], -}; - @$core.Deprecated('Use encryptedContentDescriptor instead') const EncryptedContent_FlameSync$json = { '1': 'FlameSync', @@ -891,107 +838,102 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( '91cGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGVICFINY29udGFj' 'dFVwZGF0ZYgBARJOCg9jb250YWN0X3JlcXVlc3QYCSABKAsyIC5FbmNyeXB0ZWRDb250ZW50Lk' 'NvbnRhY3RSZXF1ZXN0SAlSDmNvbnRhY3RSZXF1ZXN0iAEBEj8KCmZsYW1lX3N5bmMYCiABKAsy' - 'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESPAoJcHVzaF9rZX' - 'lzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0gLUghwdXNoS2V5c4gBARI7Cghy' - 'ZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVhY3Rpb25IDFIIcmVhY3Rpb26IAQ' - 'ESRQoMdGV4dF9tZXNzYWdlGA0gASgLMh0uRW5jcnlwdGVkQ29udGVudC5UZXh0TWVzc2FnZUgN' - 'Ugt0ZXh0TWVzc2FnZYgBARJFCgxncm91cF9jcmVhdGUYDiABKAsyHS5FbmNyeXB0ZWRDb250ZW' - '50Lkdyb3VwQ3JlYXRlSA5SC2dyb3VwQ3JlYXRliAEBEj8KCmdyb3VwX2pvaW4YDyABKAsyGy5F' - 'bmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgPUglncm91cEpvaW6IAQESRQoMZ3JvdXBfdXBkYX' - 'RlGBAgASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cFVwZGF0ZUgQUgtncm91cFVwZGF0ZYgB' - 'ARJiChdyZXNlbmRfZ3JvdXBfcHVibGljX2tleRgRIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUm' - 'VzZW5kR3JvdXBQdWJsaWNLZXlIEVIUcmVzZW5kR3JvdXBQdWJsaWNLZXmIAQESSwoOZXJyb3Jf' - 'bWVzc2FnZXMYEiABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXNIElINZXJyb3' - 'JNZXNzYWdlc4gBARJkChdhZGRpdGlvbmFsX2RhdGFfbWVzc2FnZRgTIAEoCzInLkVuY3J5cHRl' - 'ZENvbnRlbnQuQWRkaXRpb25hbERhdGFNZXNzYWdlSBNSFWFkZGl0aW9uYWxEYXRhTWVzc2FnZY' - 'gBARJRChB0eXBpbmdfaW5kaWNhdG9yGBQgASgLMiEuRW5jcnlwdGVkQ29udGVudC5UeXBpbmdJ' - 'bmRpY2F0b3JIFFIPdHlwaW5nSW5kaWNhdG9yiAEBEmEKFnVzZXJfZGlzY292ZXJ5X3JlcXVlc3' - 'QYFiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlVzZXJEaXNjb3ZlcnlSZXF1ZXN0SBVSFHVzZXJE' - 'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3' - 'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl' - 'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk' - 'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3' - 'b3JkbGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUm' - 'Vjb3ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292' - 'ZXJ5X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3' - 'ZlcnlIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQEalgIKDUVy' - 'cm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYW' - 'dlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2Vp' - 'cHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU' - '5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lP' - 'Tl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQ' - 'oLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJs' - 'aWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3' - 'VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tl' - 'eRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3' - 'VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoT' - 'YWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZX' - 'dfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNz' - 'YWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck' - '1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25h' - 'bWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGq8BCgtUZXh0TW' - 'Vzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRl' - 'eHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbW' - 'Vzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdGVfbWVzc2FnZV9p' - 'ZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg' - '9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgD' - 'IAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbm' - 'FsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9u' - 'EioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAi' - 'ABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgK' - 'BHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZR' - 'IvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVs' - 'dGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZH' - 'MSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1w' - 'Ii0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3Nlbm' - 'Rlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgB' - 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk' - '1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANI' - 'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYX' - 'Rpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0' - 'aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBAR' - 'IqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRp' - 'b25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKA' - 'xIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5' - 'cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdG' - 'lvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoF' - 'VklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0X2luX21pbGxpc2' - 'Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW5CEQoPX2VuY3J5' - 'cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9ub25jZUIaChhfYW' - 'RkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5F' - 'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2' - 'VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RP' - 'UkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYAS' - 'ABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlw' - 'ZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNvbnRhY3RVcGRhdG' - 'USOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0' - 'eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcmVzc2' - 'VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3BsYXlfbmFtZRgE' - 'IAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAU' - 'IYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZGlzcGxheV9uYW1l' - 'GtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy' - '5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVID' - 'a2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUk' - 'VRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3JlYXRlZF9hdBqv' - 'AQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYX' - 'N0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8K' - 'C2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm' - '9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBp' - 'bmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZX' - 'N0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2Nv' - 'dmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW' - '9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3' - 'b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3Zlcn' - 'lTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyAB' - 'KANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZW' - 'NvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNf' - 'ZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2' - 'Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNz' - 'YWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdG' - 'VCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9y' - 'ZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW' - '5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9y' - 'X21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYX' - 'RvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRl' - 'QhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29mQhgKFl9wYXNzd29yZGxlc3NfcmVjb3ZlcnlCIg' - 'ogX3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZWFydGJlYXQ='); + 'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESOwoIcmVhY3Rpb2' + '4YDCABKAsyGi5FbmNyeXB0ZWRDb250ZW50LlJlYWN0aW9uSAtSCHJlYWN0aW9uiAEBEkUKDHRl' + 'eHRfbWVzc2FnZRgNIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuVGV4dE1lc3NhZ2VIDFILdGV4dE' + '1lc3NhZ2WIAQESRQoMZ3JvdXBfY3JlYXRlGA4gASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91' + 'cENyZWF0ZUgNUgtncm91cENyZWF0ZYgBARI/Cgpncm91cF9qb2luGA8gASgLMhsuRW5jcnlwdG' + 'VkQ29udGVudC5Hcm91cEpvaW5IDlIJZ3JvdXBKb2luiAEBEkUKDGdyb3VwX3VwZGF0ZRgQIAEo' + 'CzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBVcGRhdGVID1ILZ3JvdXBVcGRhdGWIAQESYgoXcm' + 'VzZW5kX2dyb3VwX3B1YmxpY19rZXkYESABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlJlc2VuZEdy' + 'b3VwUHVibGljS2V5SBBSFHJlc2VuZEdyb3VwUHVibGljS2V5iAEBEksKDmVycm9yX21lc3NhZ2' + 'VzGBIgASgLMh8uRW5jcnlwdGVkQ29udGVudC5FcnJvck1lc3NhZ2VzSBFSDWVycm9yTWVzc2Fn' + 'ZXOIAQESZAoXYWRkaXRpb25hbF9kYXRhX21lc3NhZ2UYEyABKAsyJy5FbmNyeXB0ZWRDb250ZW' + '50LkFkZGl0aW9uYWxEYXRhTWVzc2FnZUgSUhVhZGRpdGlvbmFsRGF0YU1lc3NhZ2WIAQESUQoQ' + 'dHlwaW5nX2luZGljYXRvchgUIAEoCzIhLkVuY3J5cHRlZENvbnRlbnQuVHlwaW5nSW5kaWNhdG' + '9ySBNSD3R5cGluZ0luZGljYXRvcogBARJhChZ1c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0GBYgASgL' + 'MiYuRW5jcnlwdGVkQ29udGVudC5Vc2VyRGlzY292ZXJ5UmVxdWVzdEgUUhR1c2VyRGlzY292ZX' + 'J5UmVxdWVzdIgBARJeChV1c2VyX2Rpc2NvdmVyeV91cGRhdGUYFyABKAsyJS5FbmNyeXB0ZWRD' + 'b250ZW50LlVzZXJEaXNjb3ZlcnlVcGRhdGVIFVITdXNlckRpc2NvdmVyeVVwZGF0ZYgBARJhCh' + 'ZrZXlfdmVyaWZpY2F0aW9uX3Byb29mGBggASgLMiYuRW5jcnlwdGVkQ29udGVudC5LZXlWZXJp' + 'ZmljYXRpb25Qcm9vZkgWUhRrZXlWZXJpZmljYXRpb25Qcm9vZogBARJgChVwYXNzd29yZGxlc3' + 'NfcmVjb3ZlcnkYGiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5' + 'SBdSFHBhc3N3b3JkbGVzc1JlY292ZXJ5iAEBEnwKH3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZW' + 'FydGJlYXQYGyABKAsyLy5FbmNyeXB0ZWRDb250ZW50LlBhc3N3b3JkTGVzc1JlY292ZXJ5SGVh' + 'cnRiZWF0SBhSHXBhc3N3b3JkbGVzc1JlY292ZXJ5SGVhcnRiZWF0iAEBGpYCCg1FcnJvck1lc3' + 'NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlw' + 'ZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQinA' + 'EKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF9SRVFV' + 'RVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05fT1VUX0' + '9GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0dyb3Vw' + 'Q3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGljX2tleR' + 'gEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE5hbWWI' + 'AQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkYASABKA' + 'xSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cFVwZGF0' + 'ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2FmZmVjdG' + 'VkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2dyb3Vw' + 'X25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2FnZXNfYW' + 'Z0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNl' + 'Y29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1lQikKJ1' + '9uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqvAQoLVGV4dE1lc3NhZ2US' + 'KgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgAS' + 'gJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2Vf' + 'aWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWQazgEKFU' + 'FkZGl0aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVy' + 'TWVzc2FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBH' + 'R5cGUSOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3Nh' + 'Z2VEYXRhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpkCghSZWFjdGlvbhIqChF0YX' + 'JnZXRfbWVzc2FnZV9pZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhQKBWVtb2ppGAIgASgJUgVl' + 'bW9qaRIWCgZyZW1vdmUYAyABKAhSBnJlbW92ZRq+AgoNTWVzc2FnZVVwZGF0ZRI4CgR0eXBlGA' + 'EgASgOMiQuRW5jcnlwdGVkQ29udGVudC5NZXNzYWdlVXBkYXRlLlR5cGVSBHR5cGUSLwoRc2Vu' + 'ZGVyX21lc3NhZ2VfaWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEBEj0KG211bHRpcGxlX3' + 'RhcmdldF9tZXNzYWdlX2lkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYWdlSWRzEhcKBHRl' + 'eHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcCItCgRUeX' + 'BlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhQKEl9zZW5kZXJfbWVz' + 'c2FnZV9pZEIHCgVfdGV4dBqFBgoFTWVkaWESKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3' + 'NlbmRlck1lc3NhZ2VJZBIwCgR0eXBlGAIgASgOMhwuRW5jcnlwdGVkQ29udGVudC5NZWRpYS5U' + 'eXBlUgR0eXBlEkYKHWRpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzGAMgASgDSABSGmRpc3' + 'BsYXlMaW1pdEluTWlsbGlzZWNvbmRziAEBEjcKF3JlcXVpcmVzX2F1dGhlbnRpY2F0aW9uGAQg' + 'ASgIUhZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW' + '1wEi0KEHF1b3RlX21lc3NhZ2VfaWQYBiABKAlIAVIOcXVvdGVNZXNzYWdlSWSIAQESKgoOZG93' + 'bmxvYWRfdG9rZW4YByABKAxIAlINZG93bmxvYWRUb2tlbogBARIqCg5lbmNyeXB0aW9uX2tleR' + 'gIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEioKDmVuY3J5cHRpb25fbWFjGAkgASgMSARSDWVu' + 'Y3J5cHRpb25NYWOIAQESLgoQZW5jcnlwdGlvbl9ub25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm' + '9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1l' + 'c3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEA' + 'ISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNC' + 'EwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2' + 'tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9u' + 'YWxfbWVzc2FnZV9kYXRhGqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdG' + 'VkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIg' + 'ASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEh' + 'QKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUu' + 'RW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUk' + 'VRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGqQCCg1Db250YWN0VXBkYXRlEjgKBHR5' + 'cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3Ch' + 'VhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIf' + 'Cgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCABKAlIAl' + 'ILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2' + 'YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVCDwoNX2Rpc3BsYXlfbmFtZRqvAQoJRm' + 'xhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYXN0X2Zs' + 'YW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3' + 'RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm9yY2VV' + 'cGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBpbmcSHQ' + 'oKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZXN0EicK' + 'D2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2NvdmVyeV' + 'VwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW9uUHJv' + 'b2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3b3JkTG' + 'Vzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3ZlcnlTZWNy' + 'ZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyABKANSCX' + 'RocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZWNvdmVy' + 'eUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNfZGlyZW' + 'N0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2Rpc2Nv' + 'dmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNzYWdlX3' + 'VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdGVCEgoQ' + 'X2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcmVhY3Rpb25CDwoNX3RleHRfbW' + 'Vzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2luQg8KDV9ncm91cF91cGRhdGVC' + 'GgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl9tZXNzYWdlc0IaChhfYWRkaX' + 'Rpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0b3JCGQoXX3VzZXJfZGlzY292' + 'ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZUIZChdfa2V5X3ZlcmlmaWNhdG' + 'lvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIKIF9wYXNzd29yZGxlc3NfcmVj' + 'b3ZlcnlfaGVhcnRiZWF0'); diff --git a/lib/src/model/protobuf/client/http_requests.proto b/lib/src/model/protobuf/client/http_requests.proto index a3ed59ce..ce9ce54d 100644 --- a/lib/src/model/protobuf/client/http_requests.proto +++ b/lib/src/model/protobuf/client/http_requests.proto @@ -4,7 +4,8 @@ package http_requests; message TextMessage { int64 user_id = 1; bytes body = 2; - optional bytes push_data = 3; + reserved 3; // was: bytes push_data + bool wake_receiver = 4; } message UploadRequest { diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index 14a527ce..bf67019c 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -710,7 +710,10 @@ Future _createUploadRequest(MediaFileService media) async { final messageOnSuccess = TextMessage() ..body = cipherText - ..userId = Int64(groupMember.contactId); + ..userId = Int64(groupMember.contactId) + // A media message is user visible, so the server may send the opaque + // FCM wake-up once the upload completes. + ..wakeReceiver = true; messagesOnSuccess.add(messageOnSuccess); downloadTokens.add(downloadToken); diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 8b436504..e70ac7f7 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -1,34 +1,4 @@ -import 'dart:async'; -import 'dart:math'; - import 'package:flutter_local_notifications/flutter_local_notifications.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - -Future customLocalPushNotification(String title, String msg) async { - final androidNotificationDetails = AndroidNotificationDetails( - '1', - 'System', - channelDescription: 'System messages.', - importance: Importance.high, - priority: Priority.high, - styleInformation: BigTextStyleInformation(msg), - icon: 'ic_launcher_foreground', - ); - - const darwinNotificationDetails = DarwinNotificationDetails(); - final notificationDetails = NotificationDetails( - android: androidNotificationDetails, - iOS: darwinNotificationDetails, - ); - - final id = Random.secure().nextInt(9999); - - await flutterLocalNotificationsPlugin.show( - id, - title, - msg, - notificationDetails, - ); -} diff --git a/lib/src/services/notifications/fcm.background.dart b/lib/src/services/notifications/fcm.background.dart deleted file mode 100644 index 01a39396..00000000 --- a/lib/src/services/notifications/fcm.background.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:async'; -import 'dart:io' show Platform; - -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; -import 'package:twonly/src/services/notifications/fcm.notifications.dart'; -import 'package:twonly/src/services/notifications/setup.notifications.dart'; -import 'package:twonly/src/utils/log.dart'; - -@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 FcmNotificationService.handleRemoteMessage(message); - - if (Platform.isAndroid) { - if (isInitialized) { - await backgroundFetch(lastExecutionInSecondsLimit: 3); - } - } else { - // make sure every thing run... - await Future.delayed(const Duration(milliseconds: 2000)); - } -} diff --git a/lib/src/services/notifications/fcm.notifications.dart b/lib/src/services/notifications/fcm.notifications.dart index 73fd37f1..7e6179ce 100644 --- a/lib/src/services/notifications/fcm.notifications.dart +++ b/lib/src/services/notifications/fcm.notifications.dart @@ -5,12 +5,8 @@ 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: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/fcm.background.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -19,14 +15,14 @@ import '../../../firebase_options.dart'; // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de class FcmNotificationService { + /// FCM is only an opaque wake-up transport now. Delivery is owned natively by + /// the iOS Notification Service Extension and by + /// `TwonlyFirebaseMessagingService` on Android, both of which call Rust + /// directly, so no Dart isolate or message listener is registered here. static Future initStartup() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); - - FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); - - FirebaseMessaging.onMessage.listen(handleRemoteMessage); } static Future initAfterUserLoaded() async { @@ -143,67 +139,6 @@ class FcmNotificationService { } } - static Future handleRemoteMessage(RemoteMessage message) async { - Log.info('handleRemoteMessage received message: ${message.messageId}'); - await _updateLastFcmMessageTimestamp(); - if (!Platform.isAndroid) { - Log.error('Got message in Dart while on iOS'); - } - if (message.notification != null && AppState.isAppInBackground) { - Log.error( - 'Got notification but app is in background, so the SDK already have shown the message.', - ); - return; - } - - // In scenarios like Android Doze Mode or aggressive background restrictions, the OS may kill - // or heavily restrict network access, preventing the WebSocket from connecting in time. - // By parsing the FCM data payload offline, we can instantly display the notification, which also - // prevents FCM from penalizing/downgrading the app's data message priority for failing to show a notification. - // This is just a workarround until the new Rust decryption is enrolled fully. - final pushDataString = message.data['push_data'] as String?; - if (pushDataString != null) { - final apiState = await RustApi.connectionState(); - if (apiState == ApiConnectionState.connected || - apiState == ApiConnectionState.authenticating || - apiState == ApiConnectionState.authenticated) { - Log.info('Got FCM message, but API is connected...'); - } else { - Log.info('Trying to connect to the API in the background.'); - - if (await backgroundFetch()) { - return; - } - } - } - - if (message.notification != null || message.data['title'] != null) { - final title = - message.notification?.title ?? message.data['title'] as String? ?? ''; - final body = - message.notification?.body ?? message.data['body'] as String? ?? ''; - await customLocalPushNotification(title, body); - } - } - - static Future _updateLastFcmMessageTimestamp() async { - const storage = FlutterSecureStorage(); - final nowMs = DateTime.now().millisecondsSinceEpoch.toString(); - try { - await storage.write( - key: SecureStorageKeys.lastFcmMessageTimestamp, - value: nowMs, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); - Log.info('Updated last FCM message timestamp to $nowMs'); - } catch (e) { - Log.error('Could not write last FCM message timestamp: $e'); - } - } - static Future updateLastServerMessageTimestamp() async { const storage = FlutterSecureStorage(); final nowMs = DateTime.now().millisecondsSinceEpoch.toString(); @@ -229,13 +164,6 @@ class FcmNotificationService { } const storage = FlutterSecureStorage(); try { - final lastFcmStr = await storage.read( - key: SecureStorageKeys.lastFcmMessageTimestamp, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); final lastServerStr = await storage.read( key: SecureStorageKeys.lastServerMessageTimestamp, iOptions: const IOSOptions( @@ -247,13 +175,12 @@ class FcmNotificationService { final now = DateTime.now(); final threeDaysAgo = now.subtract(const Duration(days: 3)); - DateTime? lastFcmTime; - if (lastFcmStr != null) { - final ms = int.tryParse(lastFcmStr); - if (ms != null) { - lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms); - } - } + // Recorded by the Rust notification worker, because neither platform + // starts Flutter for a background wake-up any more. + final lastFcmWakeup = userService.currentUser.lastFcmWakeupAt; + final lastFcmTime = lastFcmWakeup == null + ? null + : DateTime.fromMillisecondsSinceEpoch(lastFcmWakeup * 1000); if (lastFcmTime != null) { Log.info( diff --git a/lib/src/services/notifications/native.notifications.dart b/lib/src/services/notifications/native.notifications.dart new file mode 100644 index 00000000..80bdb905 --- /dev/null +++ b/lib/src/services/notifications/native.notifications.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:io' show Platform; + +import 'package:flutter/services.dart'; +import 'package:twonly/src/utils/log.dart'; + +/// Taps on notifications rendered natively (Android `MessagingStyle`) arrive +/// through a platform channel instead of the Firebase Messaging plugin, which +/// no longer owns background delivery. +/// +/// Only the opaque conversation identifier crosses the channel; the Flutter +/// route is built here so the native layer stays free of routing knowledge. +class NativeNotificationService { + static const MethodChannel _channel = MethodChannel( + 'eu.twonly/notificationTap', + ); + + static const String _conversationIdKey = 'conversation_id'; + static const String _notificationIdsKey = 'notification_ids'; + + static final StreamController _taps = + StreamController.broadcast(); + + /// Emits the conversation id of every notification tapped while the app is + /// running. A `null` value means the notification had no specific + /// conversation and should only open the chats tab. + static Stream get taps => _taps.stream; + + static void init() { + if (!Platform.isAndroid) return; + _channel.setMethodCallHandler((call) async { + if (call.method != 'onNotificationTapped') return; + _taps.add(_conversationIdOf(call.arguments)); + }); + } + + /// Returns the tap that launched the app, or `null` when the app was not + /// started from a native notification. The result is consumed once. + static Future<({String? conversationId})?> consumeInitialTap() async { + if (!Platform.isAndroid) return null; + try { + final result = await _channel.invokeMapMethod( + 'consumeInitialNotification', + ); + if (result == null) return null; + return (conversationId: _conversationIdOf(result)); + } catch (e) { + Log.error('Could not read the initial native notification: $e'); + return null; + } + } + + /// Withdraws notifications for messages that have just been opened. Native + /// notification identifiers are strings on iOS and stable hashes on Android, + /// so the conversion stays in the platform implementation. + static Future cancelNotifications( + Iterable notificationIds, + ) async { + if (!Platform.isAndroid && !Platform.isIOS) return; + final ids = notificationIds.where((id) => id.isNotEmpty).toSet().toList(); + if (ids.isEmpty) return; + try { + await _channel.invokeMethod('cancelNotifications', { + _notificationIdsKey: ids, + }); + } catch (e) { + Log.error('Could not withdraw opened-message notifications: $e'); + } + } + + static String? _conversationIdOf(Object? arguments) { + if (arguments is! Map) return null; + final conversationId = arguments[_conversationIdKey]; + if (conversationId is! String || conversationId.isEmpty) return null; + return conversationId; + } +} diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 9af0d49e..16e8f267 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -77,6 +77,17 @@ class Log { final message = filterLogMessage('$messageInput'); Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace); } + + /// Re-emits a record that was produced outside Dart, keeping the origin's + /// level and source location. Deriving either from the Dart call site would + /// only ever point back at the forwarding code. + static void forward({ + required Level level, + required String source, + required Object? messageInput, + }) { + Logger(source).log(level, filterLogMessage('$messageInput')); + } } Future loadLogFile() async { diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index 01956147..541d9669 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -15,7 +15,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/services/notifications/background.notifications.dart'; +import 'package:twonly/src/services/notifications/native.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/contact_labels.comp.dart'; @@ -354,10 +354,6 @@ class _ChatMessagesViewState extends State List groupActions, { bool reportOpened = false, }) async { - if (reportOpened && _isViewActive()) { - unawaited(flutterLocalNotificationsPlugin.cancelAll()); - } - for (final msg in newMessages) { if (_animationState.hasReceivedFirstBatch && !_animationState.knownMessageIds.contains(msg.messageId) && @@ -435,12 +431,7 @@ class _ChatMessagesViewState extends State _animationState.reportedOpenedMessageIds.addAll( openedMessages[contactId]!, ); - unawaited( - RustApi.notifyMessagesOpened( - contactId: contactId, - messageIds: openedMessages[contactId]!, - ), - ); + unawaited(_reportMessagesOpened(contactId, openedMessages[contactId]!)); } } @@ -472,6 +463,23 @@ class _ChatMessagesViewState extends State _updateGalleryItems(messages: storedMediaFiles); } + Future _reportMessagesOpened( + int contactId, + List messageIds, + ) async { + // Cancel once immediately for an already visible alert, and once after the + // durable outbox update to close the small race with a native push worker. + await NativeNotificationService.cancelNotifications(messageIds); + try { + await RustApi.notifyMessagesOpened( + contactId: contactId, + messageIds: messageIds, + ); + } finally { + await NativeNotificationService.cancelNotifications(messageIds); + } + } + void _updateGalleryItems({List? messages, bool force = false}) { final storedMediaMessages = messages ?? diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart index 671708c1..0488cb49 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -5,6 +7,7 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/notifications/native.notifications.dart'; import 'package:twonly/src/visual/elements/better_text.element.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart'; @@ -217,10 +220,7 @@ class _InChatAudioPlayerState extends State { _playerController.startPlayer(); if (widget.message.senderId != null && widget.message.openedAt == null) { - RustApi.notifyMessagesOpened( - contactId: widget.message.senderId!, - messageIds: [widget.message.messageId], - ); + unawaited(_notifyMessageOpened()); } } setState(() { @@ -259,6 +259,21 @@ class _InChatAudioPlayerState extends State { ], ); } + + Future _notifyMessageOpened() async { + final senderId = widget.message.senderId; + if (senderId == null) return; + final messageIds = [widget.message.messageId]; + await NativeNotificationService.cancelNotifications(messageIds); + try { + await RustApi.notifyMessagesOpened( + contactId: senderId, + messageIds: messageIds, + ); + } finally { + await NativeNotificationService.cancelNotifications(messageIds); + } + } } String formatMsToMinSec(int milliseconds) { diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 6c63694a..64463681 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mutex/mutex.dart'; import 'package:screen_protector/screen_protector.dart'; -import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; @@ -19,7 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/services/notifications/background.notifications.dart'; +import 'package:twonly/src/services/notifications/native.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; @@ -133,12 +132,6 @@ class _MediaViewerViewState extends State { final Mutex _messageUpdateLock = Mutex(); - bool _isViewActive() { - if (!mounted) return false; - return !AppState.isAppInBackground && - (ModalRoute.of(context)?.isCurrent ?? false); - } - Future listenForUnopenedMedia(bool firstRun) async { _subscription = twonlyDB.messagesDao .watchMediaNotOpened(widget.group.groupId) @@ -256,10 +249,6 @@ class _MediaViewerViewState extends State { showSendTextMessageInput = false; }); - if (_isViewActive()) { - unawaited(flutterLocalNotificationsPlugin.cancelAll()); - } - final stream = twonlyDB.mediaFilesDao.watchMedia( allMediaFiles.first.mediaId!, ); @@ -416,10 +405,15 @@ class _MediaViewerViewState extends State { markAsOpenMessageIDs = messageIds; } - await RustApi.notifyMessagesOpened( - contactId: currentMessage!.senderId!, - messageIds: markAsOpenMessageIDs, - ); + await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs); + try { + await RustApi.notifyMessagesOpened( + contactId: currentMessage!.senderId!, + messageIds: markAsOpenMessageIDs, + ); + } finally { + await NativeNotificationService.cancelNotifications(markAsOpenMessageIDs); + } } Future _setupVideoPlayer(MediaFileService mediaLocal) async { diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index b86a3c55..e43ac157 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -14,6 +14,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/notifications/native.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -50,6 +51,7 @@ class HomeViewState extends State with WidgetsBindingObserver { StreamSubscription? _onMessageOpenedAppSub; StreamSubscription? _homeViewPageIndexSub; StreamSubscription? _selectNotificationSub; + StreamSubscription? _nativeNotificationSub; StreamSubscription<(String, MediaType)>? _sharedMediaSub; static Uri? pendingSharedLink; @@ -113,6 +115,10 @@ class HomeViewState extends State with WidgetsBindingObserver { streamHomeViewPageIndex.add(0); }); + _nativeNotificationSub = NativeNotificationService.taps.listen( + _openNativeNotification, + ); + _sharedLinkSub = streamSharedLink.stream.listen((uri) { HomeViewState.pendingSharedLink = null; _mainCameraController.setSharedLinkForPreview(uri); @@ -192,7 +198,20 @@ class HomeViewState extends State with WidgetsBindingObserver { }); } + void _openNativeNotification(String? conversationId) { + Log.info('Opened app from a native push notification tap.'); + if (conversationId != null) { + routerProvider.go(Routes.chatsMessages(conversationId)); + } + streamHomeViewPageIndex.add(0); + } + Future _initAsync() async { + final initialNativeTap = await NativeNotificationService.consumeInitialTap(); + if (initialNativeTap != null) { + _openNativeNotification(initialNativeTap.conversationId); + } + final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); @@ -245,6 +264,7 @@ class HomeViewState extends State with WidgetsBindingObserver { _onMessageOpenedAppSub?.cancel(); _homeViewPageIndexSub?.cancel(); _selectNotificationSub?.cancel(); + _nativeNotificationSub?.cancel(); _disableCameraTimer?.cancel(); _mainCameraController.setState = null; _mainCameraController.closeCamera(); diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 53efd4b1..162cfff2 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -93,6 +93,7 @@ class SetupView extends StatefulWidget { class _SetupViewState extends State { StreamSubscription? _userUpdateStream; late UserDiscoverySetupState state; + bool _setupDone = false; @override void initState() { @@ -102,16 +103,27 @@ class _SetupViewState extends State { if (widget.onUpdate != null) { _userUpdateStream = userService.onUserUpdated.listen((u) { if (userService.currentUser.currentSetupPage == null) { - widget.onUpdate?.call(); + _notifySetupDone(); } }); } } + /// Notifies the parent exactly once. Further user updates must not trigger + /// another callback, otherwise a parent popping this view would pop the + /// route below it as well. + void _notifySetupDone() { + if (_setupDone) return; + _setupDone = true; + unawaited(_userUpdateStream?.cancel()); + _userUpdateStream = null; + widget.onUpdate?.call(); + } + @override void dispose() { - super.dispose(); _userUpdateStream?.cancel(); + super.dispose(); } @override @@ -184,7 +196,7 @@ class _SetupViewState extends State { await UserService.update( (u) => u.skipSetupPages = true, ); - widget.onUpdate?.call(); + _notifySetupDone(); }, variant: MyButtonVariant.text, child: Text( diff --git a/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart b/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart index 53851dae..8a474021 100644 --- a/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/finish_setup.comp.dart @@ -13,13 +13,18 @@ class FinishSetupComp extends StatefulWidget { class _FinishSetupCompState extends State { Future onTap() async { - await context.navPush( - SetupView( - onUpdate: () { - if (mounted) { - Navigator.pop(context); - } - }, + // Captured before pushing so the callback never resolves the navigator + // through this widget's context, which lives below the pushed route. + final navigator = Navigator.of(context); + await navigator.push( + MaterialPageRoute( + builder: (_) => SetupView( + onUpdate: () { + if (mounted && navigator.canPop()) { + navigator.pop(); + } + }, + ), ), ); } diff --git a/lib/src/visual/views/settings/developer/informations.view.dart b/lib/src/visual/views/settings/developer/informations.view.dart index 6faaa7ec..7383df43 100644 --- a/lib/src/visual/views/settings/developer/informations.view.dart +++ b/lib/src/visual/views/settings/developer/informations.view.dart @@ -14,7 +14,6 @@ class DeveloperInformationsView extends StatefulWidget { } class _DeveloperInformationsViewState extends State { - String? _lastFcmTimestamp; String? _lastServerTimestamp; @override @@ -26,13 +25,6 @@ class _DeveloperInformationsViewState extends State { Future _loadInformations({bool showFeedback = false}) async { const storage = FlutterSecureStorage(); try { - final lastFcm = await storage.read( - key: SecureStorageKeys.lastFcmMessageTimestamp, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); final lastServer = await storage.read( key: SecureStorageKeys.lastServerMessageTimestamp, iOptions: const IOSOptions( @@ -42,7 +34,6 @@ class _DeveloperInformationsViewState extends State { ); if (mounted) { setState(() { - _lastFcmTimestamp = lastFcm; _lastServerTimestamp = lastServer; }); if (showFeedback) { @@ -56,6 +47,14 @@ class _DeveloperInformationsViewState extends State { } catch (_) {} } + String _formatFcmWakeup() { + final seconds = userService.currentUser.lastFcmWakeupAt; + if (seconds == null) return 'Never'; + return DateTime.fromMillisecondsSinceEpoch( + seconds * 1000, + ).toLocal().toString(); + } + String _formatTimestamp(String? timestampStr) { if (timestampStr == null) return 'Never'; final ms = int.tryParse(timestampStr); @@ -94,7 +93,7 @@ class _DeveloperInformationsViewState extends State { const Divider(), ListTile( title: const Text('Last FCM Message'), - subtitle: Text(_formatTimestamp(_lastFcmTimestamp)), + subtitle: Text(_formatFcmWakeup()), ), ListTile( title: const Text('Last Server Message'), diff --git a/rust/.sqlx/query-229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb.json b/rust/.sqlx/query-0095795ef631e1c4a775c8984bbbe0ca0c546dcc3ef1a608527fa9c42484f763.json similarity index 81% rename from rust/.sqlx/query-229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb.json rename to rust/.sqlx/query-0095795ef631e1c4a775c8984bbbe0ca0c546dcc3ef1a608527fa9c42484f763.json index c3eff5e9..b81aba03 100644 --- a/rust/.sqlx/query-229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb.json +++ b/rust/.sqlx/query-0095795ef631e1c4a775c8984bbbe0ca0c546dcc3ef1a608527fa9c42484f763.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n promotion_id,\n public_id,\n from_contact_id,\n threshold,\n announcement_share,\n public_key_verified_timestamp\n FROM user_discovery_other_promotions\n WHERE public_id = ?\n ", + "query": "\n SELECT promotion_id, public_id, from_contact_id, threshold,\n announcement_share, public_key_verified_timestamp\n FROM user_discovery_other_promotions\n WHERE public_id = ?\n ", "describe": { "columns": [ { @@ -82,5 +82,5 @@ true ] }, - "hash": "229b83c3c0777a900e71c71d83920a2b93f5df34f29921263f0c960fe0a34ceb" + "hash": "0095795ef631e1c4a775c8984bbbe0ca0c546dcc3ef1a608527fa9c42484f763" } diff --git a/rust/.sqlx/query-00e0c32ce73e5448c93de168ac43e7c46db5e3806197aeaf43850d7f8141f63d.json b/rust/.sqlx/query-00e0c32ce73e5448c93de168ac43e7c46db5e3806197aeaf43850d7f8141f63d.json new file mode 100644 index 00000000..e4f378b9 --- /dev/null +++ b/rust/.sqlx/query-00e0c32ce73e5448c93de168ac43e7c46db5e3806197aeaf43850d7f8141f63d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "00e0c32ce73e5448c93de168ac43e7c46db5e3806197aeaf43850d7f8141f63d" +} diff --git a/rust/.sqlx/query-02f92beaef5af931c37b62b9ebfbdcf0490b17343ba0e11427f572cc9c41b8b4.json b/rust/.sqlx/query-02f92beaef5af931c37b62b9ebfbdcf0490b17343ba0e11427f572cc9c41b8b4.json new file mode 100644 index 00000000..16557e36 --- /dev/null +++ b/rust/.sqlx/query-02f92beaef5af931c37b62b9ebfbdcf0490b17343ba0e11427f572cc9c41b8b4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(\n SELECT 1 FROM group_members gm\n WHERE gm.group_id = messages.group_id AND NOT EXISTS(\n SELECT 1 FROM message_actions ma\n WHERE ma.message_id = messages.message_id\n AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'\n )\n ) THEN ? ELSE NULL END\n WHERE message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "02f92beaef5af931c37b62b9ebfbdcf0490b17343ba0e11427f572cc9c41b8b4" +} diff --git a/rust/.sqlx/query-03c020a855d64733408f6cf11297505ecaa67c943819bcb3a5ed2a824e668ca6.json b/rust/.sqlx/query-03c020a855d64733408f6cf11297505ecaa67c943819bcb3a5ed2a824e668ca6.json new file mode 100644 index 00000000..0175749d --- /dev/null +++ b/rust/.sqlx/query-03c020a855d64733408f6cf11297505ecaa67c943819bcb3a5ed2a824e668ca6.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "03c020a855d64733408f6cf11297505ecaa67c943819bcb3a5ed2a824e668ca6" +} diff --git a/rust/.sqlx/query-051214ce0e6182d160aa7d3f039009d0fb62c946560da627d7de41e57a0d289d.json b/rust/.sqlx/query-051214ce0e6182d160aa7d3f039009d0fb62c946560da627d7de41e57a0d289d.json new file mode 100644 index 00000000..467a8306 --- /dev/null +++ b/rust/.sqlx/query-051214ce0e6182d160aa7d3f039009d0fb62c946560da627d7de41e57a0d289d.json @@ -0,0 +1,86 @@ +{ + "db_name": "SQLite", + "query": "SELECT media_id, type AS media_type, download_token,\n encryption_key, encryption_mac, encryption_nonce\n FROM media_files WHERE media_id = ?", + "describe": { + "columns": [ + { + "name": "media_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "media_id" + } + } + }, + { + "name": "media_type", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "type" + } + } + }, + { + "name": "download_token", + "ordinal": 2, + "type_info": "Blob", + "origin": { + "Table": { + "table": "media_files", + "name": "download_token" + } + } + }, + { + "name": "encryption_key", + "ordinal": 3, + "type_info": "Blob", + "origin": { + "Table": { + "table": "media_files", + "name": "encryption_key" + } + } + }, + { + "name": "encryption_mac", + "ordinal": 4, + "type_info": "Blob", + "origin": { + "Table": { + "table": "media_files", + "name": "encryption_mac" + } + } + }, + { + "name": "encryption_nonce", + "ordinal": 5, + "type_info": "Blob", + "origin": { + "Table": { + "table": "media_files", + "name": "encryption_nonce" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + true, + true, + true + ] + }, + "hash": "051214ce0e6182d160aa7d3f039009d0fb62c946560da627d7de41e57a0d289d" +} diff --git a/rust/.sqlx/query-1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3.json b/rust/.sqlx/query-06bde3698b9256418111975b79dc581ea72a4f68f7dfd4ceb63bfac45aa031b3.json similarity index 64% rename from rust/.sqlx/query-1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3.json rename to rust/.sqlx/query-06bde3698b9256418111975b79dc581ea72a4f68f7dfd4ceb63bfac45aa031b3.json index 2b0dfb36..8151df35 100644 --- a/rust/.sqlx/query-1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3.json +++ b/rust/.sqlx/query-06bde3698b9256418111975b79dc581ea72a4f68f7dfd4ceb63bfac45aa031b3.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT promotion\n FROM user_discovery_own_promotions\n WHERE version_id > ?\n ", + "query": "SELECT promotion FROM user_discovery_own_promotions\n WHERE contact_id = ? ORDER BY version_id DESC LIMIT 1", "describe": { "columns": [ { @@ -22,5 +22,5 @@ false ] }, - "hash": "1bf6e82ae32eff099bd28dc8e577247ebef69141bd7c6b887b5b11ed724e11b3" + "hash": "06bde3698b9256418111975b79dc581ea72a4f68f7dfd4ceb63bfac45aa031b3" } diff --git a/rust/.sqlx/query-09817a671f97e3aaeffcf37f8018c231462700155ea0b6b1914a8432cfeb6dcf.json b/rust/.sqlx/query-09817a671f97e3aaeffcf37f8018c231462700155ea0b6b1914a8432cfeb6dcf.json new file mode 100644 index 00000000..c88e7c4d --- /dev/null +++ b/rust/.sqlx/query-09817a671f97e3aaeffcf37f8018c231462700155ea0b6b1914a8432cfeb6dcf.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT sender_id, content, is_deleted_from_sender FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "sender_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + }, + { + "name": "content", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "content" + } + } + }, + { + "name": "is_deleted_from_sender", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "is_deleted_from_sender" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + false + ] + }, + "hash": "09817a671f97e3aaeffcf37f8018c231462700155ea0b6b1914a8432cfeb6dcf" +} diff --git a/rust/.sqlx/query-0a000bb74996bb70072c0b0dbb3bd54b690ec20c817a2cb1a29b0ac0fcc884ad.json b/rust/.sqlx/query-0a000bb74996bb70072c0b0dbb3bd54b690ec20c817a2cb1a29b0ac0fcc884ad.json deleted file mode 100644 index b44300fb..00000000 --- a/rust/.sqlx/query-0a000bb74996bb70072c0b0dbb3bd54b690ec20c817a2cb1a29b0ac0fcc884ad.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO messages(\n group_id,\n message_id,\n sender_id,\n type,\n content,\n quotes_message_id,\n created_at,\n ack_by_server\n ) VALUES (?, ?, ?, 'text', ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))\n ON CONFLICT(message_id) DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "0a000bb74996bb70072c0b0dbb3bd54b690ec20c817a2cb1a29b0ac0fcc884ad" -} diff --git a/rust/.sqlx/query-0a969e410875ea46fde7c9e7fa773b73da80d2a71736532a4bea6dd2efb43cbb.json b/rust/.sqlx/query-0a969e410875ea46fde7c9e7fa773b73da80d2a71736532a4bea6dd2efb43cbb.json new file mode 100644 index 00000000..61a951c1 --- /dev/null +++ b/rust/.sqlx/query-0a969e410875ea46fde7c9e7fa773b73da80d2a71736532a4bea6dd2efb43cbb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO user_discovery_user_relations (\n announced_user_id, from_contact_id, public_key_verified_timestamp\n ) VALUES (?, ?, ?)\n ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "0a969e410875ea46fde7c9e7fa773b73da80d2a71736532a4bea6dd2efb43cbb" +} diff --git a/rust/.sqlx/query-0cf3d4227521bf00d715c1f66b595d9a4943283ab72635c63d184aab40dc19d5.json b/rust/.sqlx/query-0cf3d4227521bf00d715c1f66b595d9a4943283ab72635c63d184aab40dc19d5.json new file mode 100644 index 00000000..28ad5e74 --- /dev/null +++ b/rust/.sqlx/query-0cf3d4227521bf00d715c1f66b595d9a4943283ab72635c63d184aab40dc19d5.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM reactions WHERE message_id = ? AND sender_id = ? AND emoji = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM reactions WHERE message_id = ? AND sender_id = ? AND emoji = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "0cf3d4227521bf00d715c1f66b595d9a4943283ab72635c63d184aab40dc19d5" +} diff --git a/rust/.sqlx/query-0dbbfbbf830b11f3e15c85e9e6004196d69f202929f836ac0c0500ea57804dfc.json b/rust/.sqlx/query-0dbbfbbf830b11f3e15c85e9e6004196d69f202929f836ac0c0500ea57804dfc.json new file mode 100644 index 00000000..3369178e --- /dev/null +++ b/rust/.sqlx/query-0dbbfbbf830b11f3e15c85e9e6004196d69f202929f836ac0c0500ea57804dfc.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT display_name FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "display_name", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "display_name" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "0dbbfbbf830b11f3e15c85e9e6004196d69f202929f836ac0c0500ea57804dfc" +} diff --git a/rust/.sqlx/query-0dd1dbaa350aa766bada77410aef0da125561fd388fad0b4943f124bc2e73600.json b/rust/.sqlx/query-0dd1dbaa350aa766bada77410aef0da125561fd388fad0b4943f124bc2e73600.json new file mode 100644 index 00000000..3fb1697b --- /dev/null +++ b/rust/.sqlx/query-0dd1dbaa350aa766bada77410aef0da125561fd388fad0b4943f124bc2e73600.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT last_flame_counter_change, flame_counter, max_flame_counter\n FROM groups\n WHERE group_id = ?\n ", + "describe": { + "columns": [ + { + "name": "last_flame_counter_change", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "last_flame_counter_change" + } + } + }, + { + "name": "flame_counter", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "flame_counter" + } + } + }, + { + "name": "max_flame_counter", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "max_flame_counter" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + false + ] + }, + "hash": "0dd1dbaa350aa766bada77410aef0da125561fd388fad0b4943f124bc2e73600" +} diff --git a/rust/.sqlx/query-0e921f020426da5e0c76cd3c555f37d2abad38c2959ea971ae3085d51ad68ccb.json b/rust/.sqlx/query-0e921f020426da5e0c76cd3c555f37d2abad38c2959ea971ae3085d51ad68ccb.json new file mode 100644 index 00000000..5bcbeeb0 --- /dev/null +++ b/rust/.sqlx/query-0e921f020426da5e0c76cd3c555f37d2abad38c2959ea971ae3085d51ad68ccb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE messages SET content = ?, modified_at = ?\n WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "0e921f020426da5e0c76cd3c555f37d2abad38c2959ea971ae3085d51ad68ccb" +} diff --git a/rust/.sqlx/query-0ee2e00e444da6707674388c8126e806a8ce02bd0008cc5774ca2d4e11aa5002.json b/rust/.sqlx/query-0ee2e00e444da6707674388c8126e806a8ce02bd0008cc5774ca2d4e11aa5002.json new file mode 100644 index 00000000..52d5624b --- /dev/null +++ b/rust/.sqlx/query-0ee2e00e444da6707674388c8126e806a8ce02bd0008cc5774ca2d4e11aa5002.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT user_id, recovery_contacts_secret_share FROM contacts\n WHERE recovery_contacts_secret_share IS NOT NULL\n AND (recovery_contacts_last_heartbeat IS NULL\n OR recovery_contacts_last_heartbeat <= ?)", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_id" + } + } + }, + { + "name": "recovery_contacts_secret_share", + "ordinal": 1, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_secret_share" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true + ] + }, + "hash": "0ee2e00e444da6707674388c8126e806a8ce02bd0008cc5774ca2d4e11aa5002" +} diff --git a/rust/.sqlx/query-10facb63e1057c6acfe8b60cfe5f6deb3535eed60977d917f57cae97bb220e30.json b/rust/.sqlx/query-10facb63e1057c6acfe8b60cfe5f6deb3535eed60977d917f57cae97bb220e30.json new file mode 100644 index 00000000..fc127651 --- /dev/null +++ b/rust/.sqlx/query-10facb63e1057c6acfe8b60cfe5f6deb3535eed60977d917f57cae97bb220e30.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_id, total_media_counter, last_flame_counter_change,\n last_flame_sync, flame_counter\n FROM groups WHERE last_flame_counter_change IS NOT NULL", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_id" + } + } + }, + { + "name": "total_media_counter", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "total_media_counter" + } + } + }, + { + "name": "last_flame_counter_change", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "last_flame_counter_change" + } + } + }, + { + "name": "last_flame_sync", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "last_flame_sync" + } + } + }, + { + "name": "flame_counter", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "flame_counter" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + true, + true, + false + ] + }, + "hash": "10facb63e1057c6acfe8b60cfe5f6deb3535eed60977d917f57cae97bb220e30" +} diff --git a/rust/.sqlx/query-117582dbddd40553a8a622ed65b24284d8fb91c141ba989e2e3f4846c93d2a4c.json b/rust/.sqlx/query-117582dbddd40553a8a622ed65b24284d8fb91c141ba989e2e3f4846c93d2a4c.json deleted file mode 100644 index dce8db06..00000000 --- a/rust/.sqlx/query-117582dbddd40553a8a622ed65b24284d8fb91c141ba989e2e3f4846c93d2a4c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET username = COALESCE(?, username),\n display_name = ?,\n avatar_svg_compressed = ?,\n sender_profile_counter = COALESCE(?, sender_profile_counter)\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "117582dbddd40553a8a622ed65b24284d8fb91c141ba989e2e3f4846c93d2a4c" -} diff --git a/rust/.sqlx/query-121378634faf12e63bdd7e116c8a0b24a9e20e39c41e11be8e98b32f7d0e8f61.json b/rust/.sqlx/query-121378634faf12e63bdd7e116c8a0b24a9e20e39c41e11be8e98b32f7d0e8f61.json new file mode 100644 index 00000000..d1dae8c6 --- /dev/null +++ b/rust/.sqlx/query-121378634faf12e63bdd7e116c8a0b24a9e20e39c41e11be8e98b32f7d0e8f61.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt)\n VALUES (?, ?, ?, 0)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "121378634faf12e63bdd7e116c8a0b24a9e20e39c41e11be8e98b32f7d0e8f61" +} diff --git a/rust/.sqlx/query-13e31c854a9086ef9fb5ae3a79ad5b915012fed2a79cf7275d74add38acff057.json b/rust/.sqlx/query-13e31c854a9086ef9fb5ae3a79ad5b915012fed2a79cf7275d74add38acff057.json new file mode 100644 index 00000000..97bacccf --- /dev/null +++ b/rust/.sqlx/query-13e31c854a9086ef9fb5ae3a79ad5b915012fed2a79cf7275d74add38acff057.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?", + "describe": { + "columns": [ + { + "name": "media_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "media_id" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true + ] + }, + "hash": "13e31c854a9086ef9fb5ae3a79ad5b915012fed2a79cf7275d74add38acff057" +} diff --git a/rust/.sqlx/query-141c8905f5d1f47fdbd88a007115dda16edadf2e6f92e7b190e99925870bdc18.json b/rust/.sqlx/query-141c8905f5d1f47fdbd88a007115dda16edadf2e6f92e7b190e99925870bdc18.json new file mode 100644 index 00000000..f09b4042 --- /dev/null +++ b/rust/.sqlx/query-141c8905f5d1f47fdbd88a007115dda16edadf2e6f92e7b190e99925870bdc18.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt, wake_receiver)\n VALUES (?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "141c8905f5d1f47fdbd88a007115dda16edadf2e6f92e7b190e99925870bdc18" +} diff --git a/rust/.sqlx/query-1446c19ba57adcff2aeecf55e73196002e6e58ddd91f9fb7811fd43e67d956c8.json b/rust/.sqlx/query-1446c19ba57adcff2aeecf55e73196002e6e58ddd91f9fb7811fd43e67d956c8.json new file mode 100644 index 00000000..9bc43079 --- /dev/null +++ b/rust/.sqlx/query-1446c19ba57adcff2aeecf55e73196002e6e58ddd91f9fb7811fd43e67d956c8.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(\n SELECT 1 FROM group_members\n WHERE group_id = ? AND contact_id = ? AND member_state != 'leftGroup'\n )", + "describe": { + "columns": [ + { + "name": "EXISTS(\n SELECT 1 FROM group_members\n WHERE group_id = ? AND contact_id = ? AND member_state != 'leftGroup'\n )", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "1446c19ba57adcff2aeecf55e73196002e6e58ddd91f9fb7811fd43e67d956c8" +} diff --git a/rust/.sqlx/query-8edba8bc58b60b678cb1730572119bd0f1ba1f98232e9d61eb0b7c920a5526f0.json b/rust/.sqlx/query-18c5303b936b2615c4da0bd935b374fabd4973f6cbb5032d76a5d8824c5d2610.json similarity index 73% rename from rust/.sqlx/query-8edba8bc58b60b678cb1730572119bd0f1ba1f98232e9d61eb0b7c920a5526f0.json rename to rust/.sqlx/query-18c5303b936b2615c4da0bd935b374fabd4973f6cbb5032d76a5d8824c5d2610.json index 9b586c76..db3fd244 100644 --- a/rust/.sqlx/query-8edba8bc58b60b678cb1730572119bd0f1ba1f98232e9d61eb0b7c920a5526f0.json +++ b/rust/.sqlx/query-18c5303b936b2615c4da0bd935b374fabd4973f6cbb5032d76a5d8824c5d2610.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT media_id\n FROM messages\n WHERE message_id = ? AND sender_id = ?\n ", + "query": "\n SELECT media_id\n FROM messages\n WHERE message_id = ?\n ", "describe": { "columns": [ { @@ -16,11 +16,11 @@ } ], "parameters": { - "Right": 2 + "Right": 1 }, "nullable": [ true ] }, - "hash": "8edba8bc58b60b678cb1730572119bd0f1ba1f98232e9d61eb0b7c920a5526f0" + "hash": "18c5303b936b2615c4da0bd935b374fabd4973f6cbb5032d76a5d8824c5d2610" } diff --git a/rust/.sqlx/query-18c85d89372ea21ae6cc75750b1c2f8fac5d97ad75b1beb452176b36d3fbcaa7.json b/rust/.sqlx/query-18c85d89372ea21ae6cc75750b1c2f8fac5d97ad75b1beb452176b36d3fbcaa7.json new file mode 100644 index 00000000..2857769e --- /dev/null +++ b/rust/.sqlx/query-18c85d89372ea21ae6cc75750b1c2f8fac5d97ad75b1beb452176b36d3fbcaa7.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT accepted, requested FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "accepted", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "accepted" + } + } + }, + { + "name": "requested", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "requested" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "18c85d89372ea21ae6cc75750b1c2f8fac5d97ad75b1beb452176b36d3fbcaa7" +} diff --git a/rust/.sqlx/query-19645cdadf2646e2c3afb1f09dd509a62222baebc54743f3485368fdf27c167a.json b/rust/.sqlx/query-19645cdadf2646e2c3afb1f09dd509a62222baebc54743f3485368fdf27c167a.json new file mode 100644 index 00000000..02b99fe6 --- /dev/null +++ b/rust/.sqlx/query-19645cdadf2646e2c3afb1f09dd509a62222baebc54743f3485368fdf27c167a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO media_files(media_id, type, download_state, upload_state) VALUES (?, 'image', 'ready', 'uploaded')", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "19645cdadf2646e2c3afb1f09dd509a62222baebc54743f3485368fdf27c167a" +} diff --git a/rust/.sqlx/query-19af16bcf6c54900ed6032ce8ed7b5ef9d71456488805e343c2af10ad471b22a.json b/rust/.sqlx/query-19af16bcf6c54900ed6032ce8ed7b5ef9d71456488805e343c2af10ad471b22a.json new file mode 100644 index 00000000..933c8a28 --- /dev/null +++ b/rust/.sqlx/query-19af16bcf6c54900ed6032ce8ed7b5ef9d71456488805e343c2af10ad471b22a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO notification_outbox(\n event_id, notification_id, conversation_id, sender_id,\n message_id, kind, content, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "19af16bcf6c54900ed6032ce8ed7b5ef9d71456488805e343c2af10ad471b22a" +} diff --git a/rust/.sqlx/query-1acafcad398cb31491dcee91a005eddd33a03d40afa505b7e0b5f59e2f731a20.json b/rust/.sqlx/query-1acafcad398cb31491dcee91a005eddd33a03d40afa505b7e0b5f59e2f731a20.json deleted file mode 100644 index a9746e54..00000000 --- a/rust/.sqlx/query-1acafcad398cb31491dcee91a005eddd33a03d40afa505b7e0b5f59e2f731a20.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE receipts\n SET mark_for_retry = CAST(strftime('%s', 'now') AS INTEGER),\n retry_count = retry_count + 1,\n ack_by_server_at = NULL\n WHERE receipt_id = ? AND contact_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "1acafcad398cb31491dcee91a005eddd33a03d40afa505b7e0b5f59e2f731a20" -} diff --git a/rust/.sqlx/query-1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18.json b/rust/.sqlx/query-1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18.json deleted file mode 100644 index 91b16f1f..00000000 --- a/rust/.sqlx/query-1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO user_discovery_user_relations (\n announced_user_id,\n from_contact_id,\n public_key_verified_timestamp\n ) VALUES (?, ?, ?)\n ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "1be4155879419ae7cdcd7f8774ea5a6cce7ecb05e825095f4fffd6aaf4288d18" -} diff --git a/rust/.sqlx/query-1d8a2e974208c2ca67e4fe792c4aedcaf5bb86dc24f09ed71069d3868df833ee.json b/rust/.sqlx/query-1d8a2e974208c2ca67e4fe792c4aedcaf5bb86dc24f09ed71069d3868df833ee.json new file mode 100644 index 00000000..cf778f79 --- /dev/null +++ b/rust/.sqlx/query-1d8a2e974208c2ca67e4fe792c4aedcaf5bb86dc24f09ed71069d3868df833ee.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT last_type_indicator FROM group_members WHERE group_id = ? AND contact_id = ?", + "describe": { + "columns": [ + { + "name": "last_type_indicator", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "group_members", + "name": "last_type_indicator" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true + ] + }, + "hash": "1d8a2e974208c2ca67e4fe792c4aedcaf5bb86dc24f09ed71069d3868df833ee" +} diff --git a/rust/.sqlx/query-1dad19878770f615fde37f4fa8ce4cb8254a723c4d353a2ccc1d9d224bb68971.json b/rust/.sqlx/query-1dad19878770f615fde37f4fa8ce4cb8254a723c4d353a2ccc1d9d224bb68971.json new file mode 100644 index 00000000..0d7b40d4 --- /dev/null +++ b/rust/.sqlx/query-1dad19878770f615fde37f4fa8ce4cb8254a723c4d353a2ccc1d9d224bb68971.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT upload_state FROM media_files WHERE media_id = ?", + "describe": { + "columns": [ + { + "name": "upload_state", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "upload_state" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "1dad19878770f615fde37f4fa8ce4cb8254a723c4d353a2ccc1d9d224bb68971" +} diff --git a/rust/.sqlx/query-1e27aa2db6de33b41610d8fe9394b7d54224d8717dd687516da0ced5268e4899.json b/rust/.sqlx/query-1e27aa2db6de33b41610d8fe9394b7d54224d8717dd687516da0ced5268e4899.json new file mode 100644 index 00000000..cc1d13ee --- /dev/null +++ b/rust/.sqlx/query-1e27aa2db6de33b41610d8fe9394b7d54224d8717dd687516da0ced5268e4899.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT recovery_last_heartbeat FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "recovery_last_heartbeat", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_last_heartbeat" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "1e27aa2db6de33b41610d8fe9394b7d54224d8717dd687516da0ced5268e4899" +} diff --git a/rust/.sqlx/query-1f22026fd1c3dc141303b1534b4d8fc497dc5fab1685aa8999f9ec9056b38df7.json b/rust/.sqlx/query-1f22026fd1c3dc141303b1534b4d8fc497dc5fab1685aa8999f9ec9056b38df7.json new file mode 100644 index 00000000..b10edc84 --- /dev/null +++ b/rust/.sqlx/query-1f22026fd1c3dc141303b1534b4d8fc497dc5fab1685aa8999f9ec9056b38df7.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(\n SELECT 1 FROM user_discovery_own_promotions\n WHERE contact_id = ? AND length(promotion) > 0\n )", + "describe": { + "columns": [ + { + "name": "EXISTS(\n SELECT 1 FROM user_discovery_own_promotions\n WHERE contact_id = ? AND length(promotion) > 0\n )", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "1f22026fd1c3dc141303b1534b4d8fc497dc5fab1685aa8999f9ec9056b38df7" +} diff --git a/rust/.sqlx/query-1f8ab52f86c62c865231b13253c61b6992a527e9fd1d89f074bed821fcb9095d.json b/rust/.sqlx/query-1f8ab52f86c62c865231b13253c61b6992a527e9fd1d89f074bed821fcb9095d.json new file mode 100644 index 00000000..980a9ec0 --- /dev/null +++ b/rust/.sqlx/query-1f8ab52f86c62c865231b13253c61b6992a527e9fd1d89f074bed821fcb9095d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET recovery_secret_share = ?, recovery_is_trusted_friend = 1 WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "1f8ab52f86c62c865231b13253c61b6992a527e9fd1d89f074bed821fcb9095d" +} diff --git a/rust/.sqlx/query-2136bd64733c0268d4da52cf36db8df3533d34a45008bddf61e1090ae1ace885.json b/rust/.sqlx/query-2136bd64733c0268d4da52cf36db8df3533d34a45008bddf61e1090ae1ace885.json deleted file mode 100644 index 53238ca7..00000000 --- a/rust/.sqlx/query-2136bd64733c0268d4da52cf36db8df3533d34a45008bddf61e1090ae1ace885.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO received_receipts(receipt_id)\n VALUES (?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "2136bd64733c0268d4da52cf36db8df3533d34a45008bddf61e1090ae1ace885" -} diff --git a/rust/.sqlx/query-2138b7b197c5637580e0aa98d5e2d7e80b0cc1b660ab96ace53bb90ea5ff2968.json b/rust/.sqlx/query-2138b7b197c5637580e0aa98d5e2d7e80b0cc1b660ab96ace53bb90ea5ff2968.json new file mode 100644 index 00000000..39e82bdf --- /dev/null +++ b/rust/.sqlx/query-2138b7b197c5637580e0aa98d5e2d7e80b0cc1b660ab96ace53bb90ea5ff2968.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "2138b7b197c5637580e0aa98d5e2d7e80b0cc1b660ab96ace53bb90ea5ff2968" +} diff --git a/rust/.sqlx/query-225cd55439cc89f0ee7bc53a0f19cf53a98ec2bdc32c82e43c6beef3bdc23d98.json b/rust/.sqlx/query-225cd55439cc89f0ee7bc53a0f19cf53a98ec2bdc32c82e43c6beef3bdc23d98.json new file mode 100644 index 00000000..d2ef958a --- /dev/null +++ b/rust/.sqlx/query-225cd55439cc89f0ee7bc53a0f19cf53a98ec2bdc32c82e43c6beef3bdc23d98.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM api_outbox WHERE sequence_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "225cd55439cc89f0ee7bc53a0f19cf53a98ec2bdc32c82e43c6beef3bdc23d98" +} diff --git a/rust/.sqlx/query-23ce3aa70fcb31de3c6e6b1ba99750488d5d732b65ee326ca5b5a1626d43155c.json b/rust/.sqlx/query-23ce3aa70fcb31de3c6e6b1ba99750488d5d732b65ee326ca5b5a1626d43155c.json new file mode 100644 index 00000000..0737fbd1 --- /dev/null +++ b/rust/.sqlx/query-23ce3aa70fcb31de3c6e6b1ba99750488d5d732b65ee326ca5b5a1626d43155c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET draft_message = 'draft text' WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "23ce3aa70fcb31de3c6e6b1ba99750488d5d732b65ee326ca5b5a1626d43155c" +} diff --git a/rust/.sqlx/query-274f8f246a13f484fd1941bb0e570a06a7db075f26dfe0c943aefa0d66cff7aa.json b/rust/.sqlx/query-274f8f246a13f484fd1941bb0e570a06a7db075f26dfe0c943aefa0d66cff7aa.json new file mode 100644 index 00000000..7ec7b931 --- /dev/null +++ b/rust/.sqlx/query-274f8f246a13f484fd1941bb0e570a06a7db075f26dfe0c943aefa0d66cff7aa.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) FROM receipts WHERE contact_id = 9", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "274f8f246a13f484fd1941bb0e570a06a7db075f26dfe0c943aefa0d66cff7aa" +} diff --git a/rust/.sqlx/query-28663e0e60055c3bef26992fecdaed69a4ae46aa385d8927a62504f838987dfc.json b/rust/.sqlx/query-28663e0e60055c3bef26992fecdaed69a4ae46aa385d8927a62504f838987dfc.json new file mode 100644 index 00000000..7e2b1b31 --- /dev/null +++ b/rust/.sqlx/query-28663e0e60055c3bef26992fecdaed69a4ae46aa385d8927a62504f838987dfc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO group_members(group_id, contact_id, member_state) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "28663e0e60055c3bef26992fecdaed69a4ae46aa385d8927a62504f838987dfc" +} diff --git a/rust/.sqlx/query-291bf3c6dfb80861e3a250243693beff0798bc51759ea525dae477bff2096585.json b/rust/.sqlx/query-291bf3c6dfb80861e3a250243693beff0798bc51759ea525dae477bff2096585.json new file mode 100644 index 00000000..b5585c71 --- /dev/null +++ b/rust/.sqlx/query-291bf3c6dfb80861e3a250243693beff0798bc51759ea525dae477bff2096585.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO group_histories(group_history_id, group_id, contact_id, type)\n VALUES (?, ?, ?, 'addMember')", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "291bf3c6dfb80861e3a250243693beff0798bc51759ea525dae477bff2096585" +} diff --git a/rust/.sqlx/query-29bd81e76831e95b2a26531cbcf59139c1954d23158600ace4b314fcd4626992.json b/rust/.sqlx/query-29bd81e76831e95b2a26531cbcf59139c1954d23158600ace4b314fcd4626992.json new file mode 100644 index 00000000..2f10056a --- /dev/null +++ b/rust/.sqlx/query-29bd81e76831e95b2a26531cbcf59139c1954d23158600ace4b314fcd4626992.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT contact_id FROM group_members WHERE group_id = ? AND group_public_key = ?", + "describe": { + "columns": [ + { + "name": "contact_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "group_members", + "name": "contact_id" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "29bd81e76831e95b2a26531cbcf59139c1954d23158600ace4b314fcd4626992" +} diff --git a/rust/.sqlx/query-2a923146a880d7f8684495e93324a743ae58b3f32e01e5ad1fa5a304bb535ae5.json b/rust/.sqlx/query-2a923146a880d7f8684495e93324a743ae58b3f32e01e5ad1fa5a304bb535ae5.json new file mode 100644 index 00000000..9f9f5f43 --- /dev/null +++ b/rust/.sqlx/query-2a923146a880d7f8684495e93324a743ae58b3f32e01e5ad1fa5a304bb535ae5.json @@ -0,0 +1,98 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT r.contact_id, r.message, r.message_id, r.contact_will_sends_receipt,\n r.wake_receiver, c.account_deleted, c.signal_version\n FROM receipts r\n JOIN contacts c ON c.user_id = r.contact_id\n WHERE r.receipt_id = ?\n ", + "describe": { + "columns": [ + { + "name": "contact_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "receipts", + "name": "contact_id" + } + } + }, + { + "name": "message", + "ordinal": 1, + "type_info": "Blob", + "origin": { + "Table": { + "table": "receipts", + "name": "message" + } + } + }, + { + "name": "message_id", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "receipts", + "name": "message_id" + } + } + }, + { + "name": "contact_will_sends_receipt", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "receipts", + "name": "contact_will_sends_receipt" + } + } + }, + { + "name": "wake_receiver", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "receipts", + "name": "wake_receiver" + } + } + }, + { + "name": "account_deleted", + "ordinal": 5, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "account_deleted" + } + } + }, + { + "name": "signal_version", + "ordinal": 6, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "signal_version" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "2a923146a880d7f8684495e93324a743ae58b3f32e01e5ad1fa5a304bb535ae5" +} diff --git a/rust/.sqlx/query-2b86ca80f38f47f115c8c8e6ce79343574e6ca435946c303dd8f35e68bfc57ef.json b/rust/.sqlx/query-2b86ca80f38f47f115c8c8e6ce79343574e6ca435946c303dd8f35e68bfc57ef.json new file mode 100644 index 00000000..20ac15a0 --- /dev/null +++ b/rust/.sqlx/query-2b86ca80f38f47f115c8c8e6ce79343574e6ca435946c303dd8f35e68bfc57ef.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET type = ?, download_state = 'pending',\n requires_authentication = ?, display_limit_in_milliseconds = ?,\n download_token = ?, encryption_key = ?, encryption_mac = ?,\n encryption_nonce = ?, created_at = ? WHERE media_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "2b86ca80f38f47f115c8c8e6ce79343574e6ca435946c303dd8f35e68bfc57ef" +} diff --git a/rust/.sqlx/query-2c4a9f54ba414124c446f52333a1c2dcf88c8ad4d4d96f746ce47532452ec19d.json b/rust/.sqlx/query-2c4a9f54ba414124c446f52333a1c2dcf88c8ad4d4d96f746ce47532452ec19d.json new file mode 100644 index 00000000..c1863f19 --- /dev/null +++ b/rust/.sqlx/query-2c4a9f54ba414124c446f52333a1c2dcf88c8ad4d4d96f746ce47532452ec19d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET recovery_contacts_last_heartbeat = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "2c4a9f54ba414124c446f52333a1c2dcf88c8ad4d4d96f746ce47532452ec19d" +} diff --git a/rust/.sqlx/query-2cc4a73378cf37ff6e3dcbd761fec56686a8f9820a3bb2c489e94fb036de0786.json b/rust/.sqlx/query-2cc4a73378cf37ff6e3dcbd761fec56686a8f9820a3bb2c489e94fb036de0786.json new file mode 100644 index 00000000..cfdc6b73 --- /dev/null +++ b/rust/.sqlx/query-2cc4a73378cf37ff6e3dcbd761fec56686a8f9820a3bb2c489e94fb036de0786.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET download_state = 'reuploadRequested' WHERE media_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "2cc4a73378cf37ff6e3dcbd761fec56686a8f9820a3bb2c489e94fb036de0786" +} diff --git a/rust/.sqlx/query-2e24484b81be3b3a726c018b26311d543af677422a69318c0e187c3cf7379bf7.json b/rust/.sqlx/query-2e24484b81be3b3a726c018b26311d543af677422a69318c0e187c3cf7379bf7.json new file mode 100644 index 00000000..dc686293 --- /dev/null +++ b/rust/.sqlx/query-2e24484b81be3b3a726c018b26311d543af677422a69318c0e187c3cf7379bf7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO group_members(group_id, contact_id, member_state)\n VALUES (?, ?, ?)\n ON CONFLICT(group_id, contact_id)\n DO UPDATE SET member_state = excluded.member_state\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "2e24484b81be3b3a726c018b26311d543af677422a69318c0e187c3cf7379bf7" +} diff --git a/rust/.sqlx/query-2e5c2ccc9be071bcebcb9c8a7b0564e6ef21a38106bb9bc90c840ac1903b75d5.json b/rust/.sqlx/query-2e5c2ccc9be071bcebcb9c8a7b0564e6ef21a38106bb9bc90c840ac1903b75d5.json new file mode 100644 index 00000000..25ec7153 --- /dev/null +++ b/rust/.sqlx/query-2e5c2ccc9be071bcebcb9c8a7b0564e6ef21a38106bb9bc90c840ac1903b75d5.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT contact_id FROM group_members\n WHERE group_id = ? AND (member_state IS NULL OR member_state != 'leftGroup')", + "describe": { + "columns": [ + { + "name": "contact_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "group_members", + "name": "contact_id" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "2e5c2ccc9be071bcebcb9c8a7b0564e6ef21a38106bb9bc90c840ac1903b75d5" +} diff --git a/rust/.sqlx/query-2ec060bf87e7c0a76c47be0010f9f9c37d20f2ea3ae1da27c757a5d32b60a1cc.json b/rust/.sqlx/query-2ec060bf87e7c0a76c47be0010f9f9c37d20f2ea3ae1da27c757a5d32b60a1cc.json new file mode 100644 index 00000000..64325dc0 --- /dev/null +++ b/rust/.sqlx/query-2ec060bf87e7c0a76c47be0010f9f9c37d20f2ea3ae1da27c757a5d32b60a1cc.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_name FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "group_name", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_name" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "2ec060bf87e7c0a76c47be0010f9f9c37d20f2ea3ae1da27c757a5d32b60a1cc" +} diff --git a/rust/.sqlx/query-2f1854f10ac694a9dcabe133c32d986978bf168dccce04d461beeef220f8952c.json b/rust/.sqlx/query-2f1854f10ac694a9dcabe133c32d986978bf168dccce04d461beeef220f8952c.json new file mode 100644 index 00000000..efb863da --- /dev/null +++ b/rust/.sqlx/query-2f1854f10ac694a9dcabe133c32d986978bf168dccce04d461beeef220f8952c.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT EXISTS(\n SELECT 1\n FROM key_verifications AS verification\n WHERE verification.contact_id = ?\n AND (\n verification.type != 'contactSharedByVerified'\n OR EXISTS(\n SELECT 1 FROM key_verifications AS verifier_verification\n WHERE verifier_verification.contact_id = verification.verified_by\n )\n )\n )\n ", + "describe": { + "columns": [ + { + "name": "EXISTS(\n SELECT 1\n FROM key_verifications AS verification\n WHERE verification.contact_id = ?\n AND (\n verification.type != 'contactSharedByVerified'\n OR EXISTS(\n SELECT 1 FROM key_verifications AS verifier_verification\n WHERE verifier_verification.contact_id = verification.verified_by\n )\n )\n )", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "2f1854f10ac694a9dcabe133c32d986978bf168dccce04d461beeef220f8952c" +} diff --git a/rust/.sqlx/query-2fec92a566a99026de111e1b9b175719cb54932ea03bd754747407d3291f764d.json b/rust/.sqlx/query-2fec92a566a99026de111e1b9b175719cb54932ea03bd754747407d3291f764d.json new file mode 100644 index 00000000..85d5e2d8 --- /dev/null +++ b/rust/.sqlx/query-2fec92a566a99026de111e1b9b175719cb54932ea03bd754747407d3291f764d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO message_histories(message_id, content, created_at)\n SELECT message_id, content, ? FROM messages\n WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "2fec92a566a99026de111e1b9b175719cb54932ea03bd754747407d3291f764d" +} diff --git a/rust/.sqlx/query-5988df8ec10b625bbd16ddcd2dccad54f2b209e74849509c1905e0f106756c7d.json b/rust/.sqlx/query-345381ad177426da0800eaee0c04352e78377c106d7e156c07aef00858bc8bc8.json similarity index 65% rename from rust/.sqlx/query-5988df8ec10b625bbd16ddcd2dccad54f2b209e74849509c1905e0f106756c7d.json rename to rust/.sqlx/query-345381ad177426da0800eaee0c04352e78377c106d7e156c07aef00858bc8bc8.json index 2d65b321..d8be76f3 100644 --- a/rust/.sqlx/query-5988df8ec10b625bbd16ddcd2dccad54f2b209e74849509c1905e0f106756c7d.json +++ b/rust/.sqlx/query-345381ad177426da0800eaee0c04352e78377c106d7e156c07aef00858bc8bc8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT sender_id\n FROM messages\n WHERE message_id = ?\n ", + "query": "\n SELECT sender_id\n FROM messages\n WHERE message_id = ?\n ", "describe": { "columns": [ { @@ -22,5 +22,5 @@ true ] }, - "hash": "5988df8ec10b625bbd16ddcd2dccad54f2b209e74849509c1905e0f106756c7d" + "hash": "345381ad177426da0800eaee0c04352e78377c106d7e156c07aef00858bc8bc8" } diff --git a/rust/.sqlx/query-3462ce3b677445a641de96634d6f69a661f2b58cbfff84eac55eaec901a54284.json b/rust/.sqlx/query-3462ce3b677445a641de96634d6f69a661f2b58cbfff84eac55eaec901a54284.json deleted file mode 100644 index 87b9cc82..00000000 --- a/rust/.sqlx/query-3462ce3b677445a641de96634d6f69a661f2b58cbfff84eac55eaec901a54284.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET user_discovery_version = ?\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "3462ce3b677445a641de96634d6f69a661f2b58cbfff84eac55eaec901a54284" -} diff --git a/rust/.sqlx/query-370baafebd6f09754b0e29cf2b384c07426a989e8b1299414d6f5e7a9cdccadb.json b/rust/.sqlx/query-370baafebd6f09754b0e29cf2b384c07426a989e8b1299414d6f5e7a9cdccadb.json deleted file mode 100644 index 5da40a79..00000000 --- a/rust/.sqlx/query-370baafebd6f09754b0e29cf2b384c07426a989e8b1299414d6f5e7a9cdccadb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET recovery_secret_share = ?\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "370baafebd6f09754b0e29cf2b384c07426a989e8b1299414d6f5e7a9cdccadb" -} diff --git a/rust/.sqlx/query-3814aef32753b08f1455d22511d11bff65e50a32861ccb8b8323e008e114a8cc.json b/rust/.sqlx/query-3814aef32753b08f1455d22511d11bff65e50a32861ccb8b8323e008e114a8cc.json new file mode 100644 index 00000000..dcedf377 --- /dev/null +++ b/rust/.sqlx/query-3814aef32753b08f1455d22511d11bff65e50a32861ccb8b8323e008e114a8cc.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT left_group FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "left_group", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "left_group" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "3814aef32753b08f1455d22511d11bff65e50a32861ccb8b8323e008e114a8cc" +} diff --git a/rust/.sqlx/query-3b22d58da61061edeccdd6188f947bf77eef678b7e378e17effbb3ce0fd7f3a9.json b/rust/.sqlx/query-3b22d58da61061edeccdd6188f947bf77eef678b7e378e17effbb3ce0fd7f3a9.json new file mode 100644 index 00000000..debad067 --- /dev/null +++ b/rust/.sqlx/query-3b22d58da61061edeccdd6188f947bf77eef678b7e378e17effbb3ce0fd7f3a9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO key_verifications(contact_id, type) VALUES (?, 'manualTest')", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "3b22d58da61061edeccdd6188f947bf77eef678b7e378e17effbb3ce0fd7f3a9" +} diff --git a/rust/.sqlx/query-3b23ab2f5440c3ba2ccb650d6b961bdfcfd1a9f3f2128bc0abdc47ab637774e3.json b/rust/.sqlx/query-3b23ab2f5440c3ba2ccb650d6b961bdfcfd1a9f3f2128bc0abdc47ab637774e3.json new file mode 100644 index 00000000..65f431d7 --- /dev/null +++ b/rust/.sqlx/query-3b23ab2f5440c3ba2ccb650d6b961bdfcfd1a9f3f2128bc0abdc47ab637774e3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET left_group = ? WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "3b23ab2f5440c3ba2ccb650d6b961bdfcfd1a9f3f2128bc0abdc47ab637774e3" +} diff --git a/rust/.sqlx/query-3c0f58c288fae9fcc8dcaae6d1d48a859644a0e5d63e16e146265f9afbb11a00.json b/rust/.sqlx/query-3c0f58c288fae9fcc8dcaae6d1d48a859644a0e5d63e16e146265f9afbb11a00.json new file mode 100644 index 00000000..8628e663 --- /dev/null +++ b/rust/.sqlx/query-3c0f58c288fae9fcc8dcaae6d1d48a859644a0e5d63e16e146265f9afbb11a00.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO user_discovery_announced_users (\n announced_user_id, announced_public_key, public_id\n ) VALUES (?, ?, ?)\n ON CONFLICT DO UPDATE SET\n announced_user_id = excluded.announced_user_id,\n announced_public_key = excluded.announced_public_key,\n public_id = excluded.public_id\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "3c0f58c288fae9fcc8dcaae6d1d48a859644a0e5d63e16e146265f9afbb11a00" +} diff --git a/rust/.sqlx/query-3c5c07a73316e7e31ef06c5a6c5b273cb4e2665bd78cb5cbc38b73447af9eaf2.json b/rust/.sqlx/query-3c5c07a73316e7e31ef06c5a6c5b273cb4e2665bd78cb5cbc38b73447af9eaf2.json new file mode 100644 index 00000000..3d0491ec --- /dev/null +++ b/rust/.sqlx/query-3c5c07a73316e7e31ef06c5a6c5b273cb4e2665bd78cb5cbc38b73447af9eaf2.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT sequence_id, payload FROM api_outbox ORDER BY created_at", + "describe": { + "columns": [ + { + "name": "sequence_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "api_outbox", + "name": "sequence_id" + } + } + }, + { + "name": "payload", + "ordinal": 1, + "type_info": "Blob", + "origin": { + "Table": { + "table": "api_outbox", + "name": "payload" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "3c5c07a73316e7e31ef06c5a6c5b273cb4e2665bd78cb5cbc38b73447af9eaf2" +} diff --git a/rust/.sqlx/query-3cac7888e450d5d16693a96c3f46bdfeaf3e0b2237f33e0855a8dfc7895fd9a3.json b/rust/.sqlx/query-3cac7888e450d5d16693a96c3f46bdfeaf3e0b2237f33e0855a8dfc7895fd9a3.json new file mode 100644 index 00000000..95eb6a74 --- /dev/null +++ b/rust/.sqlx/query-3cac7888e450d5d16693a96c3f46bdfeaf3e0b2237f33e0855a8dfc7895fd9a3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO key_verifications(contact_id, type, verified_by)\n VALUES (?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "3cac7888e450d5d16693a96c3f46bdfeaf3e0b2237f33e0855a8dfc7895fd9a3" +} diff --git a/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json b/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json new file mode 100644 index 00000000..e84cb676 --- /dev/null +++ b/rust/.sqlx/query-3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?)\n WHERE group_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "3d1fdb8baa14a261fd2e2943ce5712ed95f12e337e88c18619aed086ebbd42aa" +} diff --git a/rust/.sqlx/query-174e1f8d22842961cc26633c7f715a0c278dcda82838188586821354c306b1e9.json b/rust/.sqlx/query-3dc2f59650c7770eaf991ff1e0ba39c24e06317ed83dd4e20bd429c4ff802ab0.json similarity index 54% rename from rust/.sqlx/query-174e1f8d22842961cc26633c7f715a0c278dcda82838188586821354c306b1e9.json rename to rust/.sqlx/query-3dc2f59650c7770eaf991ff1e0ba39c24e06317ed83dd4e20bd429c4ff802ab0.json index c51fffc1..14f11223 100644 --- a/rust/.sqlx/query-174e1f8d22842961cc26633c7f715a0c278dcda82838188586821354c306b1e9.json +++ b/rust/.sqlx/query-3dc2f59650c7770eaf991ff1e0ba39c24e06317ed83dd4e20bd429c4ff802ab0.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT COUNT(*)\n FROM reactions\n WHERE message_id = 'path-text-message' AND sender_id = ? AND emoji = '👍'\n ", + "query": "SELECT COUNT(*) FROM messages WHERE media_id = ?", "describe": { "columns": [ { @@ -17,5 +17,5 @@ false ] }, - "hash": "174e1f8d22842961cc26633c7f715a0c278dcda82838188586821354c306b1e9" + "hash": "3dc2f59650c7770eaf991ff1e0ba39c24e06317ed83dd4e20bd429c4ff802ab0" } diff --git a/rust/.sqlx/query-405bd3fad9cb5b677e1354c27453e3d4afe7c79858ea617337036717f6fea03a.json b/rust/.sqlx/query-405bd3fad9cb5b677e1354c27453e3d4afe7c79858ea617337036717f6fea03a.json new file mode 100644 index 00000000..649f30fc --- /dev/null +++ b/rust/.sqlx/query-405bd3fad9cb5b677e1354c27453e3d4afe7c79858ea617337036717f6fea03a.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT my_group_private_key FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "my_group_private_key", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "groups", + "name": "my_group_private_key" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "405bd3fad9cb5b677e1354c27453e3d4afe7c79858ea617337036717f6fea03a" +} diff --git a/rust/.sqlx/query-408e8205bae9e1640fbaf2f91eef3e3896323fce641a76b9cab489cd40e4506c.json b/rust/.sqlx/query-408e8205bae9e1640fbaf2f91eef3e3896323fce641a76b9cab489cd40e4506c.json new file mode 100644 index 00000000..82c5d2fb --- /dev/null +++ b/rust/.sqlx/query-408e8205bae9e1640fbaf2f91eef3e3896323fce641a76b9cab489cd40e4506c.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_id, group_name, state_version_id,\n state_encryption_key, my_group_private_key\n FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_id" + } + } + }, + { + "name": "group_name", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_name" + } + } + }, + { + "name": "state_version_id", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "state_version_id" + } + } + }, + { + "name": "state_encryption_key", + "ordinal": 3, + "type_info": "Blob", + "origin": { + "Table": { + "table": "groups", + "name": "state_encryption_key" + } + } + }, + { + "name": "my_group_private_key", + "ordinal": 4, + "type_info": "Blob", + "origin": { + "Table": { + "table": "groups", + "name": "my_group_private_key" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true + ] + }, + "hash": "408e8205bae9e1640fbaf2f91eef3e3896323fce641a76b9cab489cd40e4506c" +} diff --git a/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json b/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json new file mode 100644 index 00000000..52de7ccd --- /dev/null +++ b/rust/.sqlx/query-411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE group_members SET last_message = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ? AND contact_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "411a854abb8d23c104b7290c25313c679ef0361348c17d2ee740d30ad8c5a424" +} diff --git a/rust/.sqlx/query-f939d4c2915e167dbcbac32fe36c5caa4719f1a75693e6e2b6bb804ea9f790ed.json b/rust/.sqlx/query-413b5a5068e17f5ba2c8bb958f51108794c944e8948d0589c21c510d010e5bd6.json similarity index 62% rename from rust/.sqlx/query-f939d4c2915e167dbcbac32fe36c5caa4719f1a75693e6e2b6bb804ea9f790ed.json rename to rust/.sqlx/query-413b5a5068e17f5ba2c8bb958f51108794c944e8948d0589c21c510d010e5bd6.json index 50aee5cc..0bde8938 100644 --- a/rust/.sqlx/query-f939d4c2915e167dbcbac32fe36c5caa4719f1a75693e6e2b6bb804ea9f790ed.json +++ b/rust/.sqlx/query-413b5a5068e17f5ba2c8bb958f51108794c944e8948d0589c21c510d010e5bd6.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT share\n FROM user_discovery_shares\n WHERE contact_id = ?\n LIMIT 1\n ", + "query": "SELECT share FROM user_discovery_shares WHERE contact_id = ? LIMIT 1", "describe": { "columns": [ { @@ -22,5 +22,5 @@ false ] }, - "hash": "f939d4c2915e167dbcbac32fe36c5caa4719f1a75693e6e2b6bb804ea9f790ed" + "hash": "413b5a5068e17f5ba2c8bb958f51108794c944e8948d0589c21c510d010e5bd6" } diff --git a/rust/.sqlx/query-41577de24c37ee6af005567456b7df22c28ebdbcd94070e924fd9c0b39725873.json b/rust/.sqlx/query-41577de24c37ee6af005567456b7df22c28ebdbcd94070e924fd9c0b39725873.json new file mode 100644 index 00000000..350659fd --- /dev/null +++ b/rust/.sqlx/query-41577de24c37ee6af005567456b7df22c28ebdbcd94070e924fd9c0b39725873.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM message_histories WHERE message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "41577de24c37ee6af005567456b7df22c28ebdbcd94070e924fd9c0b39725873" +} diff --git a/rust/.sqlx/query-44196efd5f9807f96f071745943ea8768f7134231a05e55746989e3cbb996eb1.json b/rust/.sqlx/query-44196efd5f9807f96f071745943ea8768f7134231a05e55746989e3cbb996eb1.json new file mode 100644 index 00000000..b766e16e --- /dev/null +++ b/rust/.sqlx/query-44196efd5f9807f96f071745943ea8768f7134231a05e55746989e3cbb996eb1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO group_histories(\n group_history_id, \n group_id, \n affected_contact_id, \n new_group_name, \n new_delete_messages_after_milliseconds, \n type\n ) VALUES (?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "44196efd5f9807f96f071745943ea8768f7134231a05e55746989e3cbb996eb1" +} diff --git a/rust/.sqlx/query-46dcd58d4b0682cf3735addcfa526215a52a5a48969c7b5ab88c0fc1b7609201.json b/rust/.sqlx/query-46dcd58d4b0682cf3735addcfa526215a52a5a48969c7b5ab88c0fc1b7609201.json new file mode 100644 index 00000000..d1225f01 --- /dev/null +++ b/rust/.sqlx/query-46dcd58d4b0682cf3735addcfa526215a52a5a48969c7b5ab88c0fc1b7609201.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE messages\n SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?\n WHERE message_id = ? AND sender_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "46dcd58d4b0682cf3735addcfa526215a52a5a48969c7b5ab88c0fc1b7609201" +} diff --git a/rust/.sqlx/query-47802f3954f7e8a17c7cb78cbaf72e68a2191c2a35eb3bb8f045765689d6520e.json b/rust/.sqlx/query-47802f3954f7e8a17c7cb78cbaf72e68a2191c2a35eb3bb8f045765689d6520e.json new file mode 100644 index 00000000..0a769c10 --- /dev/null +++ b/rust/.sqlx/query-47802f3954f7e8a17c7cb78cbaf72e68a2191c2a35eb3bb8f045765689d6520e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO contacts(user_id, username, signal_version, accepted, requested, deleted_by_user, blocked)\n VALUES (?, COALESCE(?, '[Unknown]'), COALESCE(?, 'v2'), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0))\n ON CONFLICT(user_id) DO UPDATE SET\n username = COALESCE(?, contacts.username),\n signal_version = COALESCE(?, contacts.signal_version),\n accepted = COALESCE(?, contacts.accepted),\n requested = COALESCE(?, contacts.requested),\n deleted_by_user = COALESCE(?, contacts.deleted_by_user),\n blocked = COALESCE(?, contacts.blocked)\n WHERE ? = 0 OR contacts.requested = 0\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "47802f3954f7e8a17c7cb78cbaf72e68a2191c2a35eb3bb8f045765689d6520e" +} diff --git a/rust/.sqlx/query-49a771af89168b9e3d09e2b0cbe49eb2a51189212e1cdb99fb8211bd614c9558.json b/rust/.sqlx/query-49a771af89168b9e3d09e2b0cbe49eb2a51189212e1cdb99fb8211bd614c9558.json deleted file mode 100644 index 53651e0f..00000000 --- a/rust/.sqlx/query-49a771af89168b9e3d09e2b0cbe49eb2a51189212e1cdb99fb8211bd614c9558.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET requested = 0, deleted_by_user = 0\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "49a771af89168b9e3d09e2b0cbe49eb2a51189212e1cdb99fb8211bd614c9558" -} diff --git a/rust/.sqlx/query-4ad8ea916d6113c45fef05150f2e619e06ac351ac0b7d0a9b340835ac7109fdf.json b/rust/.sqlx/query-4ad8ea916d6113c45fef05150f2e619e06ac351ac0b7d0a9b340835ac7109fdf.json new file mode 100644 index 00000000..c41836fe --- /dev/null +++ b/rust/.sqlx/query-4ad8ea916d6113c45fef05150f2e619e06ac351ac0b7d0a9b340835ac7109fdf.json @@ -0,0 +1,28 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS \"count!: i64\", MAX(last_retry) AS last_retry\n FROM receipts WHERE contact_id = ? AND message_id = ?", + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + }, + { + "name": "last_retry", + "ordinal": 1, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + true + ] + }, + "hash": "4ad8ea916d6113c45fef05150f2e619e06ac351ac0b7d0a9b340835ac7109fdf" +} diff --git a/rust/.sqlx/query-4b8683059700bf2e6ad30130891cc5ac8759171c57fae2271a493e85c2ee4a2e.json b/rust/.sqlx/query-4b8683059700bf2e6ad30130891cc5ac8759171c57fae2271a493e85c2ee4a2e.json new file mode 100644 index 00000000..6b0a4d0d --- /dev/null +++ b/rust/.sqlx/query-4b8683059700bf2e6ad30130891cc5ac8759171c57fae2271a493e85c2ee4a2e.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT announced_user_id, announced_public_key\n FROM user_discovery_announced_users WHERE username IS NULL", + "describe": { + "columns": [ + { + "name": "announced_user_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "user_discovery_announced_users", + "name": "announced_user_id" + } + } + }, + { + "name": "announced_public_key", + "ordinal": 1, + "type_info": "Blob", + "origin": { + "Table": { + "table": "user_discovery_announced_users", + "name": "announced_public_key" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "4b8683059700bf2e6ad30130891cc5ac8759171c57fae2271a493e85c2ee4a2e" +} diff --git a/rust/.sqlx/query-4cceb9755b12fe18a8462b8e38ada4a606a8c973b2dbb7a70fb25bd0938f9c24.json b/rust/.sqlx/query-4cceb9755b12fe18a8462b8e38ada4a606a8c973b2dbb7a70fb25bd0938f9c24.json new file mode 100644 index 00000000..c41102fc --- /dev/null +++ b/rust/.sqlx/query-4cceb9755b12fe18a8462b8e38ada4a606a8c973b2dbb7a70fb25bd0938f9c24.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT delete_messages_after_milliseconds FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "delete_messages_after_milliseconds", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "delete_messages_after_milliseconds" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "4cceb9755b12fe18a8462b8e38ada4a606a8c973b2dbb7a70fb25bd0938f9c24" +} diff --git a/rust/.sqlx/query-4cf56a4c69c3f809333c83aae082980edd05095bf43bc00b481d613ad250724c.json b/rust/.sqlx/query-4cf56a4c69c3f809333c83aae082980edd05095bf43bc00b481d613ad250724c.json new file mode 100644 index 00000000..2ecfa264 --- /dev/null +++ b/rust/.sqlx/query-4cf56a4c69c3f809333c83aae082980edd05095bf43bc00b481d613ad250724c.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT MAX(created_at, ?) AS \"action_at!: i64\" FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "action_at!: i64", + "ordinal": 0, + "type_info": "Null", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + null + ] + }, + "hash": "4cf56a4c69c3f809333c83aae082980edd05095bf43bc00b481d613ad250724c" +} diff --git a/rust/.sqlx/query-5357d7c57bd2d7a7d2c52e4dd41e90bdea08f915d736745592ddd375171787f8.json b/rust/.sqlx/query-5357d7c57bd2d7a7d2c52e4dd41e90bdea08f915d736745592ddd375171787f8.json deleted file mode 100644 index f413e567..00000000 --- a/rust/.sqlx/query-5357d7c57bd2d7a7d2c52e4dd41e90bdea08f915d736745592ddd375171787f8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE messages\n SET is_deleted_from_sender = 1, modified_at = ?\n WHERE message_id = ? AND sender_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "5357d7c57bd2d7a7d2c52e4dd41e90bdea08f915d736745592ddd375171787f8" -} diff --git a/rust/.sqlx/query-53e9edf1571d1f0fe88384aa3423846fd4a4d9a202c5d7e706c54c7a91df215d.json b/rust/.sqlx/query-53e9edf1571d1f0fe88384aa3423846fd4a4d9a202c5d7e706c54c7a91df215d.json new file mode 100644 index 00000000..1c06c82b --- /dev/null +++ b/rust/.sqlx/query-53e9edf1571d1f0fe88384aa3423846fd4a4d9a202c5d7e706c54c7a91df215d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO message_actions(message_id, contact_id, type, action_at)\n SELECT messages.message_id, group_members.contact_id, 'ackByServerAt', ?\n FROM messages\n JOIN group_members ON group_members.group_id = messages.group_id\n WHERE messages.media_id = ?\n AND (group_members.member_state IS NULL OR group_members.member_state != 'leftGroup')\n ON CONFLICT(message_id, contact_id, type)\n DO UPDATE SET action_at = excluded.action_at\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "53e9edf1571d1f0fe88384aa3423846fd4a4d9a202c5d7e706c54c7a91df215d" +} diff --git a/rust/.sqlx/query-58cd5f927d873ff3325f391cd8c6b9ca9e68bb6cd423dce99be407402ced523e.json b/rust/.sqlx/query-58cd5f927d873ff3325f391cd8c6b9ca9e68bb6cd423dce99be407402ced523e.json new file mode 100644 index 00000000..ba95aa4a --- /dev/null +++ b/rust/.sqlx/query-58cd5f927d873ff3325f391cd8c6b9ca9e68bb6cd423dce99be407402ced523e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt, wake_receiver)\n VALUES (?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "58cd5f927d873ff3325f391cd8c6b9ca9e68bb6cd423dce99be407402ced523e" +} diff --git a/rust/.sqlx/query-5a183a98692dd2aa25f61799cfbcf794e507a69be0524624b4ba75d69fc9fa1a.json b/rust/.sqlx/query-5a183a98692dd2aa25f61799cfbcf794e507a69be0524624b4ba75d69fc9fa1a.json new file mode 100644 index 00000000..1f43ab4d --- /dev/null +++ b/rust/.sqlx/query-5a183a98692dd2aa25f61799cfbcf794e507a69be0524624b4ba75d69fc9fa1a.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT draft_message FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "draft_message", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "draft_message" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "5a183a98692dd2aa25f61799cfbcf794e507a69be0524624b4ba75d69fc9fa1a" +} diff --git a/rust/.sqlx/query-5a7ba59111d6353f7a18d3f8eb8fa1ae3a491f080a4735361e9aaa41ccaa7d51.json b/rust/.sqlx/query-5a7ba59111d6353f7a18d3f8eb8fa1ae3a491f080a4735361e9aaa41ccaa7d51.json deleted file mode 100644 index f91fa816..00000000 --- a/rust/.sqlx/query-5a7ba59111d6353f7a18d3f8eb8fa1ae3a491f080a4735361e9aaa41ccaa7d51.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE user_discovery_own_promotions\n SET promotion = X''\n WHERE contact_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "5a7ba59111d6353f7a18d3f8eb8fa1ae3a491f080a4735361e9aaa41ccaa7d51" -} diff --git a/rust/.sqlx/query-5b1b9e6003bab16e21e1c38f5115c854c13749275db28bb96970e2056cf851c1.json b/rust/.sqlx/query-5b1b9e6003bab16e21e1c38f5115c854c13749275db28bb96970e2056cf851c1.json new file mode 100644 index 00000000..f7c0ce3f --- /dev/null +++ b/rust/.sqlx/query-5b1b9e6003bab16e21e1c38f5115c854c13749275db28bb96970e2056cf851c1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE notification_outbox SET cleared_at = ? WHERE conversation_id = ? AND cleared_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "5b1b9e6003bab16e21e1c38f5115c854c13749275db28bb96970e2056cf851c1" +} diff --git a/rust/.sqlx/query-07d19c693f05b07d6d3c29ff7c77571d8dd6b4618434e36f2d9d97a9178ddb3b.json b/rust/.sqlx/query-5b3a65331dc9b6528819f6fddb248917c4419b0d68b76e662eb3f76ed2975b14.json similarity index 50% rename from rust/.sqlx/query-07d19c693f05b07d6d3c29ff7c77571d8dd6b4618434e36f2d9d97a9178ddb3b.json rename to rust/.sqlx/query-5b3a65331dc9b6528819f6fddb248917c4419b0d68b76e662eb3f76ed2975b14.json index 97a6f7ef..37c6b4b8 100644 --- a/rust/.sqlx/query-07d19c693f05b07d6d3c29ff7c77571d8dd6b4618434e36f2d9d97a9178ddb3b.json +++ b/rust/.sqlx/query-5b3a65331dc9b6528819f6fddb248917c4419b0d68b76e662eb3f76ed2975b14.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "\n SELECT my_group_private_key IS NOT NULL\n FROM groups\n WHERE group_id = ?\n ", + "query": "SELECT COUNT(*) FROM receipts WHERE contact_id = ?", "describe": { "columns": [ { - "name": "my_group_private_key IS NOT NULL", + "name": "COUNT(*)", "ordinal": 0, "type_info": "Integer", "origin": "Expression" @@ -17,5 +17,5 @@ false ] }, - "hash": "07d19c693f05b07d6d3c29ff7c77571d8dd6b4618434e36f2d9d97a9178ddb3b" + "hash": "5b3a65331dc9b6528819f6fddb248917c4419b0d68b76e662eb3f76ed2975b14" } diff --git a/rust/.sqlx/query-5b848eba9965a386b9189ff911015c9652305c82281efd37491139b814a4562a.json b/rust/.sqlx/query-5b848eba9965a386b9189ff911015c9652305c82281efd37491139b814a4562a.json deleted file mode 100644 index aeaec2f6..00000000 --- a/rust/.sqlx/query-5b848eba9965a386b9189ff911015c9652305c82281efd37491139b814a4562a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE groups\n SET also_best_friend = ?,\n flame_counter = MAX(flame_counter, ?),\n max_flame_counter = MAX(max_flame_counter, ?)\n WHERE group_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "5b848eba9965a386b9189ff911015c9652305c82281efd37491139b814a4562a" -} diff --git a/rust/.sqlx/query-5c21ee90d244d4859b3b9544f182dd071b91356acbf297ced95ebc496ff03091.json b/rust/.sqlx/query-5c21ee90d244d4859b3b9544f182dd071b91356acbf297ced95ebc496ff03091.json deleted file mode 100644 index b77bdb16..00000000 --- a/rust/.sqlx/query-5c21ee90d244d4859b3b9544f182dd071b91356acbf297ced95ebc496ff03091.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET accepted = 0, requested = 1\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "5c21ee90d244d4859b3b9544f182dd071b91356acbf297ced95ebc496ff03091" -} diff --git a/rust/.sqlx/query-5cdf6bbec2b16b9917ee87af7cd4ca712af5966ebc9a93978317d708c2f960fd.json b/rust/.sqlx/query-5cdf6bbec2b16b9917ee87af7cd4ca712af5966ebc9a93978317d708c2f960fd.json new file mode 100644 index 00000000..b438e285 --- /dev/null +++ b/rust/.sqlx/query-5cdf6bbec2b16b9917ee87af7cd4ca712af5966ebc9a93978317d708c2f960fd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM media_files WHERE media_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "5cdf6bbec2b16b9917ee87af7cd4ca712af5966ebc9a93978317d708c2f960fd" +} diff --git a/rust/.sqlx/query-5e79f50002566a12094f2aed7771a19c98dfab500af103f25f3c188ee53006c6.json b/rust/.sqlx/query-5e79f50002566a12094f2aed7771a19c98dfab500af103f25f3c188ee53006c6.json new file mode 100644 index 00000000..bb96adf3 --- /dev/null +++ b/rust/.sqlx/query-5e79f50002566a12094f2aed7771a19c98dfab500af103f25f3c188ee53006c6.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS \"count: i64\" FROM notification_outbox WHERE cleared_at IS NULL", + "describe": { + "columns": [ + { + "name": "count: i64", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "5e79f50002566a12094f2aed7771a19c98dfab500af103f25f3c188ee53006c6" +} diff --git a/rust/.sqlx/query-9f78cc93c1680c63c1a258ca2ce1de0ea43bf3a57382b0505828d34c562f96a6.json b/rust/.sqlx/query-60db27444be839288588e655829a7f940f23a9a86cb8b1803a74ff4cf94c00f1.json similarity index 75% rename from rust/.sqlx/query-9f78cc93c1680c63c1a258ca2ce1de0ea43bf3a57382b0505828d34c562f96a6.json rename to rust/.sqlx/query-60db27444be839288588e655829a7f940f23a9a86cb8b1803a74ff4cf94c00f1.json index 711cb24f..2fac717e 100644 --- a/rust/.sqlx/query-9f78cc93c1680c63c1a258ca2ce1de0ea43bf3a57382b0505828d34c562f96a6.json +++ b/rust/.sqlx/query-60db27444be839288588e655829a7f940f23a9a86cb8b1803a74ff4cf94c00f1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n announced_user_id,\n announced_public_key,\n public_id\n FROM user_discovery_announced_users\n WHERE public_id = ?\n ", + "query": "\n SELECT announced_user_id, announced_public_key, public_id\n FROM user_discovery_announced_users\n WHERE public_id = ?\n ", "describe": { "columns": [ { @@ -46,5 +46,5 @@ false ] }, - "hash": "9f78cc93c1680c63c1a258ca2ce1de0ea43bf3a57382b0505828d34c562f96a6" + "hash": "60db27444be839288588e655829a7f940f23a9a86cb8b1803a74ff4cf94c00f1" } diff --git a/rust/.sqlx/query-61c2483b50f9f5c8a586eaa2c2381b06898954f5c898abf280b9ab02d96ff457.json b/rust/.sqlx/query-61c2483b50f9f5c8a586eaa2c2381b06898954f5c898abf280b9ab02d96ff457.json deleted file mode 100644 index c10f1ad5..00000000 --- a/rust/.sqlx/query-61c2483b50f9f5c8a586eaa2c2381b06898954f5c898abf280b9ab02d96ff457.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO messages(group_id, message_id, sender_id, type)\n VALUES('group', 'message', 7, 'text')\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "61c2483b50f9f5c8a586eaa2c2381b06898954f5c898abf280b9ab02d96ff457" -} diff --git a/rust/.sqlx/query-629c9b14b6e65f3209752e66b4f7709d252feae82dd009ce628274ed9b9c680a.json b/rust/.sqlx/query-629c9b14b6e65f3209752e66b4f7709d252feae82dd009ce628274ed9b9c680a.json deleted file mode 100644 index 2dd4cc25..00000000 --- a/rust/.sqlx/query-629c9b14b6e65f3209752e66b4f7709d252feae82dd009ce628274ed9b9c680a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE user_discovery_shares\n SET contact_id = ?\n WHERE share_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "629c9b14b6e65f3209752e66b4f7709d252feae82dd009ce628274ed9b9c680a" -} diff --git a/rust/.sqlx/query-638a0ec306d93a0c9c57abcd61437c0bec21dfa994a5398d7e87bd4d99b6fb92.json b/rust/.sqlx/query-638a0ec306d93a0c9c57abcd61437c0bec21dfa994a5398d7e87bd4d99b6fb92.json new file mode 100644 index 00000000..360317bd --- /dev/null +++ b/rust/.sqlx/query-638a0ec306d93a0c9c57abcd61437c0bec21dfa994a5398d7e87bd4d99b6fb92.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET username = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "638a0ec306d93a0c9c57abcd61437c0bec21dfa994a5398d7e87bd4d99b6fb92" +} diff --git a/rust/.sqlx/query-65c975e50cc9edb95d66cac2698429f03d2fdc1d0a17e9155267491f68af3d1f.json b/rust/.sqlx/query-65c975e50cc9edb95d66cac2698429f03d2fdc1d0a17e9155267491f68af3d1f.json new file mode 100644 index 00000000..9e562dbf --- /dev/null +++ b/rust/.sqlx/query-65c975e50cc9edb95d66cac2698429f03d2fdc1d0a17e9155267491f68af3d1f.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT my_group_private_key\n FROM groups\n WHERE group_id = ?\n ", + "describe": { + "columns": [ + { + "name": "my_group_private_key", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "groups", + "name": "my_group_private_key" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "65c975e50cc9edb95d66cac2698429f03d2fdc1d0a17e9155267491f68af3d1f" +} diff --git a/rust/.sqlx/query-70f9d68d251f1c354c61584a84f509682e341595e0d98cb880c50ec80f11f02a.json b/rust/.sqlx/query-6668c78b62a4d359c1027219abe352fec2832bbeaa721cf29bf8961868d867dd.json similarity index 65% rename from rust/.sqlx/query-70f9d68d251f1c354c61584a84f509682e341595e0d98cb880c50ec80f11f02a.json rename to rust/.sqlx/query-6668c78b62a4d359c1027219abe352fec2832bbeaa721cf29bf8961868d867dd.json index 29b00d56..1fece209 100644 --- a/rust/.sqlx/query-70f9d68d251f1c354c61584a84f509682e341595e0d98cb880c50ec80f11f02a.json +++ b/rust/.sqlx/query-6668c78b62a4d359c1027219abe352fec2832bbeaa721cf29bf8961868d867dd.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT user_discovery_version\n FROM contacts\n WHERE user_id = ?\n ", + "query": "SELECT user_discovery_version FROM contacts WHERE user_id = ?", "describe": { "columns": [ { @@ -22,5 +22,5 @@ true ] }, - "hash": "70f9d68d251f1c354c61584a84f509682e341595e0d98cb880c50ec80f11f02a" + "hash": "6668c78b62a4d359c1027219abe352fec2832bbeaa721cf29bf8961868d867dd" } diff --git a/rust/.sqlx/query-668764467495cec13ad814f2d5e3f272b0fa1476fe24f22d40eb721baaf0f2b7.json b/rust/.sqlx/query-668764467495cec13ad814f2d5e3f272b0fa1476fe24f22d40eb721baaf0f2b7.json new file mode 100644 index 00000000..fd1f3edb --- /dev/null +++ b/rust/.sqlx/query-668764467495cec13ad814f2d5e3f272b0fa1476fe24f22d40eb721baaf0f2b7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET draft_message = NULL WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "668764467495cec13ad814f2d5e3f272b0fa1476fe24f22d40eb721baaf0f2b7" +} diff --git a/rust/.sqlx/query-6a2ad15584f9fb4a07ff035e3d4277932b702e63a1887b9c873a62d058ce6a69.json b/rust/.sqlx/query-6a2ad15584f9fb4a07ff035e3d4277932b702e63a1887b9c873a62d058ce6a69.json new file mode 100644 index 00000000..1e93f223 --- /dev/null +++ b/rust/.sqlx/query-6a2ad15584f9fb4a07ff035e3d4277932b702e63a1887b9c873a62d058ce6a69.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO messages(group_id, message_id, type, media_id, created_at) VALUES (?, ?, 'media', ?, CAST(strftime('%s','now') AS INTEGER))", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "6a2ad15584f9fb4a07ff035e3d4277932b702e63a1887b9c873a62d058ce6a69" +} diff --git a/rust/.sqlx/query-6a54a91ead7d088a7d47e2bf6e869ac7b4c2d1b7f5870ab5ab0e160fb99fae83.json b/rust/.sqlx/query-6a54a91ead7d088a7d47e2bf6e869ac7b4c2d1b7f5870ab5ab0e160fb99fae83.json new file mode 100644 index 00000000..5de3fb10 --- /dev/null +++ b/rust/.sqlx/query-6a54a91ead7d088a7d47e2bf6e869ac7b4c2d1b7f5870ab5ab0e160fb99fae83.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT notification_id FROM notification_outbox WHERE conversation_id = ? AND cleared_at IS NULL", + "describe": { + "columns": [ + { + "name": "notification_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "notification_id" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "6a54a91ead7d088a7d47e2bf6e869ac7b4c2d1b7f5870ab5ab0e160fb99fae83" +} diff --git a/rust/.sqlx/query-6c5f0fd707422f9e5808a3b7efeb18d4b3c27eb605637b932bbd2d4fbf23e8a9.json b/rust/.sqlx/query-6c5f0fd707422f9e5808a3b7efeb18d4b3c27eb605637b932bbd2d4fbf23e8a9.json new file mode 100644 index 00000000..7ea8a1d4 --- /dev/null +++ b/rust/.sqlx/query-6c5f0fd707422f9e5808a3b7efeb18d4b3c27eb605637b932bbd2d4fbf23e8a9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO message_actions(message_id, contact_id, type)\n VALUES (?, ?, 'ackByServerAt')\n ON CONFLICT(message_id, contact_id, type)\n DO UPDATE SET action_at = CAST(strftime('%s', 'now') AS INTEGER)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6c5f0fd707422f9e5808a3b7efeb18d4b3c27eb605637b932bbd2d4fbf23e8a9" +} diff --git a/rust/.sqlx/query-6ceb2e7b39ba54d335210bbc71c56e68ac09881f86189e833ab60ec34778b2ae.json b/rust/.sqlx/query-6ceb2e7b39ba54d335210bbc71c56e68ac09881f86189e833ab60ec34778b2ae.json new file mode 100644 index 00000000..de6707f7 --- /dev/null +++ b/rust/.sqlx/query-6ceb2e7b39ba54d335210bbc71c56e68ac09881f86189e833ab60ec34778b2ae.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM signal_identities WHERE name = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM signal_identities WHERE name = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "6ceb2e7b39ba54d335210bbc71c56e68ac09881f86189e833ab60ec34778b2ae" +} diff --git a/rust/.sqlx/query-6d2c616fc02e1fdc3686b34de0440936a9cb54e5921b5b17bfe17c021e12ca23.json b/rust/.sqlx/query-6d2c616fc02e1fdc3686b34de0440936a9cb54e5921b5b17bfe17c021e12ca23.json new file mode 100644 index 00000000..195d8ec5 --- /dev/null +++ b/rust/.sqlx/query-6d2c616fc02e1fdc3686b34de0440936a9cb54e5921b5b17bfe17c021e12ca23.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO signal_identities(name, identity_key, timestamp)\n VALUES (?, ?, CAST(strftime('%s', 'now') AS INTEGER))\n ON CONFLICT(name) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6d2c616fc02e1fdc3686b34de0440936a9cb54e5921b5b17bfe17c021e12ca23" +} diff --git a/rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json b/rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json new file mode 100644 index 00000000..98aec940 --- /dev/null +++ b/rust/.sqlx/query-6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT group_id, contact_id \n FROM group_members \n WHERE group_public_key IS NULL \n AND last_message >= CAST(strftime('%s','now','-2 days') AS INTEGER)\n ", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "group_members", + "name": "group_id" + } + } + }, + { + "name": "contact_id", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "group_members", + "name": "contact_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "6d6a5fb10742309ab6e4242179ef86b64ac9f751f1c6d647d4ecfa5ecac5d332" +} diff --git a/rust/.sqlx/query-6e69b233b4bd2b2a5aedb8057780a929eb6f3ac1dbe2b4c753ab0ec760a628fa.json b/rust/.sqlx/query-6e69b233b4bd2b2a5aedb8057780a929eb6f3ac1dbe2b4c753ab0ec760a628fa.json new file mode 100644 index 00000000..0977b5a7 --- /dev/null +++ b/rust/.sqlx/query-6e69b233b4bd2b2a5aedb8057780a929eb6f3ac1dbe2b4c753ab0ec760a628fa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET download_state = 'pending'\n WHERE media_id = ? AND download_state = 'downloading'", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "6e69b233b4bd2b2a5aedb8057780a929eb6f3ac1dbe2b4c753ab0ec760a628fa" +} diff --git a/rust/.sqlx/query-6e7d6401f94c0a597c4a62db858cc409d46c19d11f9d4419fc95caaf38968471.json b/rust/.sqlx/query-6e7d6401f94c0a597c4a62db858cc409d46c19d11f9d4419fc95caaf38968471.json new file mode 100644 index 00000000..fb752c5f --- /dev/null +++ b/rust/.sqlx/query-6e7d6401f94c0a597c4a62db858cc409d46c19d11f9d4419fc95caaf38968471.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT flame_counter FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "flame_counter", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "flame_counter" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "6e7d6401f94c0a597c4a62db858cc409d46c19d11f9d4419fc95caaf38968471" +} diff --git a/rust/.sqlx/query-713b45ba555a737f059569886ba38eedcd0664aa6c8a6d2190802b7a85e77371.json b/rust/.sqlx/query-713b45ba555a737f059569886ba38eedcd0664aa6c8a6d2190802b7a85e77371.json deleted file mode 100644 index 7a8dfc58..00000000 --- a/rust/.sqlx/query-713b45ba555a737f059569886ba38eedcd0664aa6c8a6d2190802b7a85e77371.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT EXISTS(\n SELECT 1 FROM contacts WHERE user_id = ?\n )\n ", - "describe": { - "columns": [ - { - "name": "EXISTS(\n SELECT 1 FROM contacts WHERE user_id = ?\n )", - "ordinal": 0, - "type_info": "Integer", - "origin": "Expression" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "713b45ba555a737f059569886ba38eedcd0664aa6c8a6d2190802b7a85e77371" -} diff --git a/rust/.sqlx/query-718877ef8c91c122ab283500ddf4594623234a5b8eca43f6b89baa0b17ff8df0.json b/rust/.sqlx/query-718877ef8c91c122ab283500ddf4594623234a5b8eca43f6b89baa0b17ff8df0.json new file mode 100644 index 00000000..943f3540 --- /dev/null +++ b/rust/.sqlx/query-718877ef8c91c122ab283500ddf4594623234a5b8eca43f6b89baa0b17ff8df0.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT m.message_id, m.sender_id, c.account_deleted\n FROM messages m\n LEFT JOIN contacts c ON c.user_id = m.sender_id\n WHERE m.media_id = ?", + "describe": { + "columns": [ + { + "name": "message_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "message_id" + } + } + }, + { + "name": "sender_id", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + }, + { + "name": "account_deleted", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "account_deleted" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + true + ] + }, + "hash": "718877ef8c91c122ab283500ddf4594623234a5b8eca43f6b89baa0b17ff8df0" +} diff --git a/rust/.sqlx/query-732496b1ad12c49c3aca6925d45abc4c801838e606d9bfd3f728f4e1dd91dd3a.json b/rust/.sqlx/query-732496b1ad12c49c3aca6925d45abc4c801838e606d9bfd3f728f4e1dd91dd3a.json new file mode 100644 index 00000000..8155b131 --- /dev/null +++ b/rust/.sqlx/query-732496b1ad12c49c3aca6925d45abc4c801838e606d9bfd3f728f4e1dd91dd3a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE receipts SET message_id = ?, will_be_retried_by_media_upload = ?,\n retry_count = ?, last_retry = ? WHERE receipt_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "732496b1ad12c49c3aca6925d45abc4c801838e606d9bfd3f728f4e1dd91dd3a" +} diff --git a/rust/.sqlx/query-7498999dcb013e18ec840820caeb996779d7aa4d74b69c8da97f0c429a41a3aa.json b/rust/.sqlx/query-7498999dcb013e18ec840820caeb996779d7aa4d74b69c8da97f0c429a41a3aa.json deleted file mode 100644 index 14ad027f..00000000 --- a/rust/.sqlx/query-7498999dcb013e18ec840820caeb996779d7aa4d74b69c8da97f0c429a41a3aa.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET requested = 0, accepted = 1, deleted_by_user = 0\n WHERE user_id = ? AND requested = 0\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "7498999dcb013e18ec840820caeb996779d7aa4d74b69c8da97f0c429a41a3aa" -} diff --git a/rust/.sqlx/query-7501f88f74cef11e63a654627e86ba29da7cd28fed9450c7c5754509cb855dfa.json b/rust/.sqlx/query-7501f88f74cef11e63a654627e86ba29da7cd28fed9450c7c5754509cb855dfa.json new file mode 100644 index 00000000..eb871cf6 --- /dev/null +++ b/rust/.sqlx/query-7501f88f74cef11e63a654627e86ba29da7cd28fed9450c7c5754509cb855dfa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET user_discovery_version = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "7501f88f74cef11e63a654627e86ba29da7cd28fed9450c7c5754509cb855dfa" +} diff --git a/rust/.sqlx/query-77b10c847e251dff552808d110d6e843d66e372742b4d225263b4c73406639eb.json b/rust/.sqlx/query-77b10c847e251dff552808d110d6e843d66e372742b4d225263b4c73406639eb.json new file mode 100644 index 00000000..1d45b0bc --- /dev/null +++ b/rust/.sqlx/query-77b10c847e251dff552808d110d6e843d66e372742b4d225263b4c73406639eb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET last_flame_sync = ? WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "77b10c847e251dff552808d110d6e843d66e372742b4d225263b4c73406639eb" +} diff --git a/rust/.sqlx/query-bd4750be71cfd64ab00d5a84d28f9df5cca1e61750a186328c65d384cfcefa15.json b/rust/.sqlx/query-78ad566837d0a3263b1daa3149971d1b2f75efcf7c03ae7ee452d7b9727d4ede.json similarity index 72% rename from rust/.sqlx/query-bd4750be71cfd64ab00d5a84d28f9df5cca1e61750a186328c65d384cfcefa15.json rename to rust/.sqlx/query-78ad566837d0a3263b1daa3149971d1b2f75efcf7c03ae7ee452d7b9727d4ede.json index 696e9659..481eb50e 100644 --- a/rust/.sqlx/query-bd4750be71cfd64ab00d5a84d28f9df5cca1e61750a186328c65d384cfcefa15.json +++ b/rust/.sqlx/query-78ad566837d0a3263b1daa3149971d1b2f75efcf7c03ae7ee452d7b9727d4ede.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT share_id, share\n FROM user_discovery_shares\n WHERE contact_id IS NULL\n LIMIT 1\n ", + "query": "SELECT share_id, share FROM user_discovery_shares WHERE contact_id IS NULL LIMIT 1", "describe": { "columns": [ { @@ -34,5 +34,5 @@ false ] }, - "hash": "bd4750be71cfd64ab00d5a84d28f9df5cca1e61750a186328c65d384cfcefa15" + "hash": "78ad566837d0a3263b1daa3149971d1b2f75efcf7c03ae7ee452d7b9727d4ede" } diff --git a/rust/.sqlx/query-794398164165a4a07387a201ce61b9b81b2712517abd4ae77565d7eac22bda41.json b/rust/.sqlx/query-794398164165a4a07387a201ce61b9b81b2712517abd4ae77565d7eac22bda41.json new file mode 100644 index 00000000..5387fa45 --- /dev/null +++ b/rust/.sqlx/query-794398164165a4a07387a201ce61b9b81b2712517abd4ae77565d7eac22bda41.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO messages(\n group_id,\n message_id,\n sender_id,\n type,\n content,\n media_id,\n additional_message_data,\n quotes_message_id,\n created_at,\n ack_by_server\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(message_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "794398164165a4a07387a201ce61b9b81b2712517abd4ae77565d7eac22bda41" +} diff --git a/rust/.sqlx/query-794e14789178809605447e9100342e824c9086817c1074503d8bf71e60828913.json b/rust/.sqlx/query-794e14789178809605447e9100342e824c9086817c1074503d8bf71e60828913.json new file mode 100644 index 00000000..8df54790 --- /dev/null +++ b/rust/.sqlx/query-794e14789178809605447e9100342e824c9086817c1074503d8bf71e60828913.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE groups \n SET group_name = ?, \n delete_messages_after_milliseconds = COALESCE(?, delete_messages_after_milliseconds), \n is_group_admin = ?, \n joined_group = ?, \n left_group = ?, \n state_version_id = ? \n WHERE group_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "794e14789178809605447e9100342e824c9086817c1074503d8bf71e60828913" +} diff --git a/rust/.sqlx/query-79a57c847e685ec171bba8db07dd9c76794b12d8cb04bf5f351636f8cd1409f8.json b/rust/.sqlx/query-79a57c847e685ec171bba8db07dd9c76794b12d8cb04bf5f351636f8cd1409f8.json new file mode 100644 index 00000000..111a9ed4 --- /dev/null +++ b/rust/.sqlx/query-79a57c847e685ec171bba8db07dd9c76794b12d8cb04bf5f351636f8cd1409f8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO message_actions(message_id, contact_id, type, action_at)\n VALUES (?, ?, 'openedAt', ?)\n ON CONFLICT(message_id, contact_id, type)\n DO UPDATE SET action_at = excluded.action_at", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "79a57c847e685ec171bba8db07dd9c76794b12d8cb04bf5f351636f8cd1409f8" +} diff --git a/rust/.sqlx/query-7ba6ad25a53db6d9ee725ad8dd06a58c119e4f445edf966b97016fb6d598f09a.json b/rust/.sqlx/query-7ba6ad25a53db6d9ee725ad8dd06a58c119e4f445edf966b97016fb6d598f09a.json new file mode 100644 index 00000000..0703e471 --- /dev/null +++ b/rust/.sqlx/query-7ba6ad25a53db6d9ee725ad8dd06a58c119e4f445edf966b97016fb6d598f09a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO messages(group_id, message_id, type, additional_message_data, created_at)\n VALUES (?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "7ba6ad25a53db6d9ee725ad8dd06a58c119e4f445edf966b97016fb6d598f09a" +} diff --git a/rust/.sqlx/query-7da111f61862ce2262bb377213c780e8a817a1bd5f53f9493c0f5314df312b44.json b/rust/.sqlx/query-7da111f61862ce2262bb377213c780e8a817a1bd5f53f9493c0f5314df312b44.json new file mode 100644 index 00000000..ad756273 --- /dev/null +++ b/rust/.sqlx/query-7da111f61862ce2262bb377213c780e8a817a1bd5f53f9493c0f5314df312b44.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT is_group_admin FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "is_group_admin", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "is_group_admin" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "7da111f61862ce2262bb377213c780e8a817a1bd5f53f9493c0f5314df312b44" +} diff --git a/rust/.sqlx/query-7da84cfdaf59649fe3735b81d4805de49530ed63fb35b06ff111743bd809d35d.json b/rust/.sqlx/query-7da84cfdaf59649fe3735b81d4805de49530ed63fb35b06ff111743bd809d35d.json new file mode 100644 index 00000000..488ba474 --- /dev/null +++ b/rust/.sqlx/query-7da84cfdaf59649fe3735b81d4805de49530ed63fb35b06ff111743bd809d35d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET account_deleted = 1 WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "7da84cfdaf59649fe3735b81d4805de49530ed63fb35b06ff111743bd809d35d" +} diff --git a/rust/.sqlx/query-7eb90b44fd4140ce19db1cf4f953910faa6c32fd434dcdfb7ddc2e4d8dc5c97b.json b/rust/.sqlx/query-7eb90b44fd4140ce19db1cf4f953910faa6c32fd434dcdfb7ddc2e4d8dc5c97b.json new file mode 100644 index 00000000..a137ca1a --- /dev/null +++ b/rust/.sqlx/query-7eb90b44fd4140ce19db1cf4f953910faa6c32fd434dcdfb7ddc2e4d8dc5c97b.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT m.sender_id, m.media_id, f.download_state\n FROM messages m\n LEFT JOIN media_files f ON f.media_id = m.media_id\n WHERE message_id = ?\n ", + "describe": { + "columns": [ + { + "name": "sender_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + }, + { + "name": "media_id", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "media_id" + } + } + }, + { + "name": "download_state", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "download_state" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + true + ] + }, + "hash": "7eb90b44fd4140ce19db1cf4f953910faa6c32fd434dcdfb7ddc2e4d8dc5c97b" +} diff --git a/rust/.sqlx/query-7ee254f54718b616580ea6ca979679f8ec8065490cf0ffd0a8f44818c1f57167.json b/rust/.sqlx/query-7ee254f54718b616580ea6ca979679f8ec8065490cf0ffd0a8f44818c1f57167.json new file mode 100644 index 00000000..ae7ac386 --- /dev/null +++ b/rust/.sqlx/query-7ee254f54718b616580ea6ca979679f8ec8065490cf0ffd0a8f44818c1f57167.json @@ -0,0 +1,184 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,\n COALESCE(c.display_name, c.username) AS \"sender_name!: String\",\n c.avatar_svg_compressed, c.sender_profile_counter,\n g.group_name AS conversation_name,\n COALESCE(g.is_direct_chat, 0) AS \"is_direct_chat!: i64\",\n n.message_id, n.kind, n.content, n.created_at,\n target_message.type AS target_message_type,\n target_media.type AS target_media_type\n FROM notification_outbox n\n JOIN contacts c ON c.user_id = n.sender_id\n LEFT JOIN groups g ON g.group_id = n.conversation_id\n LEFT JOIN messages target_message ON target_message.message_id = n.message_id\n LEFT JOIN media_files target_media ON target_media.media_id = target_message.media_id\n WHERE n.delivered_at IS NULL AND n.cleared_at IS NULL\n ORDER BY n.created_at, n.event_id\n LIMIT ?\n ", + "describe": { + "columns": [ + { + "name": "event_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "event_id" + } + } + }, + { + "name": "notification_id", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "notification_id" + } + } + }, + { + "name": "conversation_id", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "conversation_id" + } + } + }, + { + "name": "sender_id", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "sender_id" + } + } + }, + { + "name": "sender_name!: String", + "ordinal": 4, + "type_info": "Text", + "origin": "Expression" + }, + { + "name": "avatar_svg_compressed", + "ordinal": 5, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "avatar_svg_compressed" + } + } + }, + { + "name": "sender_profile_counter", + "ordinal": 6, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "sender_profile_counter" + } + } + }, + { + "name": "conversation_name", + "ordinal": 7, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_name" + } + } + }, + { + "name": "is_direct_chat!: i64", + "ordinal": 8, + "type_info": "Integer", + "origin": "Expression" + }, + { + "name": "message_id", + "ordinal": 9, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "message_id" + } + } + }, + { + "name": "kind", + "ordinal": 10, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "kind" + } + } + }, + { + "name": "content", + "ordinal": 11, + "type_info": "Text", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "content" + } + } + }, + { + "name": "created_at", + "ordinal": 12, + "type_info": "Integer", + "origin": { + "Table": { + "table": "notification_outbox", + "name": "created_at" + } + } + }, + { + "name": "target_message_type", + "ordinal": 13, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "type" + } + } + }, + { + "name": "target_media_type", + "ordinal": 14, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "type" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false, + true, + false, + true, + false, + true, + false, + true, + false, + true, + true + ] + }, + "hash": "7ee254f54718b616580ea6ca979679f8ec8065490cf0ffd0a8f44818c1f57167" +} diff --git a/rust/.sqlx/query-81135d8e9734594afc97deee965b5b2db4edab812e94912b4a70c3a83ad91933.json b/rust/.sqlx/query-81135d8e9734594afc97deee965b5b2db4edab812e94912b4a70c3a83ad91933.json deleted file mode 100644 index ff979a32..00000000 --- a/rust/.sqlx/query-81135d8e9734594afc97deee965b5b2db4edab812e94912b4a70c3a83ad91933.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE messages\n SET opened_at = ?, opened_by_all = ?\n WHERE message_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "81135d8e9734594afc97deee965b5b2db4edab812e94912b4a70c3a83ad91933" -} diff --git a/rust/.sqlx/query-815f8d36932a7cad2f415eef0e2252acb4f823132f89b42d697c83be5beca994.json b/rust/.sqlx/query-815f8d36932a7cad2f415eef0e2252acb4f823132f89b42d697c83be5beca994.json new file mode 100644 index 00000000..fec99aa2 --- /dev/null +++ b/rust/.sqlx/query-815f8d36932a7cad2f415eef0e2252acb4f823132f89b42d697c83be5beca994.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT user_id, recovery_secret_share FROM contacts\n WHERE recovery_is_trusted_friend = 1\n AND recovery_last_heartbeat IS NULL\n AND recovery_secret_share IS NOT NULL", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_id" + } + } + }, + { + "name": "recovery_secret_share", + "ordinal": 1, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_secret_share" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true + ] + }, + "hash": "815f8d36932a7cad2f415eef0e2252acb4f823132f89b42d697c83be5beca994" +} diff --git a/rust/.sqlx/query-83913e710fbd04e98cd6de21e7c917ed6472ef90230a10a69f74cb07d31a99fa.json b/rust/.sqlx/query-83913e710fbd04e98cd6de21e7c917ed6472ef90230a10a69f74cb07d31a99fa.json new file mode 100644 index 00000000..e9f6ed5c --- /dev/null +++ b/rust/.sqlx/query-83913e710fbd04e98cd6de21e7c917ed6472ef90230a10a69f74cb07d31a99fa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM receipts WHERE contact_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "83913e710fbd04e98cd6de21e7c917ed6472ef90230a10a69f74cb07d31a99fa" +} diff --git a/rust/.sqlx/query-84098bf2101af287c203998ebf38377a472c93b4baf0a6970c8f8075b7aeae33.json b/rust/.sqlx/query-84098bf2101af287c203998ebf38377a472c93b4baf0a6970c8f8075b7aeae33.json new file mode 100644 index 00000000..8c2a450c --- /dev/null +++ b/rust/.sqlx/query-84098bf2101af287c203998ebf38377a472c93b4baf0a6970c8f8075b7aeae33.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO user_discovery_other_promotions (\n from_contact_id, promotion_id, public_id, threshold,\n announcement_share, public_key_verified_timestamp\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(from_contact_id, public_id) DO UPDATE SET\n promotion_id = excluded.promotion_id,\n threshold = excluded.threshold,\n announcement_share = excluded.announcement_share,\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "84098bf2101af287c203998ebf38377a472c93b4baf0a6970c8f8075b7aeae33" +} diff --git a/rust/.sqlx/query-84c77f8397fdf0dcf0d3c9ccab7f3854d2d607de0662b9e5f7d279f5e9417ebf.json b/rust/.sqlx/query-84c77f8397fdf0dcf0d3c9ccab7f3854d2d607de0662b9e5f7d279f5e9417ebf.json deleted file mode 100644 index 6b5d5241..00000000 --- a/rust/.sqlx/query-84c77f8397fdf0dcf0d3c9ccab7f3854d2d607de0662b9e5f7d279f5e9417ebf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET requested = 1, deleted_by_user = 0\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "84c77f8397fdf0dcf0d3c9ccab7f3854d2d607de0662b9e5f7d279f5e9417ebf" -} diff --git a/rust/.sqlx/query-84efd2ee67643544a39e5bc79b33db8ddb3697ee130fa93f3c8ea0665c449d42.json b/rust/.sqlx/query-84efd2ee67643544a39e5bc79b33db8ddb3697ee130fa93f3c8ea0665c449d42.json new file mode 100644 index 00000000..7bf7a0e0 --- /dev/null +++ b/rust/.sqlx/query-84efd2ee67643544a39e5bc79b33db8ddb3697ee130fa93f3c8ea0665c449d42.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_public_key FROM group_members WHERE group_id = ? AND contact_id = ?", + "describe": { + "columns": [ + { + "name": "group_public_key", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "group_members", + "name": "group_public_key" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true + ] + }, + "hash": "84efd2ee67643544a39e5bc79b33db8ddb3697ee130fa93f3c8ea0665c449d42" +} diff --git a/rust/.sqlx/query-852a504566c7e3f6faab472a5c0c9bdbf88b4c308097d8fea31f122ac980144c.json b/rust/.sqlx/query-852a504566c7e3f6faab472a5c0c9bdbf88b4c308097d8fea31f122ac980144c.json new file mode 100644 index 00000000..29b71c0e --- /dev/null +++ b/rust/.sqlx/query-852a504566c7e3f6faab472a5c0c9bdbf88b4c308097d8fea31f122ac980144c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO api_outbox(sequence_id, operation_kind, payload) VALUES(?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "852a504566c7e3f6faab472a5c0c9bdbf88b4c308097d8fea31f122ac980144c" +} diff --git a/rust/.sqlx/query-856ba1dc64d80914973128cefd776a9442a6a73ba94145c85245780a86b241e8.json b/rust/.sqlx/query-856ba1dc64d80914973128cefd776a9442a6a73ba94145c85245780a86b241e8.json new file mode 100644 index 00000000..2fb5192b --- /dev/null +++ b/rust/.sqlx/query-856ba1dc64d80914973128cefd776a9442a6a73ba94145c85245780a86b241e8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE contacts\n SET user_discovery_excluded = ?, user_discovery_version = NULL\n WHERE user_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "856ba1dc64d80914973128cefd776a9442a6a73ba94145c85245780a86b241e8" +} diff --git a/rust/.sqlx/query-869eb7007b683a3415ac0bc00e929aa117302944f52b95f6dece05c52b4fc905.json b/rust/.sqlx/query-869eb7007b683a3415ac0bc00e929aa117302944f52b95f6dece05c52b4fc905.json new file mode 100644 index 00000000..c0cc5d3d --- /dev/null +++ b/rust/.sqlx/query-869eb7007b683a3415ac0bc00e929aa117302944f52b95f6dece05c52b4fc905.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM key_verifications WHERE contact_id = ? AND type = 'manualTest'", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "869eb7007b683a3415ac0bc00e929aa117302944f52b95f6dece05c52b4fc905" +} diff --git a/rust/.sqlx/query-86f467ce150a319885c90ca3e582f2cc26f097a9094ddf600c3d7462f988858e.json b/rust/.sqlx/query-86f467ce150a319885c90ca3e582f2cc26f097a9094ddf600c3d7462f988858e.json new file mode 100644 index 00000000..f6e6d393 --- /dev/null +++ b/rust/.sqlx/query-86f467ce150a319885c90ca3e582f2cc26f097a9094ddf600c3d7462f988858e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE receipts\n SET mark_for_retry = CAST(strftime('%s', 'now') AS INTEGER)\n WHERE contact_id = ? AND mark_for_retry IS NULL\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "86f467ce150a319885c90ca3e582f2cc26f097a9094ddf600c3d7462f988858e" +} diff --git a/rust/.sqlx/query-87eac4d0b721d8726db7fe81b8842c877dbc1de54b276baca6f4f90d58e77406.json b/rust/.sqlx/query-87eac4d0b721d8726db7fe81b8842c877dbc1de54b276baca6f4f90d58e77406.json new file mode 100644 index 00000000..499dabfc --- /dev/null +++ b/rust/.sqlx/query-87eac4d0b721d8726db7fe81b8842c877dbc1de54b276baca6f4f90d58e77406.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT is_direct_chat FROM groups WHERE group_id = ?", + "describe": { + "columns": [ + { + "name": "is_direct_chat", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "groups", + "name": "is_direct_chat" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "87eac4d0b721d8726db7fe81b8842c877dbc1de54b276baca6f4f90d58e77406" +} diff --git a/rust/.sqlx/query-b3242f4964184d187d818d2e112aa826377c5ce5e09749f780c3a6a9870157d9.json b/rust/.sqlx/query-8ec24eb618ebb570820cc72e5f8f6dafa2efb86d5054b1f38ca2b6ccfa95a79d.json similarity index 52% rename from rust/.sqlx/query-b3242f4964184d187d818d2e112aa826377c5ce5e09749f780c3a6a9870157d9.json rename to rust/.sqlx/query-8ec24eb618ebb570820cc72e5f8f6dafa2efb86d5054b1f38ca2b6ccfa95a79d.json index 7b8012e9..c7644191 100644 --- a/rust/.sqlx/query-b3242f4964184d187d818d2e112aa826377c5ce5e09749f780c3a6a9870157d9.json +++ b/rust/.sqlx/query-8ec24eb618ebb570820cc72e5f8f6dafa2efb86d5054b1f38ca2b6ccfa95a79d.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "\n INSERT INTO contacts (user_id, username)\n VALUES (1, 'one'), (2, 'two')\n ", + "query": "DELETE FROM group_members WHERE group_id = ?", "describe": { "columns": [], "parameters": { - "Right": 0 + "Right": 1 }, "nullable": [] }, - "hash": "b3242f4964184d187d818d2e112aa826377c5ce5e09749f780c3a6a9870157d9" + "hash": "8ec24eb618ebb570820cc72e5f8f6dafa2efb86d5054b1f38ca2b6ccfa95a79d" } diff --git a/rust/.sqlx/query-6c01273c757a772f2c2726b75a95df7f39ee9353043ec309fe128183fe1cdcd6.json b/rust/.sqlx/query-8f818e8dc1be8e9bcb4fd908e3cf47fbbfe50c98b7fa506aee637e06e1ab8029.json similarity index 67% rename from rust/.sqlx/query-6c01273c757a772f2c2726b75a95df7f39ee9353043ec309fe128183fe1cdcd6.json rename to rust/.sqlx/query-8f818e8dc1be8e9bcb4fd908e3cf47fbbfe50c98b7fa506aee637e06e1ab8029.json index 94a3b1a1..3a06ac5e 100644 --- a/rust/.sqlx/query-6c01273c757a772f2c2726b75a95df7f39ee9353043ec309fe128183fe1cdcd6.json +++ b/rust/.sqlx/query-8f818e8dc1be8e9bcb4fd908e3cf47fbbfe50c98b7fa506aee637e06e1ab8029.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT COUNT(*) FROM reactions", + "query": "SELECT COUNT(*) FROM user_discovery_shares", "describe": { "columns": [ { @@ -17,5 +17,5 @@ false ] }, - "hash": "6c01273c757a772f2c2726b75a95df7f39ee9353043ec309fe128183fe1cdcd6" + "hash": "8f818e8dc1be8e9bcb4fd908e3cf47fbbfe50c98b7fa506aee637e06e1ab8029" } diff --git a/rust/.sqlx/query-90cb5a536c050635d321454a8bf027bf836e5b52681edce95a53d8175cceee48.json b/rust/.sqlx/query-90cb5a536c050635d321454a8bf027bf836e5b52681edce95a53d8175cceee48.json new file mode 100644 index 00000000..b0597612 --- /dev/null +++ b/rust/.sqlx/query-90cb5a536c050635d321454a8bf027bf836e5b52681edce95a53d8175cceee48.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO group_histories(\n group_history_id,\n group_id,\n contact_id,\n old_group_name,\n new_group_name,\n type\n ) VALUES (?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "90cb5a536c050635d321454a8bf027bf836e5b52681edce95a53d8175cceee48" +} diff --git a/rust/.sqlx/query-90fddc5282c6f13b56cd08e39dd24eec89963291cf50ad69ad13916953ec0539.json b/rust/.sqlx/query-90fddc5282c6f13b56cd08e39dd24eec89963291cf50ad69ad13916953ec0539.json new file mode 100644 index 00000000..9a1539ca --- /dev/null +++ b/rust/.sqlx/query-90fddc5282c6f13b56cd08e39dd24eec89963291cf50ad69ad13916953ec0539.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT type FROM media_files WHERE media_id = ?", + "describe": { + "columns": [ + { + "name": "type", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "type" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "90fddc5282c6f13b56cd08e39dd24eec89963291cf50ad69ad13916953ec0539" +} diff --git a/rust/.sqlx/query-93e9fb25a53d3de02840e86d0c450c364e2e4c3b4af9d015fb2fbb87770e703f.json b/rust/.sqlx/query-93e9fb25a53d3de02840e86d0c450c364e2e4c3b4af9d015fb2fbb87770e703f.json deleted file mode 100644 index dace5654..00000000 --- a/rust/.sqlx/query-93e9fb25a53d3de02840e86d0c450c364e2e4c3b4af9d015fb2fbb87770e703f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO groups(group_id, group_name, is_direct_chat, joined_group)\n VALUES (?, ?, 1, 1)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "93e9fb25a53d3de02840e86d0c450c364e2e4c3b4af9d015fb2fbb87770e703f" -} diff --git a/rust/.sqlx/query-94f378d23149fc82d31b600b76ecd89af66301c64b0a30d3b82f285f5647e5ac.json b/rust/.sqlx/query-94f378d23149fc82d31b600b76ecd89af66301c64b0a30d3b82f285f5647e5ac.json new file mode 100644 index 00000000..d78781a8 --- /dev/null +++ b/rust/.sqlx/query-94f378d23149fc82d31b600b76ecd89af66301c64b0a30d3b82f285f5647e5ac.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO contacts(user_id, username, accepted, requested) VALUES (?, '[deleted]', 1, 0)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "94f378d23149fc82d31b600b76ecd89af66301c64b0a30d3b82f285f5647e5ac" +} diff --git a/rust/.sqlx/query-96830f8fde36c6db93fb959c0e140effefbb271e84813a4d85fe255e90b28695.json b/rust/.sqlx/query-96830f8fde36c6db93fb959c0e140effefbb271e84813a4d85fe255e90b28695.json new file mode 100644 index 00000000..79f3b5b9 --- /dev/null +++ b/rust/.sqlx/query-96830f8fde36c6db93fb959c0e140effefbb271e84813a4d85fe255e90b28695.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT username FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "username" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "96830f8fde36c6db93fb959c0e140effefbb271e84813a4d85fe255e90b28695" +} diff --git a/rust/.sqlx/query-96a6b8415b84654e0bc41298aa4ca886e42740dddf2e4db1a4a289e4f75a1ff4.json b/rust/.sqlx/query-96a6b8415b84654e0bc41298aa4ca886e42740dddf2e4db1a4a289e4f75a1ff4.json deleted file mode 100644 index 926f2c7e..00000000 --- a/rust/.sqlx/query-96a6b8415b84654e0bc41298aa4ca886e42740dddf2e4db1a4a289e4f75a1ff4.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT EXISTS(\n SELECT 1\n FROM received_receipts\n WHERE receipt_id = ?\n )\n ", - "describe": { - "columns": [ - { - "name": "EXISTS(\n SELECT 1\n FROM received_receipts\n WHERE receipt_id = ?\n )", - "ordinal": 0, - "type_info": "Integer", - "origin": "Expression" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "96a6b8415b84654e0bc41298aa4ca886e42740dddf2e4db1a4a289e4f75a1ff4" -} diff --git a/rust/.sqlx/query-96dfd5c9fea55bbc19f096c6d3d2d15cbf4bd389ccca0ea4831ddbd956b711c5.json b/rust/.sqlx/query-96dfd5c9fea55bbc19f096c6d3d2d15cbf4bd389ccca0ea4831ddbd956b711c5.json new file mode 100644 index 00000000..2a637e29 --- /dev/null +++ b/rust/.sqlx/query-96dfd5c9fea55bbc19f096c6d3d2d15cbf4bd389ccca0ea4831ddbd956b711c5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO contacts(user_id, username, signal_version, accepted, requested)\n VALUES (?, ?, ?, 0, 1)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "96dfd5c9fea55bbc19f096c6d3d2d15cbf4bd389ccca0ea4831ddbd956b711c5" +} diff --git a/rust/.sqlx/query-986b8712045ce40ce4c6cd7b46f28472f0d22b0b37453c5d53bab12aeb153553.json b/rust/.sqlx/query-986b8712045ce40ce4c6cd7b46f28472f0d22b0b37453c5d53bab12aeb153553.json new file mode 100644 index 00000000..c11fd632 --- /dev/null +++ b/rust/.sqlx/query-986b8712045ce40ce4c6cd7b46f28472f0d22b0b37453c5d53bab12aeb153553.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_id FROM group_members WHERE contact_id = ?", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "group_members", + "name": "group_id" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "986b8712045ce40ce4c6cd7b46f28472f0d22b0b37453c5d53bab12aeb153553" +} diff --git a/rust/.sqlx/query-987d30f5e96500da929cd3417030c284bc17657b9ea34e25e80cb5672f3c5abc.json b/rust/.sqlx/query-987d30f5e96500da929cd3417030c284bc17657b9ea34e25e80cb5672f3c5abc.json new file mode 100644 index 00000000..bd9f6aed --- /dev/null +++ b/rust/.sqlx/query-987d30f5e96500da929cd3417030c284bc17657b9ea34e25e80cb5672f3c5abc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE group_members SET member_state = ? WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "987d30f5e96500da929cd3417030c284bc17657b9ea34e25e80cb5672f3c5abc" +} diff --git a/rust/.sqlx/query-9a69d1998079dc207b5c9a4b62ff804b96475d3b5b25ef55822011610967ace2.json b/rust/.sqlx/query-9a69d1998079dc207b5c9a4b62ff804b96475d3b5b25ef55822011610967ace2.json new file mode 100644 index 00000000..7d00687e --- /dev/null +++ b/rust/.sqlx/query-9a69d1998079dc207b5c9a4b62ff804b96475d3b5b25ef55822011610967ace2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO user_discovery_own_promotions(contact_id, promotion) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "9a69d1998079dc207b5c9a4b62ff804b96475d3b5b25ef55822011610967ace2" +} diff --git a/rust/.sqlx/query-9bc6a3746c3a8df3b089a34a4e70098d5691f4511904fe17ab67885c07a7dbe8.json b/rust/.sqlx/query-9bc6a3746c3a8df3b089a34a4e70098d5691f4511904fe17ab67885c07a7dbe8.json new file mode 100644 index 00000000..9d3dd508 --- /dev/null +++ b/rust/.sqlx/query-9bc6a3746c3a8df3b089a34a4e70098d5691f4511904fe17ab67885c07a7dbe8.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8", + "describe": { + "columns": [ + { + "name": "recovery_last_heartbeat", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_last_heartbeat" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true + ] + }, + "hash": "9bc6a3746c3a8df3b089a34a4e70098d5691f4511904fe17ab67885c07a7dbe8" +} diff --git a/rust/.sqlx/query-9de60ce6ee3cf9449d5142a94637426faa727329c78224909732cde2205cf631.json b/rust/.sqlx/query-9de60ce6ee3cf9449d5142a94637426faa727329c78224909732cde2205cf631.json new file mode 100644 index 00000000..ef8154a2 --- /dev/null +++ b/rust/.sqlx/query-9de60ce6ee3cf9449d5142a94637426faa727329c78224909732cde2205cf631.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE group_members SET group_public_key = ? WHERE group_id = ? AND contact_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "9de60ce6ee3cf9449d5142a94637426faa727329c78224909732cde2205cf631" +} diff --git a/rust/.sqlx/query-9e596b383a0af665ed0645792af43636345cf9372c0d4005df9d179f1004a619.json b/rust/.sqlx/query-9e596b383a0af665ed0645792af43636345cf9372c0d4005df9d179f1004a619.json new file mode 100644 index 00000000..bb6d4f8e --- /dev/null +++ b/rust/.sqlx/query-9e596b383a0af665ed0645792af43636345cf9372c0d4005df9d179f1004a619.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET download_state = 'pending' WHERE download_state = 'downloading'", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "9e596b383a0af665ed0645792af43636345cf9372c0d4005df9d179f1004a619" +} diff --git a/rust/.sqlx/query-9e7086794e7e4cb9b9db781830423875720f5458d209f9ff4ecbed856dd45e08.json b/rust/.sqlx/query-9e7086794e7e4cb9b9db781830423875720f5458d209f9ff4ecbed856dd45e08.json new file mode 100644 index 00000000..e709250e --- /dev/null +++ b/rust/.sqlx/query-9e7086794e7e4cb9b9db781830423875720f5458d209f9ff4ecbed856dd45e08.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT sender_profile_counter FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "sender_profile_counter", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "sender_profile_counter" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "9e7086794e7e4cb9b9db781830423875720f5458d209f9ff4ecbed856dd45e08" +} diff --git a/rust/.sqlx/query-9d236335cde01fe13d6836309e7a4bcb8485282d2230d2504350642ce1236169.json b/rust/.sqlx/query-a0e47f74f1f40f409dd51e4a3cc2638d5ca30a0f7db6fd3d620c597ad630cecc.json similarity index 58% rename from rust/.sqlx/query-9d236335cde01fe13d6836309e7a4bcb8485282d2230d2504350642ce1236169.json rename to rust/.sqlx/query-a0e47f74f1f40f409dd51e4a3cc2638d5ca30a0f7db6fd3d620c597ad630cecc.json index 45809abf..3542fb8f 100644 --- a/rust/.sqlx/query-9d236335cde01fe13d6836309e7a4bcb8485282d2230d2504350642ce1236169.json +++ b/rust/.sqlx/query-a0e47f74f1f40f409dd51e4a3cc2638d5ca30a0f7db6fd3d620c597ad630cecc.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT promotion\n FROM user_discovery_own_promotions\n WHERE contact_id = ?\n ORDER BY version_id DESC\n LIMIT 1\n ", + "query": "SELECT promotion FROM user_discovery_own_promotions WHERE version_id > ?", "describe": { "columns": [ { @@ -22,5 +22,5 @@ false ] }, - "hash": "9d236335cde01fe13d6836309e7a4bcb8485282d2230d2504350642ce1236169" + "hash": "a0e47f74f1f40f409dd51e4a3cc2638d5ca30a0f7db6fd3d620c597ad630cecc" } diff --git a/rust/.sqlx/query-a0e60588e599879c2d5b5c40088f3a0e6caf0061e009784d225d0b863487c2aa.json b/rust/.sqlx/query-a0e60588e599879c2d5b5c40088f3a0e6caf0061e009784d225d0b863487c2aa.json new file mode 100644 index 00000000..1bfc7fc9 --- /dev/null +++ b/rust/.sqlx/query-a0e60588e599879c2d5b5c40088f3a0e6caf0061e009784d225d0b863487c2aa.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT sender_id, type, additional_message_data FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "sender_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + }, + { + "name": "type", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "type" + } + } + }, + { + "name": "additional_message_data", + "ordinal": 2, + "type_info": "Blob", + "origin": { + "Table": { + "table": "messages", + "name": "additional_message_data" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + true + ] + }, + "hash": "a0e60588e599879c2d5b5c40088f3a0e6caf0061e009784d225d0b863487c2aa" +} diff --git a/rust/.sqlx/query-a2b5abc3e3b81290f8b86db19f4e14ad964e8b614c7698e864da79e906c3737c.json b/rust/.sqlx/query-a2b5abc3e3b81290f8b86db19f4e14ad964e8b614c7698e864da79e906c3737c.json new file mode 100644 index 00000000..92bd3cf7 --- /dev/null +++ b/rust/.sqlx/query-a2b5abc3e3b81290f8b86db19f4e14ad964e8b614c7698e864da79e906c3737c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM receipts WHERE receipt_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "a2b5abc3e3b81290f8b86db19f4e14ad964e8b614c7698e864da79e906c3737c" +} diff --git a/rust/.sqlx/query-a2e8523eb0be989585370d3d8b1d9a5586a1f75b84a609c61fd2f18faa67832f.json b/rust/.sqlx/query-a2e8523eb0be989585370d3d8b1d9a5586a1f75b84a609c61fd2f18faa67832f.json new file mode 100644 index 00000000..89ec1a50 --- /dev/null +++ b/rust/.sqlx/query-a2e8523eb0be989585370d3d8b1d9a5586a1f75b84a609c61fd2f18faa67832f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE receipts SET\n ack_by_server_at = CAST(strftime('%s', 'now') AS INTEGER),\n retry_count = retry_count + 1,\n last_retry = CAST(strftime('%s', 'now') AS INTEGER),\n mark_for_retry = NULL\n WHERE receipt_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "a2e8523eb0be989585370d3d8b1d9a5586a1f75b84a609c61fd2f18faa67832f" +} diff --git a/rust/.sqlx/query-500c182a7888c7b36c257936ba188aff109a5dac250d5402f33b55ec9d127f76.json b/rust/.sqlx/query-a4277bba7d52fe5f3d87a5d0148d46fea6e798610c1248f95cd919e10257d4ad.json similarity index 53% rename from rust/.sqlx/query-500c182a7888c7b36c257936ba188aff109a5dac250d5402f33b55ec9d127f76.json rename to rust/.sqlx/query-a4277bba7d52fe5f3d87a5d0148d46fea6e798610c1248f95cd919e10257d4ad.json index 4def1f40..ade00ef5 100644 --- a/rust/.sqlx/query-500c182a7888c7b36c257936ba188aff109a5dac250d5402f33b55ec9d127f76.json +++ b/rust/.sqlx/query-a4277bba7d52fe5f3d87a5d0148d46fea6e798610c1248f95cd919e10257d4ad.json @@ -1,21 +1,21 @@ { "db_name": "SQLite", - "query": "\n SELECT EXISTS(\n SELECT 1\n FROM received_receipts\n WHERE receipt_id = ?\n )\n ", + "query": "\n SELECT EXISTS(\n SELECT 1\n FROM group_members\n WHERE group_id = ? AND contact_id = ?\n )\n ", "describe": { "columns": [ { - "name": "EXISTS(\n SELECT 1\n FROM received_receipts\n WHERE receipt_id = ?\n )", + "name": "EXISTS(\n SELECT 1\n FROM group_members\n WHERE group_id = ? AND contact_id = ?\n )", "ordinal": 0, "type_info": "Integer", "origin": "Expression" } ], "parameters": { - "Right": 1 + "Right": 2 }, "nullable": [ false ] }, - "hash": "500c182a7888c7b36c257936ba188aff109a5dac250d5402f33b55ec9d127f76" + "hash": "a4277bba7d52fe5f3d87a5d0148d46fea6e798610c1248f95cd919e10257d4ad" } diff --git a/rust/.sqlx/query-a54b3db0042be8b336a097a62a27ceb79c2983736ea880483b1be71ae7a2af94.json b/rust/.sqlx/query-a54b3db0042be8b336a097a62a27ceb79c2983736ea880483b1be71ae7a2af94.json new file mode 100644 index 00000000..9fdc959e --- /dev/null +++ b/rust/.sqlx/query-a54b3db0042be8b336a097a62a27ceb79c2983736ea880483b1be71ae7a2af94.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO groups(\n group_id,\n group_name,\n is_group_admin,\n state_encryption_key,\n state_version_id,\n my_group_private_key,\n joined_group\n ) VALUES (?, ?, 1, ?, 1, ?, 1)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "a54b3db0042be8b336a097a62a27ceb79c2983736ea880483b1be71ae7a2af94" +} diff --git a/rust/.sqlx/query-a55a2f3891948d7260a1fa7cb250596e8dca1e86c487657d0043f5e369131b0b.json b/rust/.sqlx/query-a55a2f3891948d7260a1fa7cb250596e8dca1e86c487657d0043f5e369131b0b.json new file mode 100644 index 00000000..24bb1dc7 --- /dev/null +++ b/rust/.sqlx/query-a55a2f3891948d7260a1fa7cb250596e8dca1e86c487657d0043f5e369131b0b.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT username, display_name FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "username" + } + } + }, + { + "name": "display_name", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "display_name" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true + ] + }, + "hash": "a55a2f3891948d7260a1fa7cb250596e8dca1e86c487657d0043f5e369131b0b" +} diff --git a/rust/.sqlx/query-a57dd1863c2d8db90075c5dc0b3550711ad15801756d571dfa97c9907969348b.json b/rust/.sqlx/query-a57dd1863c2d8db90075c5dc0b3550711ad15801756d571dfa97c9907969348b.json new file mode 100644 index 00000000..ae81de22 --- /dev/null +++ b/rust/.sqlx/query-a57dd1863c2d8db90075c5dc0b3550711ad15801756d571dfa97c9907969348b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET blocked = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "a57dd1863c2d8db90075c5dc0b3550711ad15801756d571dfa97c9907969348b" +} diff --git a/rust/.sqlx/query-a8edfc3a445195355f8324396c28396a3298fecd579a5e9fbc84ae4974685724.json b/rust/.sqlx/query-a8edfc3a445195355f8324396c28396a3298fecd579a5e9fbc84ae4974685724.json new file mode 100644 index 00000000..178c95a0 --- /dev/null +++ b/rust/.sqlx/query-a8edfc3a445195355f8324396c28396a3298fecd579a5e9fbc84ae4974685724.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_id FROM groups WHERE is_direct_chat = 0 ORDER BY rowid DESC LIMIT 1", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "a8edfc3a445195355f8324396c28396a3298fecd579a5e9fbc84ae4974685724" +} diff --git a/rust/.sqlx/query-a90a25b72f9fb6269a4943fda3da3c8cc58b2d692fb79e9445e070d949afc9ab.json b/rust/.sqlx/query-a90a25b72f9fb6269a4943fda3da3c8cc58b2d692fb79e9445e070d949afc9ab.json deleted file mode 100644 index 1d7abca1..00000000 --- a/rust/.sqlx/query-a90a25b72f9fb6269a4943fda3da3c8cc58b2d692fb79e9445e070d949afc9ab.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO messages(\n group_id,\n message_id,\n sender_id,\n type,\n additional_message_data,\n created_at,\n ack_by_server\n ) VALUES (?, ?, ?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))\n ON CONFLICT(message_id) DO NOTHING\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "a90a25b72f9fb6269a4943fda3da3c8cc58b2d692fb79e9445e070d949afc9ab" -} diff --git a/rust/.sqlx/query-a93838b8031d794e21141424f5a7856c33d05ff01422e040df34783dffcc9b91.json b/rust/.sqlx/query-a93838b8031d794e21141424f5a7856c33d05ff01422e040df34783dffcc9b91.json new file mode 100644 index 00000000..7b63f485 --- /dev/null +++ b/rust/.sqlx/query-a93838b8031d794e21141424f5a7856c33d05ff01422e040df34783dffcc9b91.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7", + "describe": { + "columns": [ + { + "name": "recovery_contacts_secret_share", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_secret_share" + } + } + }, + { + "name": "recovery_contacts_threshold", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_threshold" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + true + ] + }, + "hash": "a93838b8031d794e21141424f5a7856c33d05ff01422e040df34783dffcc9b91" +} diff --git a/rust/.sqlx/query-abdea64654e2f35b26a349227190a84fb4eebe583da68e21353005bd87c15058.json b/rust/.sqlx/query-abdea64654e2f35b26a349227190a84fb4eebe583da68e21353005bd87c15058.json new file mode 100644 index 00000000..6d1feead --- /dev/null +++ b/rust/.sqlx/query-abdea64654e2f35b26a349227190a84fb4eebe583da68e21353005bd87c15058.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) FROM notification_outbox WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "abdea64654e2f35b26a349227190a84fb4eebe583da68e21353005bd87c15058" +} diff --git a/rust/.sqlx/query-ac89da5b76bdd44d6c3c8901b2d99ff9352ab497a2abb687a1b8ef3cb09701ab.json b/rust/.sqlx/query-ac89da5b76bdd44d6c3c8901b2d99ff9352ab497a2abb687a1b8ef3cb09701ab.json deleted file mode 100644 index 156adfdb..00000000 --- a/rust/.sqlx/query-ac89da5b76bdd44d6c3c8901b2d99ff9352ab497a2abb687a1b8ef3cb09701ab.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT EXISTS(\n SELECT 1\n FROM group_members\n WHERE group_id = ? AND contact_id = ?\n )\n ", - "describe": { - "columns": [ - { - "name": "EXISTS(\n SELECT 1\n FROM group_members\n WHERE group_id = ? AND contact_id = ?\n )", - "ordinal": 0, - "type_info": "Integer", - "origin": "Expression" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false - ] - }, - "hash": "ac89da5b76bdd44d6c3c8901b2d99ff9352ab497a2abb687a1b8ef3cb09701ab" -} diff --git a/rust/.sqlx/query-ada4acec27f1fcbac9dcf87d5a2a95845f759c05772b82a8717a4241801bf785.json b/rust/.sqlx/query-ada4acec27f1fcbac9dcf87d5a2a95845f759c05772b82a8717a4241801bf785.json new file mode 100644 index 00000000..541f34cb --- /dev/null +++ b/rust/.sqlx/query-ada4acec27f1fcbac9dcf87d5a2a95845f759c05772b82a8717a4241801bf785.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) FROM notification_outbox WHERE event_id = ?", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "ada4acec27f1fcbac9dcf87d5a2a95845f759c05772b82a8717a4241801bf785" +} diff --git a/rust/.sqlx/query-ae43489000b353b7d3252a7626d75239dca0a96c2645f885df5bd2738d8035b7.json b/rust/.sqlx/query-ae43489000b353b7d3252a7626d75239dca0a96c2645f885df5bd2738d8035b7.json new file mode 100644 index 00000000..d84f5076 --- /dev/null +++ b/rust/.sqlx/query-ae43489000b353b7d3252a7626d75239dca0a96c2645f885df5bd2738d8035b7.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT download_state FROM media_files WHERE media_id = ?", + "describe": { + "columns": [ + { + "name": "download_state", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "download_state" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "ae43489000b353b7d3252a7626d75239dca0a96c2645f885df5bd2738d8035b7" +} diff --git a/rust/.sqlx/query-aeed49e886c108b34f98af86d5a9b00536458ebf6f45ab47a265fc0faff78a00.json b/rust/.sqlx/query-aeed49e886c108b34f98af86d5a9b00536458ebf6f45ab47a265fc0faff78a00.json new file mode 100644 index 00000000..a2dd20e6 --- /dev/null +++ b/rust/.sqlx/query-aeed49e886c108b34f98af86d5a9b00536458ebf6f45ab47a265fc0faff78a00.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT additional_message_data FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "additional_message_data", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "messages", + "name": "additional_message_data" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "aeed49e886c108b34f98af86d5a9b00536458ebf6f45ab47a265fc0faff78a00" +} diff --git a/rust/.sqlx/query-b28e87ff6d1286b511ec7923b3dd8343423b6f4c01a6825e4a1b52b39047b1c7.json b/rust/.sqlx/query-b28e87ff6d1286b511ec7923b3dd8343423b6f4c01a6825e4a1b52b39047b1c7.json new file mode 100644 index 00000000..f8c7f67d --- /dev/null +++ b/rust/.sqlx/query-b28e87ff6d1286b511ec7923b3dd8343423b6f4c01a6825e4a1b52b39047b1c7.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) FROM contacts WHERE accepted = 1", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "b28e87ff6d1286b511ec7923b3dd8343423b6f4c01a6825e4a1b52b39047b1c7" +} diff --git a/rust/.sqlx/query-b375c5808bf4526a65d96c6262cf03bdb10c4c2dd00a4a57ab49c27ff7fa8d05.json b/rust/.sqlx/query-b375c5808bf4526a65d96c6262cf03bdb10c4c2dd00a4a57ab49c27ff7fa8d05.json deleted file mode 100644 index 97f5add9..00000000 --- a/rust/.sqlx/query-b375c5808bf4526a65d96c6262cf03bdb10c4c2dd00a4a57ab49c27ff7fa8d05.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO contact_push_keys(contact_id, key_id, key, created_at)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(contact_id, key_id)\n DO UPDATE SET key = excluded.key, created_at = excluded.created_at\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "b375c5808bf4526a65d96c6262cf03bdb10c4c2dd00a4a57ab49c27ff7fa8d05" -} diff --git a/rust/.sqlx/query-b5be3159caa3c6bd1d6408e20faac6b1a334bf987f57b1bddf355498ccd0a1e8.json b/rust/.sqlx/query-b5be3159caa3c6bd1d6408e20faac6b1a334bf987f57b1bddf355498ccd0a1e8.json new file mode 100644 index 00000000..67578244 --- /dev/null +++ b/rust/.sqlx/query-b5be3159caa3c6bd1d6408e20faac6b1a334bf987f57b1bddf355498ccd0a1e8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET recovery_secret_share = ? WHERE user_id = 8", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "b5be3159caa3c6bd1d6408e20faac6b1a334bf987f57b1bddf355498ccd0a1e8" +} diff --git a/rust/.sqlx/query-b84d4a6bba4e8eb084bbb6b6ffdff05df10bc93c4ff9bfba248f376439e268a1.json b/rust/.sqlx/query-b84d4a6bba4e8eb084bbb6b6ffdff05df10bc93c4ff9bfba248f376439e268a1.json new file mode 100644 index 00000000..1df89b90 --- /dev/null +++ b/rust/.sqlx/query-b84d4a6bba4e8eb084bbb6b6ffdff05df10bc93c4ff9bfba248f376439e268a1.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT media_id FROM media_files WHERE download_state = 'pending'", + "describe": { + "columns": [ + { + "name": "media_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "media_files", + "name": "media_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "b84d4a6bba4e8eb084bbb6b6ffdff05df10bc93c4ff9bfba248f376439e268a1" +} diff --git a/rust/.sqlx/query-b88ca6e0e8412660ff6f7be96cf25bfa2f77cfca7578ddcc6a7d7a2d16a88402.json b/rust/.sqlx/query-b88ca6e0e8412660ff6f7be96cf25bfa2f77cfca7578ddcc6a7d7a2d16a88402.json new file mode 100644 index 00000000..cac2585a --- /dev/null +++ b/rust/.sqlx/query-b88ca6e0e8412660ff6f7be96cf25bfa2f77cfca7578ddcc6a7d7a2d16a88402.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM key_verifications WHERE contact_id = ? AND type = 'contactSharedByVerified' AND verified_by = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM key_verifications WHERE contact_id = ? AND type = 'contactSharedByVerified' AND verified_by = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "b88ca6e0e8412660ff6f7be96cf25bfa2f77cfca7578ddcc6a7d7a2d16a88402" +} diff --git a/rust/.sqlx/query-b9ac2c9ad836219b0851fa2acb5cf28bbec02249744c104086ce643b4e5fc6b4.json b/rust/.sqlx/query-b9ac2c9ad836219b0851fa2acb5cf28bbec02249744c104086ce643b4e5fc6b4.json new file mode 100644 index 00000000..3414bed6 --- /dev/null +++ b/rust/.sqlx/query-b9ac2c9ad836219b0851fa2acb5cf28bbec02249744c104086ce643b4e5fc6b4.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT accepted, blocked, media_send_counter, user_discovery_excluded,\n user_discovery_manual_approved\n FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "accepted", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "accepted" + } + } + }, + { + "name": "blocked", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "blocked" + } + } + }, + { + "name": "media_send_counter", + "ordinal": 2, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "media_send_counter" + } + } + }, + { + "name": "user_discovery_excluded", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_discovery_excluded" + } + } + }, + { + "name": "user_discovery_manual_approved", + "ordinal": 4, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_discovery_manual_approved" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + true + ] + }, + "hash": "b9ac2c9ad836219b0851fa2acb5cf28bbec02249744c104086ce643b4e5fc6b4" +} diff --git a/rust/.sqlx/query-b9b7b629a1b669c5bcabf97209e8eb6239fc973bad00068cf15022f1adc28c19.json b/rust/.sqlx/query-b9b7b629a1b669c5bcabf97209e8eb6239fc973bad00068cf15022f1adc28c19.json new file mode 100644 index 00000000..8b4fbefd --- /dev/null +++ b/rust/.sqlx/query-b9b7b629a1b669c5bcabf97209e8eb6239fc973bad00068cf15022f1adc28c19.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE groups\n SET also_best_friend = ?,\n flame_counter = ?,\n max_flame_counter = ?\n WHERE group_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "b9b7b629a1b669c5bcabf97209e8eb6239fc973bad00068cf15022f1adc28c19" +} diff --git a/rust/.sqlx/query-b9d176b15d5c2a7b8c03eb6975b7faf251ff3f318e0f7cb138131eaa705d267b.json b/rust/.sqlx/query-b9d176b15d5c2a7b8c03eb6975b7faf251ff3f318e0f7cb138131eaa705d267b.json new file mode 100644 index 00000000..1e43d5e3 --- /dev/null +++ b/rust/.sqlx/query-b9d176b15d5c2a7b8c03eb6975b7faf251ff3f318e0f7cb138131eaa705d267b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM signal_sessions WHERE name = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "b9d176b15d5c2a7b8c03eb6975b7faf251ff3f318e0f7cb138131eaa705d267b" +} diff --git a/rust/.sqlx/query-bc0a466a7b9cbdf21d3434b9fb6320ce1184e38b26d8074dbef3173173fee740.json b/rust/.sqlx/query-bc0a466a7b9cbdf21d3434b9fb6320ce1184e38b26d8074dbef3173173fee740.json new file mode 100644 index 00000000..fcc00887 --- /dev/null +++ b/rust/.sqlx/query-bc0a466a7b9cbdf21d3434b9fb6320ce1184e38b26d8074dbef3173173fee740.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO groups(group_id, group_name, is_direct_chat, is_group_admin, joined_group)\n VALUES (?, ?, 1, 1, 1)\n ON CONFLICT(group_id) DO UPDATE SET joined_group = 1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "bc0a466a7b9cbdf21d3434b9fb6320ce1184e38b26d8074dbef3173173fee740" +} diff --git a/rust/.sqlx/query-bc4170238e7463dbfd973883ba4f3b3a5f1fc6954c1d26adb1e48ca80de2c7f8.json b/rust/.sqlx/query-bc4170238e7463dbfd973883ba4f3b3a5f1fc6954c1d26adb1e48ca80de2c7f8.json new file mode 100644 index 00000000..8fe39d59 --- /dev/null +++ b/rust/.sqlx/query-bc4170238e7463dbfd973883ba4f3b3a5f1fc6954c1d26adb1e48ca80de2c7f8.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "SELECT sender_id, content, quotes_message_id, is_deleted_from_sender FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "sender_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + }, + { + "name": "content", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "content" + } + } + }, + { + "name": "quotes_message_id", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "quotes_message_id" + } + } + }, + { + "name": "is_deleted_from_sender", + "ordinal": 3, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "is_deleted_from_sender" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + true, + false + ] + }, + "hash": "bc4170238e7463dbfd973883ba4f3b3a5f1fc6954c1d26adb1e48ca80de2c7f8" +} diff --git a/rust/.sqlx/query-0c7011264675a005d09dbe0f690e9475f8b8964a8cbce2b0dd4919bd5953bc08.json b/rust/.sqlx/query-bc787f9691dde78e262af844b775841212afe9f4de3d3f0709ba1c1f76726ae8.json similarity index 55% rename from rust/.sqlx/query-0c7011264675a005d09dbe0f690e9475f8b8964a8cbce2b0dd4919bd5953bc08.json rename to rust/.sqlx/query-bc787f9691dde78e262af844b775841212afe9f4de3d3f0709ba1c1f76726ae8.json index b20434ce..32f1aa46 100644 --- a/rust/.sqlx/query-0c7011264675a005d09dbe0f690e9475f8b8964a8cbce2b0dd4919bd5953bc08.json +++ b/rust/.sqlx/query-bc787f9691dde78e262af844b775841212afe9f4de3d3f0709ba1c1f76726ae8.json @@ -1,16 +1,16 @@ { "db_name": "SQLite", - "query": "\n SELECT content\n FROM messages\n WHERE message_id = ?\n ", + "query": "SELECT media_id FROM messages WHERE message_id = ?", "describe": { "columns": [ { - "name": "content", + "name": "media_id", "ordinal": 0, "type_info": "Text", "origin": { "Table": { "table": "messages", - "name": "content" + "name": "media_id" } } } @@ -22,5 +22,5 @@ true ] }, - "hash": "0c7011264675a005d09dbe0f690e9475f8b8964a8cbce2b0dd4919bd5953bc08" + "hash": "bc787f9691dde78e262af844b775841212afe9f4de3d3f0709ba1c1f76726ae8" } diff --git a/rust/.sqlx/query-bc9cdc4b65da8ee1c8c40a72dfd44f5b5c8016a582c36571ce656764b8f2b5fd.json b/rust/.sqlx/query-bc9cdc4b65da8ee1c8c40a72dfd44f5b5c8016a582c36571ce656764b8f2b5fd.json new file mode 100644 index 00000000..9f15095d --- /dev/null +++ b/rust/.sqlx/query-bc9cdc4b65da8ee1c8c40a72dfd44f5b5c8016a582c36571ce656764b8f2b5fd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "bc9cdc4b65da8ee1c8c40a72dfd44f5b5c8016a582c36571ce656764b8f2b5fd" +} diff --git a/rust/.sqlx/query-08ec1e1198b16f3bb313b4c2c5a92bd8ca3251f0aac8d8e2748a7617c53afc65.json b/rust/.sqlx/query-bd2ac211ab56544bc1eeb1a3d47411b577e5d7a4ebb213743bb174ebdacdb30c.json similarity index 51% rename from rust/.sqlx/query-08ec1e1198b16f3bb313b4c2c5a92bd8ca3251f0aac8d8e2748a7617c53afc65.json rename to rust/.sqlx/query-bd2ac211ab56544bc1eeb1a3d47411b577e5d7a4ebb213743bb174ebdacdb30c.json index d9052206..e1919040 100644 --- a/rust/.sqlx/query-08ec1e1198b16f3bb313b4c2c5a92bd8ca3251f0aac8d8e2748a7617c53afc65.json +++ b/rust/.sqlx/query-bd2ac211ab56544bc1eeb1a3d47411b577e5d7a4ebb213743bb174ebdacdb30c.json @@ -1,16 +1,16 @@ { "db_name": "SQLite", - "query": "\n SELECT identity_key\n FROM signal_identities\n WHERE name = ?\n ", + "query": "SELECT opened_at FROM messages WHERE message_id = ?", "describe": { "columns": [ { - "name": "identity_key", + "name": "opened_at", "ordinal": 0, - "type_info": "Blob", + "type_info": "Integer", "origin": { "Table": { - "table": "signal_identities", - "name": "identity_key" + "table": "messages", + "name": "opened_at" } } } @@ -19,8 +19,8 @@ "Right": 1 }, "nullable": [ - false + true ] }, - "hash": "08ec1e1198b16f3bb313b4c2c5a92bd8ca3251f0aac8d8e2748a7617c53afc65" + "hash": "bd2ac211ab56544bc1eeb1a3d47411b577e5d7a4ebb213743bb174ebdacdb30c" } diff --git a/rust/.sqlx/query-bdf69a9fec2ffc402c655567c8b29eb77ab35a1dcaf3c1f129b83c331da33329.json b/rust/.sqlx/query-bdf69a9fec2ffc402c655567c8b29eb77ab35a1dcaf3c1f129b83c331da33329.json new file mode 100644 index 00000000..dd59eec8 --- /dev/null +++ b/rust/.sqlx/query-bdf69a9fec2ffc402c655567c8b29eb77ab35a1dcaf3c1f129b83c331da33329.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT receipt_id FROM receipts\n WHERE will_be_retried_by_media_upload = 0\n AND (ack_by_server_at IS NULL OR mark_for_retry IS NOT NULL)\n AND (mark_for_retry_after_accepted IS NULL OR EXISTS(\n SELECT 1 FROM contacts\n WHERE contacts.user_id = receipts.contact_id AND contacts.accepted = 1\n ))\n ORDER BY created_at\n ", + "describe": { + "columns": [ + { + "name": "receipt_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "receipts", + "name": "receipt_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "bdf69a9fec2ffc402c655567c8b29eb77ab35a1dcaf3c1f129b83c331da33329" +} diff --git a/rust/.sqlx/query-be0c18b2b1c60c720364458799336262f49d3cf2473f7d7305c81047d0cc7aa5.json b/rust/.sqlx/query-be0c18b2b1c60c720364458799336262f49d3cf2473f7d7305c81047d0cc7aa5.json new file mode 100644 index 00000000..07f1a1bb --- /dev/null +++ b/rust/.sqlx/query-be0c18b2b1c60c720364458799336262f49d3cf2473f7d7305c81047d0cc7aa5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE contacts SET\n username = COALESCE(?, username),\n display_name = CASE WHEN ? THEN ? ELSE display_name END,\n avatar_svg_compressed = CASE WHEN ? THEN ? ELSE avatar_svg_compressed END,\n sender_profile_counter = COALESCE(?, sender_profile_counter),\n signal_version = COALESCE(?, signal_version),\n accepted = COALESCE(?, accepted),\n requested = COALESCE(?, requested),\n deleted_by_user = COALESCE(?, deleted_by_user),\n blocked = COALESCE(?, blocked)\n WHERE user_id = ? AND (? = 0 OR requested = 0)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "be0c18b2b1c60c720364458799336262f49d3cf2473f7d7305c81047d0cc7aa5" +} diff --git a/rust/.sqlx/query-be136b085a392787e934ac0026f3621d0f6208e4d786e6ac9d1dc16b5dbdd238.json b/rust/.sqlx/query-be136b085a392787e934ac0026f3621d0f6208e4d786e6ac9d1dc16b5dbdd238.json new file mode 100644 index 00000000..b4569d3b --- /dev/null +++ b/rust/.sqlx/query-be136b085a392787e934ac0026f3621d0f6208e4d786e6ac9d1dc16b5dbdd238.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT user_id FROM contacts WHERE username IN ('[deleted]', '[Unknown]')", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "be136b085a392787e934ac0026f3621d0f6208e4d786e6ac9d1dc16b5dbdd238" +} diff --git a/rust/.sqlx/query-beef80b471a50234ac99decd9bef164fceb680c1b53760fc5b0e83a8216be53d.json b/rust/.sqlx/query-beef80b471a50234ac99decd9bef164fceb680c1b53760fc5b0e83a8216be53d.json new file mode 100644 index 00000000..e52f0860 --- /dev/null +++ b/rust/.sqlx/query-beef80b471a50234ac99decd9bef164fceb680c1b53760fc5b0e83a8216be53d.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM user_discovery_announced_users WHERE announced_user_id = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM user_discovery_announced_users WHERE announced_user_id = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "beef80b471a50234ac99decd9bef164fceb680c1b53760fc5b0e83a8216be53d" +} diff --git a/rust/.sqlx/query-fdc73a53a58eea5df4925e2a11ce1befa52f1dc0f709de124d9c6f5ec29c88f0.json b/rust/.sqlx/query-bf32b99b2e2332ce9903bd6c41540b365a60565bcdd6e75a82fbdf64ad031493.json similarity index 53% rename from rust/.sqlx/query-fdc73a53a58eea5df4925e2a11ce1befa52f1dc0f709de124d9c6f5ec29c88f0.json rename to rust/.sqlx/query-bf32b99b2e2332ce9903bd6c41540b365a60565bcdd6e75a82fbdf64ad031493.json index 3b2afdea..7e9c2cfe 100644 --- a/rust/.sqlx/query-fdc73a53a58eea5df4925e2a11ce1befa52f1dc0f709de124d9c6f5ec29c88f0.json +++ b/rust/.sqlx/query-bf32b99b2e2332ce9903bd6c41540b365a60565bcdd6e75a82fbdf64ad031493.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n INSERT INTO received_receipts(receipt_id)\n VALUES (?)\n ", + "query": "\n INSERT INTO received_receipts(receipt_id)\n VALUES (?)\n ON CONFLICT(receipt_id) DO NOTHING\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "fdc73a53a58eea5df4925e2a11ce1befa52f1dc0f709de124d9c6f5ec29c88f0" + "hash": "bf32b99b2e2332ce9903bd6c41540b365a60565bcdd6e75a82fbdf64ad031493" } diff --git a/rust/.sqlx/query-c1993cbf0d0255b2e68d0bf4ffc80d37fdec7b2d995b2553e534e9dc6e6db195.json b/rust/.sqlx/query-c1993cbf0d0255b2e68d0bf4ffc80d37fdec7b2d995b2553e534e9dc6e6db195.json deleted file mode 100644 index 2b400771..00000000 --- a/rust/.sqlx/query-c1993cbf0d0255b2e68d0bf4ffc80d37fdec7b2d995b2553e534e9dc6e6db195.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n (SELECT COUNT(*) FROM messages WHERE message_id LIKE 'path-media-%') AS media_count,\n (SELECT COUNT(*) FROM group_histories WHERE group_id = ?) AS history_count,\n (SELECT COUNT(*) FROM contact_push_keys WHERE contact_id = ?) AS push_key_count,\n (SELECT ask_for_friend_promotions FROM contacts WHERE user_id = ?) AS ask_promotions\n ", - "describe": { - "columns": [ - { - "name": "media_count", - "ordinal": 0, - "type_info": "Integer", - "origin": "Expression" - }, - { - "name": "history_count", - "ordinal": 1, - "type_info": "Integer", - "origin": "Expression" - }, - { - "name": "push_key_count", - "ordinal": 2, - "type_info": "Integer", - "origin": "Expression" - }, - { - "name": "ask_promotions", - "ordinal": 3, - "type_info": "Integer", - "origin": { - "Table": { - "table": "contacts", - "name": "ask_for_friend_promotions" - } - } - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - false, - true - ] - }, - "hash": "c1993cbf0d0255b2e68d0bf4ffc80d37fdec7b2d995b2553e534e9dc6e6db195" -} diff --git a/rust/.sqlx/query-c471755c5d39658b62ead063f040573ab36022c3f7c3cc9d039caafc45f49616.json b/rust/.sqlx/query-c471755c5d39658b62ead063f040573ab36022c3f7c3cc9d039caafc45f49616.json new file mode 100644 index 00000000..ebecc648 --- /dev/null +++ b/rust/.sqlx/query-c471755c5d39658b62ead063f040573ab36022c3f7c3cc9d039caafc45f49616.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT is_deleted_from_sender FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "is_deleted_from_sender", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "is_deleted_from_sender" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "c471755c5d39658b62ead063f040573ab36022c3f7c3cc9d039caafc45f49616" +} diff --git a/rust/.sqlx/query-c4845dbfc70e136ed2ccbb9efa78a9bdba7773c4f5ab2276da9b9ac1e74693dd.json b/rust/.sqlx/query-c4845dbfc70e136ed2ccbb9efa78a9bdba7773c4f5ab2276da9b9ac1e74693dd.json new file mode 100644 index 00000000..832bb6de --- /dev/null +++ b/rust/.sqlx/query-c4845dbfc70e136ed2ccbb9efa78a9bdba7773c4f5ab2276da9b9ac1e74693dd.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(DISTINCT from_contact_id) FROM user_discovery_user_relations WHERE announced_user_id = ?", + "describe": { + "columns": [ + { + "name": "COUNT(DISTINCT from_contact_id)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "c4845dbfc70e136ed2ccbb9efa78a9bdba7773c4f5ab2276da9b9ac1e74693dd" +} diff --git a/rust/.sqlx/query-c56b5839278ea567fdde83485ddb42baa0780992501f9e836794a21d2e2e3b58.json b/rust/.sqlx/query-c56b5839278ea567fdde83485ddb42baa0780992501f9e836794a21d2e2e3b58.json new file mode 100644 index 00000000..fd719202 --- /dev/null +++ b/rust/.sqlx/query-c56b5839278ea567fdde83485ddb42baa0780992501f9e836794a21d2e2e3b58.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM messages WHERE message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "c56b5839278ea567fdde83485ddb42baa0780992501f9e836794a21d2e2e3b58" +} diff --git a/rust/.sqlx/query-c57017d2c9c2b4b2282f89010ddd4250b566cda50870d09ecedc7e8eb81c5c26.json b/rust/.sqlx/query-c57017d2c9c2b4b2282f89010ddd4250b566cda50870d09ecedc7e8eb81c5c26.json deleted file mode 100644 index 77edd100..00000000 --- a/rust/.sqlx/query-c57017d2c9c2b4b2282f89010ddd4250b566cda50870d09ecedc7e8eb81c5c26.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT SUM(retry_count)\n FROM receipts\n WHERE receipt_id LIKE 'path-plaintext-%'\n ", - "describe": { - "columns": [ - { - "name": "SUM(retry_count)", - "ordinal": 0, - "type_info": "Integer", - "origin": "Expression" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - true - ] - }, - "hash": "c57017d2c9c2b4b2282f89010ddd4250b566cda50870d09ecedc7e8eb81c5c26" -} diff --git a/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json b/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json new file mode 100644 index 00000000..4f2971be --- /dev/null +++ b/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json @@ -0,0 +1,326 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_id" + } + } + }, + { + "name": "username", + "ordinal": 1, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "username" + } + } + }, + { + "name": "display_name", + "ordinal": 2, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "display_name" + } + } + }, + { + "name": "nick_name", + "ordinal": 3, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "nick_name" + } + } + }, + { + "name": "avatar_svg_compressed", + "ordinal": 4, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "avatar_svg_compressed" + } + } + }, + { + "name": "sender_profile_counter", + "ordinal": 5, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "sender_profile_counter" + } + } + }, + { + "name": "accepted", + "ordinal": 6, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "accepted" + } + } + }, + { + "name": "deleted_by_user", + "ordinal": 7, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "deleted_by_user" + } + } + }, + { + "name": "requested", + "ordinal": 8, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "requested" + } + } + }, + { + "name": "blocked", + "ordinal": 9, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "blocked" + } + } + }, + { + "name": "verified", + "ordinal": 10, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "verified" + } + } + }, + { + "name": "account_deleted", + "ordinal": 11, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "account_deleted" + } + } + }, + { + "name": "created_at", + "ordinal": 12, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "created_at" + } + } + }, + { + "name": "signal_version", + "ordinal": 13, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "signal_version" + } + } + }, + { + "name": "user_discovery_version", + "ordinal": 14, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "user_discovery_version" + } + } + }, + { + "name": "user_discovery_excluded", + "ordinal": 15, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_discovery_excluded" + } + } + }, + { + "name": "user_discovery_manual_approved", + "ordinal": 16, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "user_discovery_manual_approved" + } + } + }, + { + "name": "recovery_is_trusted_friend", + "ordinal": 17, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_is_trusted_friend" + } + } + }, + { + "name": "recovery_last_heartbeat", + "ordinal": 18, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_last_heartbeat" + } + } + }, + { + "name": "recovery_secret_share", + "ordinal": 19, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_secret_share" + } + } + }, + { + "name": "recovery_contacts_secret_share", + "ordinal": 20, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_secret_share" + } + } + }, + { + "name": "recovery_contacts_last_heartbeat", + "ordinal": 21, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_last_heartbeat" + } + } + }, + { + "name": "recovery_contacts_threshold", + "ordinal": 22, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_threshold" + } + } + }, + { + "name": "ask_for_friend_promotions", + "ordinal": 23, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "ask_for_friend_promotions" + } + } + }, + { + "name": "media_send_counter", + "ordinal": 24, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "media_send_counter" + } + } + }, + { + "name": "media_received_counter", + "ordinal": 25, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "media_received_counter" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false + ] + }, + "hash": "c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f" +} diff --git a/rust/.sqlx/query-c6818f3b395db59a7fbe80435c4ef89416ff0cfb32139abff226eec319053a68.json b/rust/.sqlx/query-c6818f3b395db59a7fbe80435c4ef89416ff0cfb32139abff226eec319053a68.json new file mode 100644 index 00000000..7f8c2fe2 --- /dev/null +++ b/rust/.sqlx/query-c6818f3b395db59a7fbe80435c4ef89416ff0cfb32139abff226eec319053a68.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO messages(group_id, message_id, type, content, quotes_message_id, created_at)\n VALUES (?, ?, 'text', ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "c6818f3b395db59a7fbe80435c4ef89416ff0cfb32139abff226eec319053a68" +} diff --git a/rust/.sqlx/query-c6878c3532b6555c1e1a51d6602ba74141445777e0b9cbd882a875d5c3481fca.json b/rust/.sqlx/query-c6878c3532b6555c1e1a51d6602ba74141445777e0b9cbd882a875d5c3481fca.json new file mode 100644 index 00000000..c5462cf0 --- /dev/null +++ b/rust/.sqlx/query-c6878c3532b6555c1e1a51d6602ba74141445777e0b9cbd882a875d5c3481fca.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE user_discovery_own_promotions SET promotion = X'' WHERE contact_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "c6878c3532b6555c1e1a51d6602ba74141445777e0b9cbd882a875d5c3481fca" +} diff --git a/rust/.sqlx/query-c7ad38ad8d6d726a67bf800b075b0496711be766ef54633d0d5a2dff6b5a57cc.json b/rust/.sqlx/query-c7ad38ad8d6d726a67bf800b075b0496711be766ef54633d0d5a2dff6b5a57cc.json new file mode 100644 index 00000000..5fa7bd7e --- /dev/null +++ b/rust/.sqlx/query-c7ad38ad8d6d726a67bf800b075b0496711be766ef54633d0d5a2dff6b5a57cc.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1\n AND blocked = 0 AND media_send_counter >= ? AND user_discovery_excluded = 0\n AND (? = 0 OR user_discovery_manual_approved = 1))", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1\n AND blocked = 0 AND media_send_counter >= ? AND user_discovery_excluded = 0\n AND (? = 0 OR user_discovery_manual_approved = 1))", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "c7ad38ad8d6d726a67bf800b075b0496711be766ef54633d0d5a2dff6b5a57cc" +} diff --git a/rust/.sqlx/query-c92d114f6a87655124f8bbdf8df3564966e6ab55725b417f44d353279627dac9.json b/rust/.sqlx/query-c92d114f6a87655124f8bbdf8df3564966e6ab55725b417f44d353279627dac9.json new file mode 100644 index 00000000..cb2fbaa4 --- /dev/null +++ b/rust/.sqlx/query-c92d114f6a87655124f8bbdf8df3564966e6ab55725b417f44d353279627dac9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE user_discovery_announced_users SET username = ? WHERE announced_user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "c92d114f6a87655124f8bbdf8df3564966e6ab55725b417f44d353279627dac9" +} diff --git a/rust/.sqlx/query-cd9f690447bdbe9dc52534194400781076ed1d3ca59dd52030c892dc4dc15224.json b/rust/.sqlx/query-cd9f690447bdbe9dc52534194400781076ed1d3ca59dd52030c892dc4dc15224.json deleted file mode 100644 index 4b3f0fe0..00000000 --- a/rust/.sqlx/query-cd9f690447bdbe9dc52534194400781076ed1d3ca59dd52030c892dc4dc15224.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE contacts\n SET requested = 0, accepted = 0, deleted_by_user = 1\n WHERE user_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "cd9f690447bdbe9dc52534194400781076ed1d3ca59dd52030c892dc4dc15224" -} diff --git a/rust/.sqlx/query-ced6f5964217225fb749bde7641da1f7bf365a53a4d06aa2696a1d24c2b398f7.json b/rust/.sqlx/query-ced6f5964217225fb749bde7641da1f7bf365a53a4d06aa2696a1d24c2b398f7.json deleted file mode 100644 index b704397d..00000000 --- a/rust/.sqlx/query-ced6f5964217225fb749bde7641da1f7bf365a53a4d06aa2696a1d24c2b398f7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO user_discovery_other_promotions (\n from_contact_id,\n promotion_id,\n public_id,\n threshold,\n announcement_share,\n public_key_verified_timestamp\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(from_contact_id, public_id) DO UPDATE SET\n promotion_id = excluded.promotion_id,\n threshold = excluded.threshold,\n announcement_share = excluded.announcement_share,\n public_key_verified_timestamp = excluded.public_key_verified_timestamp\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "ced6f5964217225fb749bde7641da1f7bf365a53a4d06aa2696a1d24c2b398f7" -} diff --git a/rust/.sqlx/query-cf821c9b1530db447a1ba030c3be50c5eeeb78cb28fe74481d6493a6a0af4161.json b/rust/.sqlx/query-cf821c9b1530db447a1ba030c3be50c5eeeb78cb28fe74481d6493a6a0af4161.json new file mode 100644 index 00000000..3251c7d7 --- /dev/null +++ b/rust/.sqlx/query-cf821c9b1530db447a1ba030c3be50c5eeeb78cb28fe74481d6493a6a0af4161.json @@ -0,0 +1,28 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) AS \"count!: i64\", MAX(last_retry) AS last_retry\n FROM receipts WHERE contact_id = ? AND message_id = ?", + "describe": { + "columns": [ + { + "name": "count!: i64", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + }, + { + "name": "last_retry", + "ordinal": 1, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + true + ] + }, + "hash": "cf821c9b1530db447a1ba030c3be50c5eeeb78cb28fe74481d6493a6a0af4161" +} diff --git a/rust/.sqlx/query-d1b24b806f4071108ba198504845cfeeeddc63628ed9ace6db3107ab2a4262ec.json b/rust/.sqlx/query-d1b24b806f4071108ba198504845cfeeeddc63628ed9ace6db3107ab2a4262ec.json deleted file mode 100644 index 2d8555db..00000000 --- a/rust/.sqlx/query-d1b24b806f4071108ba198504845cfeeeddc63628ed9ace6db3107ab2a4262ec.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE messages\n SET content = ?, modified_at = ?\n WHERE message_id = ? AND sender_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "d1b24b806f4071108ba198504845cfeeeddc63628ed9ace6db3107ab2a4262ec" -} diff --git a/rust/.sqlx/query-d2688abdeef4590a1c762a1683a638a25806d9be376027c4ac86955a42135ef2.json b/rust/.sqlx/query-d2688abdeef4590a1c762a1683a638a25806d9be376027c4ac86955a42135ef2.json new file mode 100644 index 00000000..0a1da0c1 --- /dev/null +++ b/rust/.sqlx/query-d2688abdeef4590a1c762a1683a638a25806d9be376027c4ac86955a42135ef2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE receipts\n SET receipt_id = ?,\n mark_for_retry = CAST(strftime('%s', 'now') AS INTEGER),\n retry_count = retry_count + 1,\n ack_by_server_at = NULL\n WHERE receipt_id = ? AND contact_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "d2688abdeef4590a1c762a1683a638a25806d9be376027c4ac86955a42135ef2" +} diff --git a/rust/.sqlx/query-d3598ec91afa76d18c5da4408bf49a16ab2caafd89bd68c58e04417921d039a6.json b/rust/.sqlx/query-d3598ec91afa76d18c5da4408bf49a16ab2caafd89bd68c58e04417921d039a6.json new file mode 100644 index 00000000..9a1ca20f --- /dev/null +++ b/rust/.sqlx/query-d3598ec91afa76d18c5da4408bf49a16ab2caafd89bd68c58e04417921d039a6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE user_discovery_shares SET contact_id = ? WHERE share_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "d3598ec91afa76d18c5da4408bf49a16ab2caafd89bd68c58e04417921d039a6" +} diff --git a/rust/.sqlx/query-d495889cd14181159e65bedf607ec7488e8c04946936dee8eb7175a10b69e77a.json b/rust/.sqlx/query-d495889cd14181159e65bedf607ec7488e8c04946936dee8eb7175a10b69e77a.json new file mode 100644 index 00000000..c5c50825 --- /dev/null +++ b/rust/.sqlx/query-d495889cd14181159e65bedf607ec7488e8c04946936dee8eb7175a10b69e77a.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT blocked FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "blocked", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "blocked" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "d495889cd14181159e65bedf607ec7488e8c04946936dee8eb7175a10b69e77a" +} diff --git a/rust/.sqlx/query-d7d94080dcd1ae95ed70072c040435db4f1f922e46041d28836e068981df3df6.json b/rust/.sqlx/query-d7d94080dcd1ae95ed70072c040435db4f1f922e46041d28836e068981df3df6.json new file mode 100644 index 00000000..026301ba --- /dev/null +++ b/rust/.sqlx/query-d7d94080dcd1ae95ed70072c040435db4f1f922e46041d28836e068981df3df6.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT username, deleted_by_user FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "contacts", + "name": "username" + } + } + }, + { + "name": "deleted_by_user", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "deleted_by_user" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "d7d94080dcd1ae95ed70072c040435db4f1f922e46041d28836e068981df3df6" +} diff --git a/rust/.sqlx/query-d8a516dd12b7f5d49807b1b5c6970f0e004f9c39a23577473d348ebb6e2b044a.json b/rust/.sqlx/query-d8a516dd12b7f5d49807b1b5c6970f0e004f9c39a23577473d348ebb6e2b044a.json deleted file mode 100644 index 87a45a41..00000000 --- a/rust/.sqlx/query-d8a516dd12b7f5d49807b1b5c6970f0e004f9c39a23577473d348ebb6e2b044a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO user_discovery_announced_users (\n announced_user_id,\n announced_public_key,\n public_id\n ) VALUES (?, ?, ?)\n ON CONFLICT DO UPDATE SET\n announced_user_id = excluded.announced_user_id,\n announced_public_key = excluded.announced_public_key,\n public_id = excluded.public_id\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "d8a516dd12b7f5d49807b1b5c6970f0e004f9c39a23577473d348ebb6e2b044a" -} diff --git a/rust/.sqlx/query-d8a8544c1d3eafe0b9c6f0ebd1b7102e792e3d27722294bdc626acde1b46ba2e.json b/rust/.sqlx/query-d8a8544c1d3eafe0b9c6f0ebd1b7102e792e3d27722294bdc626acde1b46ba2e.json deleted file mode 100644 index 3c1e66fd..00000000 --- a/rust/.sqlx/query-d8a8544c1d3eafe0b9c6f0ebd1b7102e792e3d27722294bdc626acde1b46ba2e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO contacts(user_id, username, accepted, signal_version)\n VALUES (?, ?, 1, 'v2')\n ON CONFLICT(user_id) DO UPDATE SET\n username = excluded.username,\n accepted = 1,\n signal_version = 'v2'\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "d8a8544c1d3eafe0b9c6f0ebd1b7102e792e3d27722294bdc626acde1b46ba2e" -} diff --git a/rust/.sqlx/query-da86a64379fbe00301ccdd82c721b99f3c78465544975d85e9e753337b2fce3e.json b/rust/.sqlx/query-da86a64379fbe00301ccdd82c721b99f3c78465544975d85e9e753337b2fce3e.json deleted file mode 100644 index 692e7321..00000000 --- a/rust/.sqlx/query-da86a64379fbe00301ccdd82c721b99f3c78465544975d85e9e753337b2fce3e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO signal_identities (name, identity_key, timestamp)\n VALUES ('1', ?, 0)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "da86a64379fbe00301ccdd82c721b99f3c78465544975d85e9e753337b2fce3e" -} diff --git a/rust/.sqlx/query-da9609bbcd1676a863f4612f19a24cfdf518d18dca57ba1693b0d51345a24ce1.json b/rust/.sqlx/query-da9609bbcd1676a863f4612f19a24cfdf518d18dca57ba1693b0d51345a24ce1.json deleted file mode 100644 index 09d4437e..00000000 --- a/rust/.sqlx/query-da9609bbcd1676a863f4612f19a24cfdf518d18dca57ba1693b0d51345a24ce1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO receipts(receipt_id, contact_id, message_id, message)\n VALUES (?, ?, 'path-text-message', x'01')\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "da9609bbcd1676a863f4612f19a24cfdf518d18dca57ba1693b0d51345a24ce1" -} diff --git a/rust/.sqlx/query-db998a6faae4968d03efb6496202c07be4b7c353d12372842b6cd5eb80fb7fd3.json b/rust/.sqlx/query-db998a6faae4968d03efb6496202c07be4b7c353d12372842b6cd5eb80fb7fd3.json deleted file mode 100644 index b0a55748..00000000 --- a/rust/.sqlx/query-db998a6faae4968d03efb6496202c07be4b7c353d12372842b6cd5eb80fb7fd3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO contacts(user_id, username, accepted)\n VALUES(7, 'alice', 1)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "db998a6faae4968d03efb6496202c07be4b7c353d12372842b6cd5eb80fb7fd3" -} diff --git a/rust/.sqlx/query-dbc3765b4629863c7de0c8452a0c526f11116b42f5718311d794a105548f2909.json b/rust/.sqlx/query-dbc3765b4629863c7de0c8452a0c526f11116b42f5718311d794a105548f2909.json new file mode 100644 index 00000000..3a6c7c43 --- /dev/null +++ b/rust/.sqlx/query-dbc3765b4629863c7de0c8452a0c526f11116b42f5718311d794a105548f2909.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT avatar_svg_compressed FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "avatar_svg_compressed", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "avatar_svg_compressed" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "dbc3765b4629863c7de0c8452a0c526f11116b42f5718311d794a105548f2909" +} diff --git a/rust/.sqlx/query-dc7fa5581ec738bbfa3b55c5faa28141f6dd07e58b30422ff038d80a10fd08b3.json b/rust/.sqlx/query-dc7fa5581ec738bbfa3b55c5faa28141f6dd07e58b30422ff038d80a10fd08b3.json new file mode 100644 index 00000000..c6ea2f0a --- /dev/null +++ b/rust/.sqlx/query-dc7fa5581ec738bbfa3b55c5faa28141f6dd07e58b30422ff038d80a10fd08b3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET download_state = 'downloading'\n WHERE media_id = ? AND download_state = 'pending'", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "dc7fa5581ec738bbfa3b55c5faa28141f6dd07e58b30422ff038d80a10fd08b3" +} diff --git a/rust/.sqlx/query-dfe45f9a925ea0d08a5195335a8d14d257d083543df4c3c8530304f6ba33b09f.json b/rust/.sqlx/query-dfe45f9a925ea0d08a5195335a8d14d257d083543df4c3c8530304f6ba33b09f.json new file mode 100644 index 00000000..c207d196 --- /dev/null +++ b/rust/.sqlx/query-dfe45f9a925ea0d08a5195335a8d14d257d083543df4c3c8530304f6ba33b09f.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT group_id FROM groups WHERE joined_group = 0 AND left_group = 0", + "describe": { + "columns": [ + { + "name": "group_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "groups", + "name": "group_id" + } + } + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "dfe45f9a925ea0d08a5195335a8d14d257d083543df4c3c8530304f6ba33b09f" +} diff --git a/rust/.sqlx/query-e026e8c5eb30ca2f7c98c0d4e8f9ccdf388bf7e98f4336ada2ac09224461082f.json b/rust/.sqlx/query-e026e8c5eb30ca2f7c98c0d4e8f9ccdf388bf7e98f4336ada2ac09224461082f.json deleted file mode 100644 index e7fdef1c..00000000 --- a/rust/.sqlx/query-e026e8c5eb30ca2f7c98c0d4e8f9ccdf388bf7e98f4336ada2ac09224461082f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO user_discovery_own_promotions (\n contact_id,\n promotion\n ) VALUES (?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "e026e8c5eb30ca2f7c98c0d4e8f9ccdf388bf7e98f4336ada2ac09224461082f" -} diff --git a/rust/.sqlx/query-e0af4bff6847c75b26509f029e6a23624c784b5ededae3dcac2b1913cf8221b0.json b/rust/.sqlx/query-e0af4bff6847c75b26509f029e6a23624c784b5ededae3dcac2b1913cf8221b0.json new file mode 100644 index 00000000..49a18acd --- /dev/null +++ b/rust/.sqlx/query-e0af4bff6847c75b26509f029e6a23624c784b5ededae3dcac2b1913cf8221b0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE messages SET opened_at = ?, opened_by_all = ? WHERE message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e0af4bff6847c75b26509f029e6a23624c784b5ededae3dcac2b1913cf8221b0" +} diff --git a/rust/.sqlx/query-e215e54ae922900c89f72406e96e16243bdea540c05dea78d1a5d7c1ec12f5df.json b/rust/.sqlx/query-e215e54ae922900c89f72406e96e16243bdea540c05dea78d1a5d7c1ec12f5df.json new file mode 100644 index 00000000..6ad1217d --- /dev/null +++ b/rust/.sqlx/query-e215e54ae922900c89f72406e96e16243bdea540c05dea78d1a5d7c1ec12f5df.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM receipts WHERE message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e215e54ae922900c89f72406e96e16243bdea540c05dea78d1a5d7c1ec12f5df" +} diff --git a/rust/.sqlx/query-e24b46f465ca104c6f288eafda55b060d389a1a07f54757b0e12ac4d16cc2ec9.json b/rust/.sqlx/query-e24b46f465ca104c6f288eafda55b060d389a1a07f54757b0e12ac4d16cc2ec9.json new file mode 100644 index 00000000..8c5766b4 --- /dev/null +++ b/rust/.sqlx/query-e24b46f465ca104c6f288eafda55b060d389a1a07f54757b0e12ac4d16cc2ec9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET download_state = 'ready', stored_file_hash = ?\n WHERE media_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "e24b46f465ca104c6f288eafda55b060d389a1a07f54757b0e12ac4d16cc2ec9" +} diff --git a/rust/.sqlx/query-e289178012389d57c467b0bc60f42c5173c81e3afaa26d2f78bc52463963da78.json b/rust/.sqlx/query-e289178012389d57c467b0bc60f42c5173c81e3afaa26d2f78bc52463963da78.json new file mode 100644 index 00000000..73ad047c --- /dev/null +++ b/rust/.sqlx/query-e289178012389d57c467b0bc60f42c5173c81e3afaa26d2f78bc52463963da78.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE receipts\n SET ack_by_server_at = ?, retry_count = 1, last_retry = ?, mark_for_retry = NULL\n WHERE EXISTS(\n SELECT 1\n FROM messages\n JOIN group_members ON group_members.group_id = messages.group_id\n WHERE messages.message_id = receipts.message_id\n AND messages.media_id = ?\n AND group_members.contact_id = receipts.contact_id\n AND (group_members.member_state IS NULL OR group_members.member_state != 'leftGroup')\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e289178012389d57c467b0bc60f42c5173c81e3afaa26d2f78bc52463963da78" +} diff --git a/rust/.sqlx/query-e2a5bdab035e10052f1091991d249407492b87fc3fe4fd56dac858a77a8f6845.json b/rust/.sqlx/query-e2a5bdab035e10052f1091991d249407492b87fc3fe4fd56dac858a77a8f6845.json deleted file mode 100644 index 18a03b56..00000000 --- a/rust/.sqlx/query-e2a5bdab035e10052f1091991d249407492b87fc3fe4fd56dac858a77a8f6845.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO groups(group_id, group_name)\n VALUES('group', 'Group')\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "e2a5bdab035e10052f1091991d249407492b87fc3fe4fd56dac858a77a8f6845" -} diff --git a/rust/.sqlx/query-e3651626e5a5537a9317a56ac877503ea73bc4446d38221e5100a4ec312e205a.json b/rust/.sqlx/query-e3651626e5a5537a9317a56ac877503ea73bc4446d38221e5100a4ec312e205a.json new file mode 100644 index 00000000..71b40f6d --- /dev/null +++ b/rust/.sqlx/query-e3651626e5a5537a9317a56ac877503ea73bc4446d38221e5100a4ec312e205a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE receipts\n SET receipt_id = ?,\n mark_for_retry = ?,\n retry_count = retry_count + 1,\n ack_by_server_at = NULL\n WHERE receipt_id = ? AND contact_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "e3651626e5a5537a9317a56ac877503ea73bc4446d38221e5100a4ec312e205a" +} diff --git a/rust/.sqlx/query-e512dd25f91d718843ac20f97264380991ff99c42097ac021b9db327462d7557.json b/rust/.sqlx/query-e512dd25f91d718843ac20f97264380991ff99c42097ac021b9db327462d7557.json new file mode 100644 index 00000000..f4693cff --- /dev/null +++ b/rust/.sqlx/query-e512dd25f91d718843ac20f97264380991ff99c42097ac021b9db327462d7557.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt)\n VALUES (?, ?, ?, 0)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e512dd25f91d718843ac20f97264380991ff99c42097ac021b9db327462d7557" +} diff --git a/rust/.sqlx/query-e5232f66455c97e49ed464ede0943c5c88bb1b6d346711688ed1e34719307988.json b/rust/.sqlx/query-e5232f66455c97e49ed464ede0943c5c88bb1b6d346711688ed1e34719307988.json new file mode 100644 index 00000000..f40c10fd --- /dev/null +++ b/rust/.sqlx/query-e5232f66455c97e49ed464ede0943c5c88bb1b6d346711688ed1e34719307988.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM signal_sessions WHERE name = ? AND device_id = 1)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM signal_sessions WHERE name = ? AND device_id = 1)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "e5232f66455c97e49ed464ede0943c5c88bb1b6d346711688ed1e34719307988" +} diff --git a/rust/.sqlx/query-e569dd744fb4a5e49cfbe14c61005d14f8c0f82a8640269ec29d5e9edabb106b.json b/rust/.sqlx/query-e569dd744fb4a5e49cfbe14c61005d14f8c0f82a8640269ec29d5e9edabb106b.json new file mode 100644 index 00000000..5d39b48e --- /dev/null +++ b/rust/.sqlx/query-e569dd744fb4a5e49cfbe14c61005d14f8c0f82a8640269ec29d5e9edabb106b.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT message_id, sender_id AS \"sender_id!: i64\" FROM messages\n WHERE media_id = ? AND opened_at IS NULL AND sender_id IS NOT NULL", + "describe": { + "columns": [ + { + "name": "message_id", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "messages", + "name": "message_id" + } + } + }, + { + "name": "sender_id!: i64", + "ordinal": 1, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "sender_id" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true + ] + }, + "hash": "e569dd744fb4a5e49cfbe14c61005d14f8c0f82a8640269ec29d5e9edabb106b" +} diff --git a/rust/.sqlx/query-e89767b813c0619775a9224a1767cfa1dcd44a0bec75f80e4875a097ab5b4491.json b/rust/.sqlx/query-e89767b813c0619775a9224a1767cfa1dcd44a0bec75f80e4875a097ab5b4491.json new file mode 100644 index 00000000..afb19bfc --- /dev/null +++ b/rust/.sqlx/query-e89767b813c0619775a9224a1767cfa1dcd44a0bec75f80e4875a097ab5b4491.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE messages SET ack_by_server = ? WHERE media_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "e89767b813c0619775a9224a1767cfa1dcd44a0bec75f80e4875a097ab5b4491" +} diff --git a/rust/.sqlx/query-eb4724c5a03a40f301e5dd8916020032cf706cf6acf70ae69c9a17b7fbaae673.json b/rust/.sqlx/query-eb4724c5a03a40f301e5dd8916020032cf706cf6acf70ae69c9a17b7fbaae673.json new file mode 100644 index 00000000..55ca98be --- /dev/null +++ b/rust/.sqlx/query-eb4724c5a03a40f301e5dd8916020032cf706cf6acf70ae69c9a17b7fbaae673.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE media_files SET upload_state = 'uploaded' WHERE media_id = ? AND upload_state IS NOT 'uploaded'", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "eb4724c5a03a40f301e5dd8916020032cf706cf6acf70ae69c9a17b7fbaae673" +} diff --git a/rust/.sqlx/query-eda6535eb2d08a691ac7a032635259b64357b4723a461a719ea7302b5257b130.json b/rust/.sqlx/query-eda6535eb2d08a691ac7a032635259b64357b4723a461a719ea7302b5257b130.json new file mode 100644 index 00000000..78a361c4 --- /dev/null +++ b/rust/.sqlx/query-eda6535eb2d08a691ac7a032635259b64357b4723a461a719ea7302b5257b130.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) FROM user_discovery_user_relations WHERE announced_user_id = ?", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "eda6535eb2d08a691ac7a032635259b64357b4723a461a719ea7302b5257b130" +} diff --git a/rust/.sqlx/query-edbc2c8babc15b0c8b73a1d9fd7326cac78c48486de3de8f898d96833828603c.json b/rust/.sqlx/query-edbc2c8babc15b0c8b73a1d9fd7326cac78c48486de3de8f898d96833828603c.json new file mode 100644 index 00000000..4040dd57 --- /dev/null +++ b/rust/.sqlx/query-edbc2c8babc15b0c8b73a1d9fd7326cac78c48486de3de8f898d96833828603c.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT media_reopened FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "media_reopened", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "media_reopened" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "edbc2c8babc15b0c8b73a1d9fd7326cac78c48486de3de8f898d96833828603c" +} diff --git a/rust/.sqlx/query-eefe20114e1b151c48f308f2f197c9a81dfe0f348725c57f7e01205d68b687a9.json b/rust/.sqlx/query-eefe20114e1b151c48f308f2f197c9a81dfe0f348725c57f7e01205d68b687a9.json deleted file mode 100644 index 15bb1144..00000000 --- a/rust/.sqlx/query-eefe20114e1b151c48f308f2f197c9a81dfe0f348725c57f7e01205d68b687a9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE receipts\n SET mark_for_retry = CAST(strftime('%s', 'now') AS INTEGER),\n retry_count = retry_count + 1\n WHERE receipt_id = ? AND contact_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "eefe20114e1b151c48f308f2f197c9a81dfe0f348725c57f7e01205d68b687a9" -} diff --git a/rust/.sqlx/query-ef217dcdf24e8161bf27a3a67f22bc4d3e6ad7ea2182686cd9ba9ff1eb9608ef.json b/rust/.sqlx/query-ef217dcdf24e8161bf27a3a67f22bc4d3e6ad7ea2182686cd9ba9ff1eb9608ef.json new file mode 100644 index 00000000..f6669cbd --- /dev/null +++ b/rust/.sqlx/query-ef217dcdf24e8161bf27a3a67f22bc4d3e6ad7ea2182686cd9ba9ff1eb9608ef.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE notification_outbox SET delivered_at = ? WHERE event_id = ? AND delivered_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "ef217dcdf24e8161bf27a3a67f22bc4d3e6ad7ea2182686cd9ba9ff1eb9608ef" +} diff --git a/rust/.sqlx/query-ef8c34f8171dfe13f2c55942f8813fa2e2811ea348375ff8d2864a9d322c6358.json b/rust/.sqlx/query-ef8c34f8171dfe13f2c55942f8813fa2e2811ea348375ff8d2864a9d322c6358.json new file mode 100644 index 00000000..fdef73bc --- /dev/null +++ b/rust/.sqlx/query-ef8c34f8171dfe13f2c55942f8813fa2e2811ea348375ff8d2864a9d322c6358.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM media_files WHERE media_id = ?)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM media_files WHERE media_id = ?)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "ef8c34f8171dfe13f2c55942f8813fa2e2811ea348375ff8d2864a9d322c6358" +} diff --git a/rust/.sqlx/query-f202050b08b772f33d9625797f23bd8c1adb9bd857065911f80d73b625e7801f.json b/rust/.sqlx/query-f202050b08b772f33d9625797f23bd8c1adb9bd857065911f80d73b625e7801f.json new file mode 100644 index 00000000..7c4cc65a --- /dev/null +++ b/rust/.sqlx/query-f202050b08b772f33d9625797f23bd8c1adb9bd857065911f80d73b625e7801f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET signal_version = 'v2' WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f202050b08b772f33d9625797f23bd8c1adb9bd857065911f80d73b625e7801f" +} diff --git a/rust/.sqlx/query-f264af9235d61c83056a34a2327949df38ca4ea372b0001c60a31b2896f71cf0.json b/rust/.sqlx/query-f264af9235d61c83056a34a2327949df38ca4ea372b0001c60a31b2896f71cf0.json new file mode 100644 index 00000000..eda66e77 --- /dev/null +++ b/rust/.sqlx/query-f264af9235d61c83056a34a2327949df38ca4ea372b0001c60a31b2896f71cf0.json @@ -0,0 +1,21 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS(SELECT 1 FROM user_discovery_shares LIMIT 1)", + "describe": { + "columns": [ + { + "name": "EXISTS(SELECT 1 FROM user_discovery_shares LIMIT 1)", + "ordinal": 0, + "type_info": "Integer", + "origin": "Expression" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "f264af9235d61c83056a34a2327949df38ca4ea372b0001c60a31b2896f71cf0" +} diff --git a/rust/.sqlx/query-f31642455fadb1864fac3386a8da4822311da3cacd61269ef4f95705ea982002.json b/rust/.sqlx/query-f31642455fadb1864fac3386a8da4822311da3cacd61269ef4f95705ea982002.json new file mode 100644 index 00000000..38864c42 --- /dev/null +++ b/rust/.sqlx/query-f31642455fadb1864fac3386a8da4822311da3cacd61269ef4f95705ea982002.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO receipts(\n receipt_id, contact_id, message, contact_will_sends_receipt\n ) VALUES (?, ?, ?, 0)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "f31642455fadb1864fac3386a8da4822311da3cacd61269ef4f95705ea982002" +} diff --git a/rust/.sqlx/query-f50e328e40de6f4f886a524118b6ef21dacefd2a248a2a8256cd100451d9889a.json b/rust/.sqlx/query-f50e328e40de6f4f886a524118b6ef21dacefd2a248a2a8256cd100451d9889a.json deleted file mode 100644 index 0bc8b05e..00000000 --- a/rust/.sqlx/query-f50e328e40de6f4f886a524118b6ef21dacefd2a248a2a8256cd100451d9889a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO group_members(group_id, contact_id, member_state)\n VALUES (?, ?, 'member')\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "f50e328e40de6f4f886a524118b6ef21dacefd2a248a2a8256cd100451d9889a" -} diff --git a/rust/.sqlx/query-f82da34ecdaf448b9825b8e1bad094c75554c2b46d0f96a1714d31b402c9f49d.json b/rust/.sqlx/query-f82da34ecdaf448b9825b8e1bad094c75554c2b46d0f96a1714d31b402c9f49d.json new file mode 100644 index 00000000..bb53e8b9 --- /dev/null +++ b/rust/.sqlx/query-f82da34ecdaf448b9825b8e1bad094c75554c2b46d0f96a1714d31b402c9f49d.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT media_stored FROM messages WHERE message_id = ?", + "describe": { + "columns": [ + { + "name": "media_stored", + "ordinal": 0, + "type_info": "Integer", + "origin": { + "Table": { + "table": "messages", + "name": "media_stored" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "f82da34ecdaf448b9825b8e1bad094c75554c2b46d0f96a1714d31b402c9f49d" +} diff --git a/rust/.sqlx/query-f93bfda82600aef970e0b7c17459ae774272af5db7898ea3594c9c921a932a66.json b/rust/.sqlx/query-f93bfda82600aef970e0b7c17459ae774272af5db7898ea3594c9c921a932a66.json new file mode 100644 index 00000000..c4ae2b8a --- /dev/null +++ b/rust/.sqlx/query-f93bfda82600aef970e0b7c17459ae774272af5db7898ea3594c9c921a932a66.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT recovery_contacts_secret_share FROM contacts WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "recovery_contacts_secret_share", + "ordinal": 0, + "type_info": "Blob", + "origin": { + "Table": { + "table": "contacts", + "name": "recovery_contacts_secret_share" + } + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "f93bfda82600aef970e0b7c17459ae774272af5db7898ea3594c9c921a932a66" +} diff --git a/rust/.sqlx/query-f95f23d23817d46082ebc54610239cfe47265d64f80c833ee8d6495ddb6b90b4.json b/rust/.sqlx/query-f95f23d23817d46082ebc54610239cfe47265d64f80c833ee8d6495ddb6b90b4.json new file mode 100644 index 00000000..66e4fe7c --- /dev/null +++ b/rust/.sqlx/query-f95f23d23817d46082ebc54610239cfe47265d64f80c833ee8d6495ddb6b90b4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM groups WHERE group_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f95f23d23817d46082ebc54610239cfe47265d64f80c833ee8d6495ddb6b90b4" +} diff --git a/rust/.sqlx/query-fa94eafde21afc4f8efdd64e95b7c2387451f1107ba53a4d1e3854a2c05737b7.json b/rust/.sqlx/query-fa94eafde21afc4f8efdd64e95b7c2387451f1107ba53a4d1e3854a2c05737b7.json new file mode 100644 index 00000000..bd5421f5 --- /dev/null +++ b/rust/.sqlx/query-fa94eafde21afc4f8efdd64e95b7c2387451f1107ba53a4d1e3854a2c05737b7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM receipts WHERE contact_id = ? AND message_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "fa94eafde21afc4f8efdd64e95b7c2387451f1107ba53a4d1e3854a2c05737b7" +} diff --git a/rust/.sqlx/query-fb9d21dc469738356e38205fc0addbbf82b85d6e4ec3090f7718dc692d7dbdf5.json b/rust/.sqlx/query-fb9d21dc469738356e38205fc0addbbf82b85d6e4ec3090f7718dc692d7dbdf5.json new file mode 100644 index 00000000..39f1afad --- /dev/null +++ b/rust/.sqlx/query-fb9d21dc469738356e38205fc0addbbf82b85d6e4ec3090f7718dc692d7dbdf5.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT member_state FROM group_members WHERE group_id = ? AND contact_id = ?", + "describe": { + "columns": [ + { + "name": "member_state", + "ordinal": 0, + "type_info": "Text", + "origin": { + "Table": { + "table": "group_members", + "name": "member_state" + } + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true + ] + }, + "hash": "fb9d21dc469738356e38205fc0addbbf82b85d6e4ec3090f7718dc692d7dbdf5" +} diff --git a/rust/.sqlx/query-fc76c1c5acdd20c8645a77388c885ad47d08683c8ef4cb1e52ee106eb898a00d.json b/rust/.sqlx/query-fc76c1c5acdd20c8645a77388c885ad47d08683c8ef4cb1e52ee106eb898a00d.json new file mode 100644 index 00000000..ad0b70ce --- /dev/null +++ b/rust/.sqlx/query-fc76c1c5acdd20c8645a77388c885ad47d08683c8ef4cb1e52ee106eb898a00d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR IGNORE INTO group_members(group_id, contact_id) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "fc76c1c5acdd20c8645a77388c885ad47d08683c8ef4cb1e52ee106eb898a00d" +} diff --git a/rust/.sqlx/query-fcba7a9b6c5c62a7086365f9120fec35a2a20a3472de9efe6f9fc55f6f2f19dd.json b/rust/.sqlx/query-fcba7a9b6c5c62a7086365f9120fec35a2a20a3472de9efe6f9fc55f6f2f19dd.json new file mode 100644 index 00000000..b3e7c44d --- /dev/null +++ b/rust/.sqlx/query-fcba7a9b6c5c62a7086365f9120fec35a2a20a3472de9efe6f9fc55f6f2f19dd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE received_receipts\n SET created_at = CAST(strftime('%s', 'now') AS INTEGER)\n WHERE receipt_id = ?\n AND created_at <= CAST(strftime('%s', 'now') AS INTEGER) - 864000\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "fcba7a9b6c5c62a7086365f9120fec35a2a20a3472de9efe6f9fc55f6f2f19dd" +} diff --git a/rust/.sqlx/query-febc50ab89aa3a9bbb2e9333d8547cb700bc3039c178e133def8a2dc2e8aabb6.json b/rust/.sqlx/query-febc50ab89aa3a9bbb2e9333d8547cb700bc3039c178e133def8a2dc2e8aabb6.json deleted file mode 100644 index a6abc4a9..00000000 --- a/rust/.sqlx/query-febc50ab89aa3a9bbb2e9333d8547cb700bc3039c178e133def8a2dc2e8aabb6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE groups\n SET last_message_exchange = MAX(last_message_exchange, ?)\n WHERE group_id = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "febc50ab89aa3a9bbb2e9333d8547cb700bc3039c178e133def8a2dc2e8aabb6" -} diff --git a/rust/.sqlx/query-fedeeeaee2ad31906726430055edac23359bb61ff80d53632c95b40837d504f3.json b/rust/.sqlx/query-fedeeeaee2ad31906726430055edac23359bb61ff80d53632c95b40837d504f3.json new file mode 100644 index 00000000..e4a07001 --- /dev/null +++ b/rust/.sqlx/query-fedeeeaee2ad31906726430055edac23359bb61ff80d53632c95b40837d504f3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE receipts SET message_id = ?, will_be_retried_by_media_upload = ?,\n retry_count = ?, last_retry = ? WHERE receipt_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "fedeeeaee2ad31906726430055edac23359bb61ff80d53632c95b40837d504f3" +} diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d685e310..2a0cd447 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -176,6 +176,18 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "assert_matches" version = "1.5.0" @@ -256,7 +268,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -270,6 +282,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -574,6 +592,15 @@ dependencies = [ "rand 0.10.2", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpubits" version = "0.1.1" @@ -840,6 +867,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "delegate-attr" version = "0.3.0" @@ -1016,6 +1049,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.4.2" @@ -1043,6 +1085,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1071,6 +1122,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "flume" version = "0.12.0" @@ -1137,6 +1194,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "log", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1768,6 +1837,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "indenter" version = "0.3.4" @@ -1917,6 +1992,17 @@ dependencies = [ "log", ] +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2046,6 +2132,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsignal-core" version = "0.1.0" @@ -2082,7 +2174,7 @@ dependencies = [ "aes-gcm-siv", "assert_matches", "async-trait", - "bitflags", + "bitflags 2.13.1", "const-str", "ctr 0.10.1", "data-encoding-macro", @@ -2360,7 +2452,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -2486,6 +2578,12 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2498,6 +2596,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -2885,7 +2996,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -2955,6 +3066,29 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -2969,6 +3103,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rust_lib_twonly" version = "0.1.0" @@ -2989,6 +3129,7 @@ dependencies = [ "hex", "hkdf 0.12.4", "hmac 0.13.0", + "jni", "keyring-core", "libsignal-protocol", "libsqlite3-sys", @@ -3000,6 +3141,7 @@ dependencies = [ "rand 0.8.7", "rand 0.9.5", "reqwest", + "resvg", "rustls", "scrypt", "serde", @@ -3048,7 +3190,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -3108,6 +3250,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.23" @@ -3166,7 +3326,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -3351,12 +3511,36 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.2" @@ -3507,7 +3691,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "bitflags", + "bitflags 2.13.1", "byteorder", "bytes", "chrono", @@ -3536,7 +3720,7 @@ checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.13.1", "byteorder", "chrono", "crc", @@ -3618,6 +3802,15 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -3641,6 +3834,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "symlink" version = "0.1.0" @@ -3799,6 +4002,32 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -3966,7 +4195,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -4071,6 +4300,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -4103,6 +4341,18 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4124,6 +4374,18 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "universal-hash" version = "0.5.1" @@ -4162,6 +4424,33 @@ dependencies = [ "serde", ] +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -4575,6 +4864,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yoke" version = "0.8.3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ffa08812..83755eb1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -52,6 +52,7 @@ rand08 = { version = "0.8.5", package = "rand" } rand = "0.9.4" uuid = { version = "1", features = ["v4"] } flate2 = "1" +resvg = { version = "0.45", default-features = false, features = ["text"] } reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", ] } @@ -74,6 +75,7 @@ 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" +jni = "0.21.1" [dev-dependencies] anyhow = "1.0.104" diff --git a/rust/src/api/messages/incoming.rs b/rust/src/api/messages/incoming.rs index 30eed7e2..e00b155c 100644 --- a/rust/src/api/messages/incoming.rs +++ b/rust/src/api/messages/incoming.rs @@ -45,13 +45,13 @@ pub(crate) async fn handle_server_message( Kind::RequestNewPqcPreKeys(_) => match handle_request_new_pqc_prekeys(ctx).await { Ok(response) => response, Err(error) => { - tracing::error!("failed to generate requested PQC prekeys: {error}"); + tracing::warn!("failed to generate requested PQC prekeys: {error}"); ok::Ok::None(true) } }, Kind::NewMessage(message) => { if let Err(error) = handle_new_server_message(ctx, message).await { - tracing::error!("failed to process client message: {error}"); + tracing::warn!("failed to process client message: {error}"); } ok::Ok::None(true) } @@ -59,29 +59,33 @@ pub(crate) async fn handle_server_message( for message in messages.new_messages { if let Err(error) = handle_new_server_message(ctx, message).await { // One bad item must not block the rest of a server batch. - tracing::error!("failed to process client message in batch: {error}"); + tracing::warn!("failed to process client message in batch: {error}"); } } ok::Ok::None(true) } Kind::SealedSenderMessage(message) => { if let Err(error) = handle_sealed_message(ctx, message.body).await { - tracing::error!("failed to process sealed-sender message: {error}"); + tracing::warn!("failed to process sealed-sender message: {error}"); } ok::Ok::None(true) } Kind::SealedSenderMessages(messages) => { for message in messages.messages { if let Err(error) = handle_sealed_message(ctx, message.body).await { - tracing::error!("failed to process sealed-sender message in batch: {error}"); + tracing::warn!("failed to process sealed-sender message in batch: {error}"); } } ok::Ok::None(true) } + Kind::MailboxDrained(_) => { + ctx.mark_mailbox_drained(); + ok::Ok::None(true) + } other => { // Dart logged unknown unsolicited messages but still acknowledged // their envelope, preventing an infinite server redelivery loop. - tracing::error!("unsupported unsolicited server message: {other:?}"); + tracing::warn!("unsupported unsolicited server message: {other:?}"); ok::Ok::None(true) } }; @@ -305,6 +309,7 @@ pub(crate) async fn handle_decoded_server_message( } t.commit().await?; + ctx.mark_incoming_committed(); database.notify_committed([ "received_receipts", @@ -314,6 +319,7 @@ pub(crate) async fn handle_decoded_server_message( "contacts", "key_verifications", "user_discovery_own_promotions", + "notification_outbox", ]); let ctx = ctx.clone(); @@ -335,6 +341,24 @@ pub(crate) async fn handle_encrypted( from_user_id: i64, receipt_id: &str, content: proto::EncryptedContent, +) -> Result<()> { + let notification_content = content.clone(); + handle_encrypted_inner(ctx, t, from_user_id, receipt_id, content).await?; + crate::services::notifications::record_incoming_event( + t, + from_user_id, + receipt_id, + ¬ification_content, + ) + .await +} + +async fn handle_encrypted_inner( + ctx: &Arc, + t: &mut Transaction<'_, Sqlite>, + from_user_id: i64, + receipt_id: &str, + content: proto::EncryptedContent, ) -> Result<()> { Receipt::mark_all_for_retry(t, from_user_id).await?; diff --git a/rust/src/api/messages/incoming/additional_data.rs b/rust/src/api/messages/incoming/additional_data.rs index 2ef17c0a..ef7f3fe5 100644 --- a/rust/src/api/messages/incoming/additional_data.rs +++ b/rust/src/api/messages/incoming/additional_data.rs @@ -78,7 +78,7 @@ async fn verify_shared_contacts( }; if stored_identity != contact.public_identity_key { - tracing::error!("shared contact public identity key does not match"); + tracing::warn!("shared contact public identity key does not match"); continue; } diff --git a/rust/src/api/messages/incoming/messages.rs b/rust/src/api/messages/incoming/messages.rs index c42b86e4..219f2a01 100644 --- a/rust/src/api/messages/incoming/messages.rs +++ b/rust/src/api/messages/incoming/messages.rs @@ -32,6 +32,8 @@ pub(crate) async fn queue_encrypted_content( ) -> Result { Contact::ensure_exists(t, target_user_id).await?; + let wake_receiver = crate::services::notifications::should_wake_receiver(&content); + let message = proto::Message { r#type: proto::message::Type::CiphertextV2 as i32, receipt_id: String::new(), @@ -44,6 +46,7 @@ pub(crate) async fn queue_encrypted_content( NewReceipt::new(&receipt_id, target_user_id, &message) .contact_will_send_receipt(contact_will_send_receipt) + .wake_receiver(wake_receiver) .insert(t) .await?; @@ -297,6 +300,17 @@ pub(crate) struct PreparedQueuedReceipt { pub contact_will_sends_receipt: i64, pub account_deleted: i64, pub payload: Vec, + pub wake_receiver: bool, +} + +struct PreparedQueuedReceiptRow { + contact_id: i64, + message: Vec, + message_id: Option, + contact_will_sends_receipt: i64, + wake_receiver: i64, + account_deleted: i64, + signal_version: String, } pub(crate) async fn prepare_queued_receipt_details( @@ -304,10 +318,11 @@ pub(crate) async fn prepare_queued_receipt_details( receipt_id: &str, ) -> Result> { let app_db = ctx.app_db.read().await.clone(); - let row = sqlx::query!( + let row = sqlx::query_as!( + PreparedQueuedReceiptRow, r#" SELECT r.contact_id, r.message, r.message_id, r.contact_will_sends_receipt, - r.retry_count, c.account_deleted, c.signal_version + r.wake_receiver, c.account_deleted, c.signal_version FROM receipts r JOIN contacts c ON c.user_id = r.contact_id WHERE r.receipt_id = ? @@ -357,6 +372,7 @@ pub(crate) async fn prepare_queued_receipt_details( contact_will_sends_receipt: row.contact_will_sends_receipt, account_deleted: row.account_deleted, payload: message.encode_to_vec(), + wake_receiver: row.wake_receiver != 0, })) } @@ -396,7 +412,14 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> return Ok(()); } - match Server::send_text_message(ctx, receipt.contact_id, receipt.payload).await? { + match Server::send_text_message( + ctx, + receipt.contact_id, + receipt.payload, + receipt.wake_receiver, + ) + .await? + { ServerResult::Ok(()) => {} ServerResult::ErrorCode(code) => { return Err(TwonlyError::Generic(format!( @@ -420,6 +443,15 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> ) .execute(&mut *t) .await?; + + // `message_actions` keeps the per-recipient acknowledgement used by + // group chats. The message row also carries the aggregate value used + // by message bubbles and chat previews to leave the "sending" state. + sqlx::query("UPDATE messages SET ack_by_server = ? WHERE message_id = ?") + .bind(chrono::Utc::now().timestamp()) + .bind(&message_id) + .execute(&mut *t) + .await?; } if receipt.contact_will_sends_receipt == 0 { Receipt::delete(&mut *t, receipt_id).await?; @@ -439,7 +471,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> .await?; } t.commit().await?; - app_db.notify_committed(["receipts", "message_actions"]); + app_db.notify_committed(["receipts", "message_actions", "messages"]); Ok(()) } diff --git a/rust/src/api/messages/mod.rs b/rust/src/api/messages/mod.rs index 804d9bde..33b64028 100644 --- a/rust/src/api/messages/mod.rs +++ b/rust/src/api/messages/mod.rs @@ -21,8 +21,6 @@ pub(crate) fn content_type_kind( "ContactRequest" } else if content.flame_sync.is_some() { "FlameSync" - } else if content.push_keys.is_some() { - "PushKeys" } else if content.reaction.is_some() { "Reaction" } else if content.text_message.is_some() { diff --git a/rust/src/api/proto/api/websocket/client_to_server.proto b/rust/src/api/proto/api/websocket/client_to_server.proto index b528e833..43991cf9 100644 --- a/rust/src/api/proto/api/websocket/client_to_server.proto +++ b/rust/src/api/proto/api/websocket/client_to_server.proto @@ -126,7 +126,8 @@ message ApplicationData { message TextMessage { int64 user_id = 1; bytes body = 3; - optional bytes push_data = 4; + reserved 4; + bool wake_receiver = 5; } message GetUserByUsername { diff --git a/rust/src/api/proto/api/websocket/server_to_client.proto b/rust/src/api/proto/api/websocket/server_to_client.proto index 985b97b6..1cb35722 100644 --- a/rust/src/api/proto/api/websocket/server_to_client.proto +++ b/rust/src/api/proto/api/websocket/server_to_client.proto @@ -20,6 +20,7 @@ message V0 { error.ErrorCode error = 6; SealedSenderMessage sealedSenderMessage = 9; SealedSenderMessages sealedSenderMessages = 10; + bool mailboxDrained = 11; } } diff --git a/rust/src/api/proto/client/messages.proto b/rust/src/api/proto/client/messages.proto index 9962cf40..24333e65 100644 --- a/rust/src/api/proto/client/messages.proto +++ b/rust/src/api/proto/client/messages.proto @@ -16,7 +16,6 @@ message EncryptedContent { optional ContactUpdate contact_update = 8; optional ContactRequest contact_request = 9; optional FlameSync flame_sync = 10; - optional PushKeys push_keys = 11; optional Reaction reaction = 12; optional TextMessage text_message = 13; optional GroupCreate group_create = 14; @@ -152,18 +151,6 @@ message EncryptedContent { optional string display_name = 4; } - message PushKeys { - enum Type { - REQUEST = 0; - UPDATE = 1; - } - - Type type = 1; - optional int64 key_id = 2; - optional bytes key = 3; - optional int64 created_at = 4; - } - message FlameSync { int64 flame_counter = 1; int64 last_flame_counter_change = 2; diff --git a/rust/src/api/runtime.rs b/rust/src/api/runtime.rs index de9b65b7..38f02c3f 100644 --- a/rust/src/api/runtime.rs +++ b/rust/src/api/runtime.rs @@ -32,10 +32,7 @@ impl ApiRuntime { pub async fn reload_configuration(ctx: &Arc) -> Result<()> { let replacement = ApiClient::new(ctx, ApiConfig::from_rust_state(ctx).await?); let current = Self::client(ctx).await?; - let slot = ctx - .api_client - .get() - .ok_or(TwonlyError::Initialization)?; + let slot = ctx.api_client.get().ok_or(TwonlyError::Initialization)?; *slot.write().await = replacement; current.close().await; Self::client(ctx).await?.connect().await @@ -228,10 +225,7 @@ impl ApiRuntime { } pub(crate) async fn client(ctx: &Arc) -> Result> { - let client = ctx - .api_client - .get() - .ok_or(TwonlyError::Initialization)?; + let client = ctx.api_client.get().ok_or(TwonlyError::Initialization)?; Ok(client.read().await.clone()) } } diff --git a/rust/src/api/runtime/client.rs b/rust/src/api/runtime/client.rs index 3eb21c1f..4f4aaf06 100644 --- a/rust/src/api/runtime/client.rs +++ b/rust/src/api/runtime/client.rs @@ -37,6 +37,7 @@ pub(crate) struct ApiClient { impl ApiClient { pub(crate) fn new(context: &Arc, config: ApiConfig) -> Arc { + let in_background = config.in_background; Arc::new(Self { context: Arc::downgrade(context), config, @@ -46,7 +47,7 @@ impl ApiClient { next_sequence: Mutex::const_new(1), events: API_EVENTS.clone(), deliberately_closed: AtomicBool::new(false), - in_background: AtomicBool::new(false), + in_background: AtomicBool::new(in_background), network_available: AtomicBool::new(true), is_authenticated: Arc::new(AtomicBool::new(false)), }) @@ -167,6 +168,7 @@ impl ApiClient { pub async fn close(&self) { self.deliberately_closed.store(true, Ordering::Release); + self.is_authenticated.store(false, Ordering::Release); let client = self.ws_client.lock().await.take(); if let Some(client) = client { if let Err(error) = client.shutdown_graceful(Duration::from_secs(5)).await { diff --git a/rust/src/api/runtime/helpers.rs b/rust/src/api/runtime/helpers.rs index 32c807e9..fa1ae124 100644 --- a/rust/src/api/runtime/helpers.rs +++ b/rust/src/api/runtime/helpers.rs @@ -9,7 +9,7 @@ use crate::api::runtime::ApiRuntime; use crate::api::Server; use crate::bridge::api::ServerResult; -use crate::context::Context; +use crate::context::{Context, RuntimeMode}; use crate::error::{Result, TwonlyError}; use crate::services::groups::GroupService; use crate::services::mediafiles::MediaFileService; @@ -40,6 +40,11 @@ pub(crate) fn response_error_code(bytes: &[u8]) -> Result> { } pub(crate) fn schedule_post_authentication(ctx: &Arc, in_background: bool) { + // Notification workers only drain and commit the mailbox. Media downloads, + // maintenance, and outbox replay belong to the main application runtime. + if ctx.runtime_mode == RuntimeMode::Notification { + return; + } let ctx = ctx.clone(); tokio::spawn(async move { // Wait a bit to let other initial state settle @@ -78,13 +83,7 @@ pub(crate) fn schedule_post_authentication(ctx: &Arc, in_background: bo tracing::warn!("passwordless recovery heartbeat failed: {error}"); } - if let Err(error) = ctx - .user_discovery - .get() - .await - .on_connected(&ctx) - .await - { + if let Err(error) = ctx.user_discovery.get().await.on_connected(&ctx).await { tracing::warn!("user-discovery post-connection refresh failed: {error}"); } diff --git a/rust/src/api/server/contacts.rs b/rust/src/api/server/contacts.rs index 4377bedc..5c0e2232 100644 --- a/rust/src/api/server/contacts.rs +++ b/rust/src/api/server/contacts.rs @@ -136,6 +136,7 @@ impl Server { ctx: &Arc, user_id: i64, body: Vec, + wake_receiver: bool, ) -> Result> { server_ok!( Self::application_for_contact( @@ -144,7 +145,7 @@ impl Server { client_to_server::application_data::TextMessage { user_id, body, - push_data: None, + wake_receiver, }, ), user_id, diff --git a/rust/src/bridge/api.rs b/rust/src/bridge/api.rs index 698dd2a8..dfe3ff1c 100644 --- a/rust/src/bridge/api.rs +++ b/rust/src/bridge/api.rs @@ -8,6 +8,7 @@ use crate::api::ApiRuntime; pub use crate::api::PqcPreKeyInput; use crate::api::Server; use crate::context::Context; +use crate::context::RuntimeMode; use crate::error::{Result, TwonlyError}; use crate::frb_generated::StreamSink; use crate::services::contacts::ContactService; @@ -51,7 +52,7 @@ impl ApiConfig { Ok(Self { websocket_url: format!("{}client", RustApi::api_base_url("wss".to_owned())), legacy_user_app_version: user.as_ref().map_or(0, |value| value.app_version), - in_background: false, + in_background: context.runtime_mode == RuntimeMode::Notification, can_use_login_token_for_auth: user .as_ref() .is_some_and(|value| value.can_use_login_token_for_auth), @@ -546,7 +547,7 @@ impl RustApi { } pub async fn send_text_message(user_id: i64, body: Vec) -> Result<()> { let ctx = Context::get_static()?; - Server::send_text_message(ctx, user_id, body) + Server::send_text_message(ctx, user_id, body, false) .await .and_then(api_result) } diff --git a/rust/src/bridge/callbacks.rs b/rust/src/bridge/callbacks.rs index 9132b2c6..ddc6911c 100644 --- a/rust/src/bridge/callbacks.rs +++ b/rust/src/bridge/callbacks.rs @@ -54,7 +54,7 @@ pub(crate) fn get_callbacks() -> Result { // we pick the first available callbacks from the map. This gracefully handles // tracing initialization which happens outside of any scoped task. if let Some((_, cb)) = map.iter().next() { - tracing::error!("FlutterCallbacks fallback used: No CURRENT_CALLBACK_ID scope was found, or the ID was missing from the map. Using an arbitrary callback. This may lead to race conditions if multiple isolates are active."); + tracing::warn!("FlutterCallbacks fallback used: No CURRENT_CALLBACK_ID scope was found, or the ID was missing from the map. Using an arbitrary callback. This may lead to race conditions if multiple isolates are active."); return Ok(cb.clone()); } diff --git a/rust/src/bridge/callbacks/log.rs b/rust/src/bridge/callbacks/log.rs index b5eaa62c..9ddebde6 100644 --- a/rust/src/bridge/callbacks/log.rs +++ b/rust/src/bridge/callbacks/log.rs @@ -4,17 +4,145 @@ */ use crate::frb_generated::StreamSink; +use std::sync::RwLock; use tracing_subscriber::fmt::MakeWriter; -#[derive(Clone)] -pub(crate) struct DartWriter { - pub(crate) sink: StreamSink, +/// The sink is bound to the native port of the Dart isolate that handed it to +/// us. That isolate can go away while the process (and therefore the tracing +/// subscriber) lives on -- a hot restart, an engine restart, or a background +/// isolate that finished its task. Keeping the sink behind a lock lets a newly +/// initialized isolate take over the log stream instead of writing into a dead +/// port forever. +static DART_SINK: RwLock>> = RwLock::new(None); + +pub(crate) fn set_dart_sink(sink: StreamSink) { + if let Ok(mut guard) = DART_SINK.write() { + *guard = Some(sink); + } } +pub fn strip_ansi(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\x1b' { + if let Some(&next) = chars.peek() { + if next == '[' { + // CSI sequence: ESC [ ... [final byte 0x40..=0x7e] + chars.next(); // consume '[' + while let Some(&ch) = chars.peek() { + chars.next(); + if ('@'..='~').contains(&ch) { + break; + } + } + continue; + } else if next == ']' { + // OSC sequence: ESC ] ... (BEL \x07 or ESC \) + chars.next(); // consume ']' + while let Some(ch) = chars.next() { + if ch == '\x07' { + break; + } + if ch == '\x1b' && chars.peek() == Some(&'\\') { + chars.next(); + break; + } + } + continue; + } else if ('@'..='_').contains(&next) { + // 2-character escape sequence + chars.next(); + continue; + } + } + } else if c == '\\' && chars.peek() == Some(&'^') { + let mut clone = chars.clone(); + clone.next(); // '^' + if clone.next() == Some('[') { + if clone.peek() == Some(&'[') { + clone.next(); + } + let mut valid = false; + while let Some(ch) = clone.next() { + if ('@'..='~').contains(&ch) { + valid = true; + break; + } else if !('0'..='?').contains(&ch) && !(' '..='/').contains(&ch) { + break; + } + } + if valid { + chars.next(); // consume '^' + chars.next(); // consume '[' + if chars.peek() == Some(&'[') { + chars.next(); + } + while let Some(&ch) = chars.peek() { + chars.next(); + if ('@'..='~').contains(&ch) { + break; + } + } + continue; + } + } + } else if c == '^' && chars.peek() == Some(&'[') { + let mut clone = chars.clone(); + clone.next(); // '[' + if clone.peek() == Some(&'[') { + clone.next(); + } + let mut valid = false; + while let Some(ch) = clone.next() { + if ('@'..='~').contains(&ch) { + valid = true; + break; + } else if !('0'..='?').contains(&ch) && !(' '..='/').contains(&ch) { + break; + } + } + if valid { + chars.next(); // consume '[' + if chars.peek() == Some(&'[') { + chars.next(); + } + while let Some(&ch) = chars.peek() { + chars.next(); + if ('@'..='~').contains(&ch) { + break; + } + } + continue; + } + } + out.push(c); + } + out +} + +#[derive(Clone, Copy)] +pub(crate) struct DartWriter; + impl std::io::Write for DartWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { if let Ok(msg) = std::str::from_utf8(buf) { - let _ = self.sink.add(msg.trim_end().to_string()); + let clean = strip_ansi(msg.trim_end()); + let failed = match DART_SINK.read() { + Ok(guard) => match guard.as_ref() { + Some(sink) => sink.add(clean).is_err(), + None => false, + }, + Err(_) => false, + }; + // The isolate behind the sink is gone. Drop it so we stop paying + // for a send on every log line until a new isolate registers. + if failed { + if let Ok(mut guard) = DART_SINK.write() { + *guard = None; + } + } } Ok(buf.len()) } @@ -28,6 +156,41 @@ impl<'a> MakeWriter<'a> for DartWriter { type Writer = DartWriter; fn make_writer(&'a self) -> Self::Writer { - self.clone() + *self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_ansi_raw() { + let input = + "\x1b[3mreceipt_id\x1b[0m\x1b[2m=\x1b[0m\"d9891084-0f7c-4f30-958d-d8619df5a91c\""; + assert_eq!( + strip_ansi(input), + "receipt_id=\"d9891084-0f7c-4f30-958d-d8619df5a91c\"" + ); + } + + #[test] + fn test_strip_ansi_escaped_caret() { + let input = r#"\^[[3mreceipt_id\^[[0m\^[[2m=\^[[0m"d9891084-0f7c-4f30-958d-d8619df5a91c" \^[[3muser\^[[0m\^[[2m=\^[[0m2214489137315557376 \^[[3mkind\^[[0m\^[[2m=\^[[0m"FlameSync" Handling incoming message: FlameSync"#; + assert_eq!( + strip_ansi(input), + r#"receipt_id="d9891084-0f7c-4f30-958d-d8619df5a91c" user=2214489137315557376 kind="FlameSync" Handling incoming message: FlameSync"# + ); + } + + #[test] + fn test_strip_ansi_caret() { + let input = "^[[3mreceipt_id^[[0m^[[2m=^[[0m\"test\""; + assert_eq!(strip_ansi(input), "receipt_id=\"test\""); + } + + #[test] + fn test_strip_ansi_plain_text() { + assert_eq!(strip_ansi("hello world 123"), "hello world 123"); } } diff --git a/rust/src/bridge/mod.rs b/rust/src/bridge/mod.rs index dcfde22c..f949aa02 100644 --- a/rust/src/bridge/mod.rs +++ b/rust/src/bridge/mod.rs @@ -10,7 +10,6 @@ pub mod groups; pub mod user_config; pub mod wrapper; - use crate::context::Context; use crate::error::Result; use flutter_rust_bridge::frb; @@ -55,3 +54,10 @@ pub async fn initialize_twonly_flutter(config: InitConfig) -> Result<()> { pub async fn initialize_twonly_standalone(config: InitConfig) -> Result<()> { Context::init_standalone(config).await } + +/// Initializes a short-lived background runtime which authenticates without +/// replacing an active foreground session. +#[frb(ignore)] +pub async fn initialize_twonly_notification(config: InitConfig) -> Result<()> { + Context::init_notification(config).await +} diff --git a/rust/src/bridge/wrapper/app_database.rs b/rust/src/bridge/wrapper/app_database.rs index 05f22e6f..4e7dfbd2 100644 --- a/rust/src/bridge/wrapper/app_database.rs +++ b/rust/src/bridge/wrapper/app_database.rs @@ -8,6 +8,8 @@ use std::path::PathBuf; use crate::bridge::get_twonly_flutter; pub use crate::database::app::{SqlExecutionResult, SqlRow, SqlRows, SqlValue}; use crate::error::Result; +use crate::frb_generated::StreamSink; +use tokio::sync::broadcast::error::RecvError; pub struct RustAppDatabase {} @@ -75,4 +77,51 @@ impl RustAppDatabase { .raw_execute(statement, arguments) .await } + + /// Streams the tables Rust has committed to. + /// + /// Rust owns the connection, so writes it makes on its own never pass + /// through the Drift compatibility executor and cannot invalidate Drift's + /// query streams. Dart forwards each batch into `notifyUpdates` so + /// `watch()` keeps reflecting Rust-side writes. + /// + /// An empty list means "assume every table changed". It is sent right + /// after (re)subscribing, and whenever the broadcast channel drops + /// notifications, so Dart never silently keeps stale rows on screen. + pub async fn changes(sink: StreamSink>) -> Result<()> { + let context = get_twonly_flutter()?; + + tokio::spawn(async move { + loop { + let mut receiver = context.app_db.read().await.subscribe(); + + // Anything committed between this subscription and the + // previous one is unobservable, so start from a clean slate. + if sink.add(Vec::new()).is_err() { + return; + } + + loop { + match receiver.recv().await { + Ok(change) => { + if sink.add(change.tables.into_iter().collect()).is_err() { + return; + } + } + Err(RecvError::Lagged(skipped)) => { + tracing::warn!(skipped, "Drift change stream lagged"); + if sink.add(Vec::new()).is_err() { + return; + } + } + // The database was replaced, most likely by a backup + // restore. Attach to the new one. + Err(RecvError::Closed) => break, + } + } + } + }); + + Ok(()) + } } diff --git a/rust/src/bridge/wrapper/signal.rs b/rust/src/bridge/wrapper/signal.rs index 9fc1f3e7..11dd0a4d 100644 --- a/rust/src/bridge/wrapper/signal.rs +++ b/rust/src/bridge/wrapper/signal.rs @@ -55,6 +55,8 @@ impl RustSignal { pub async fn get_contact_public_key(contact_id: i64) -> Result>> { let guard = get_twonly_flutter()?.signal_engine.lock().await; let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?; - engine.get_contact_identity_key(&contact_id.to_string()).await + engine + .get_contact_identity_key(&contact_id.to_string()) + .await } } diff --git a/rust/src/context.rs b/rust/src/context.rs index 0b1d36ab..ab3bafb6 100644 --- a/rust/src/context.rs +++ b/rust/src/context.rs @@ -5,7 +5,7 @@ use crate::api::runtime::{ApiClient, ApiRuntime}; use crate::bridge::InitConfig; -use crate::database::app::{APP_DATABASE_FILE, AppDatabase}; +use crate::database::app::{AppDatabase, APP_DATABASE_FILE}; use crate::database::signal::Database; use crate::error::Result; use crate::error::TwonlyError; @@ -17,14 +17,23 @@ use crate::signal::engine::RustSignalEngine; use crate::user_discovery::UserDiscovery; use crate::utils::Shared; use libsignal_protocol::IdentityKey; +use std::sync::atomic::{AtomicU64, Ordering}; use std::{path::PathBuf, sync::Arc}; -use tokio::sync::{Mutex, OnceCell, RwLock}; +use tokio::sync::{Mutex, Notify, OnceCell, RwLock}; use zeroize::Zeroize; static GLOBAL_CONTEXT: OnceCell> = OnceCell::const_new(); +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RuntimeMode { + Flutter, + Standalone, + Notification, +} + pub struct Context { pub config: InitConfig, + pub(crate) runtime_mode: RuntimeMode, pub rust_db: Arc>>, pub app_db: Arc>>, pub(crate) secure_storage: SecureStorage, @@ -32,16 +41,24 @@ pub struct Context { pub(crate) user_discovery: Shared, pub(crate) signal_engine: Arc>>, pub(crate) api_client: OnceCell>>, + mailbox_generation: AtomicU64, + mailbox_drained: Notify, + incoming_generation: AtomicU64, + incoming_committed: Notify, } impl Context { pub(crate) async fn init_flutter(config: InitConfig) -> Result<()> { - Self::init_common(config, true).await + Self::init_common(config, RuntimeMode::Flutter).await } #[allow(dead_code)] pub(crate) async fn init_standalone(config: InitConfig) -> Result<()> { - Self::init_common(config, false).await + Self::init_common(config, RuntimeMode::Standalone).await + } + + pub(crate) async fn init_notification(config: InitConfig) -> Result<()> { + Self::init_common(config, RuntimeMode::Notification).await } pub async fn init_for_testing( @@ -92,6 +109,7 @@ impl Context { let ctx = Arc::new(Context { config, + runtime_mode: RuntimeMode::Standalone, rust_db, app_db, secure_storage, @@ -99,6 +117,10 @@ impl Context { user_discovery, signal_engine: Arc::new(Mutex::new(None)), api_client: OnceCell::const_new(), + mailbox_generation: AtomicU64::new(0), + mailbox_drained: Notify::new(), + incoming_generation: AtomicU64::new(0), + incoming_committed: Notify::new(), }); ApiRuntime::initialize(&ctx).await?; ApiRuntime::connect(&ctx).await?; @@ -155,19 +177,20 @@ impl Context { .await } - 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(()); - } - + async fn init_common(config: InitConfig, runtime_mode: RuntimeMode) -> Result<()> { std::fs::create_dir_all(&config.database_dir)?; std::fs::create_dir_all(&config.data_dir)?; + // Ahead of the already-initialized check: the context is a process-wide + // OnceCell, but the calling isolate may be a new one that has to hand + // tracing a live log sink. let log_dir = PathBuf::from(&config.data_dir).join("log"); - init_tracing(&log_dir, is_flutter).await; + init_tracing(&log_dir, runtime_mode == RuntimeMode::Flutter).await; + + if GLOBAL_CONTEXT.initialized() { + tracing::info!("twonly already initialized. Ensuring storage directories exist."); + return Ok(()); + } SecureStorage::init()?; let secure_storage = SecureStorage::new("eu.twonly"); @@ -182,7 +205,7 @@ impl Context { let key_manager = match KeyManager::try_from_keychain(&secure_storage) { Ok(key) => key, Err(err) => { - tracing::error!("{err}"); + tracing::warn!("{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); @@ -218,7 +241,7 @@ impl Context { app_db_key.zeroize(); rust_db_key.zeroize(); - if is_flutter { + if runtime_mode == RuntimeMode::Flutter { let key_manager = Arc::new(Mutex::new(key_manager)); let signal_engine = { let key_manager_guard = key_manager.lock().await; @@ -245,6 +268,7 @@ impl Context { )?); let ctx = Arc::new(Context { config, + runtime_mode, secure_storage, rust_db: rust_db_handle, app_db, @@ -252,6 +276,10 @@ impl Context { user_discovery, signal_engine, api_client: OnceCell::const_new(), + mailbox_generation: AtomicU64::new(0), + mailbox_drained: Notify::new(), + incoming_generation: AtomicU64::new(0), + incoming_committed: Notify::new(), }); if let Err(error) = ctx.initialize_user_discovery_from_config().await { tracing::warn!("failed to initialize user discovery: {error}"); @@ -280,6 +308,7 @@ impl Context { )?); let ctx = Arc::new(Context { config, + runtime_mode, rust_db: rust_db_handle, app_db, key_manager, @@ -287,6 +316,10 @@ impl Context { user_discovery, signal_engine, api_client: OnceCell::const_new(), + mailbox_generation: AtomicU64::new(0), + mailbox_drained: Notify::new(), + incoming_generation: AtomicU64::new(0), + incoming_committed: Notify::new(), }); if let Err(error) = ctx.initialize_user_discovery_from_config().await { tracing::warn!("failed to initialize user discovery: {error}"); @@ -297,7 +330,9 @@ impl Context { }) .await; let ctx = res?; - ApiRuntime::connect(ctx).await?; + if runtime_mode != RuntimeMode::Notification { + ApiRuntime::connect(ctx).await?; + } Ok(()) } @@ -305,6 +340,44 @@ impl Context { GLOBAL_CONTEXT.get().ok_or(TwonlyError::Initialization) } + pub(crate) fn mailbox_generation(&self) -> u64 { + self.mailbox_generation.load(Ordering::Acquire) + } + + pub(crate) fn mark_mailbox_drained(&self) { + self.mailbox_generation.fetch_add(1, Ordering::AcqRel); + self.mailbox_drained.notify_waiters(); + } + + pub(crate) async fn wait_for_mailbox_after(&self, generation: u64) { + while self.mailbox_generation() <= generation { + let notified = self.mailbox_drained.notified(); + if self.mailbox_generation() > generation { + break; + } + notified.await; + } + } + + pub(crate) fn incoming_generation(&self) -> u64 { + self.incoming_generation.load(Ordering::Acquire) + } + + pub(crate) fn mark_incoming_committed(&self) { + self.incoming_generation.fetch_add(1, Ordering::AcqRel); + self.incoming_committed.notify_waiters(); + } + + pub(crate) async fn wait_for_incoming_after(&self, generation: u64) { + while self.incoming_generation() <= generation { + let notified = self.incoming_committed.notified(); + if self.incoming_generation() > generation { + break; + } + notified.await; + } + } + pub(crate) async fn user_id(&self) -> Result { self.key_manager .lock() diff --git a/rust/src/database/app/migrations/0003_notification_outbox.sql b/rust/src/database/app/migrations/0003_notification_outbox.sql new file mode 100644 index 00000000..9400a31d --- /dev/null +++ b/rust/src/database/app/migrations/0003_notification_outbox.sql @@ -0,0 +1,21 @@ +CREATE TABLE notification_outbox ( + event_id TEXT NOT NULL PRIMARY KEY, + notification_id TEXT NOT NULL, + conversation_id TEXT, + sender_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE, + message_id TEXT, + kind TEXT NOT NULL, + content TEXT, + created_at INTEGER NOT NULL, + delivered_at INTEGER, + cleared_at INTEGER +); + +CREATE INDEX idx_notification_outbox_pending + ON notification_outbox(delivered_at, created_at); + +CREATE INDEX idx_notification_outbox_conversation + ON notification_outbox(conversation_id, cleared_at); + +ALTER TABLE receipts + ADD COLUMN wake_receiver INTEGER NOT NULL DEFAULT 0 CHECK (wake_receiver IN (0, 1)); diff --git a/rust/src/database/app/mod.rs b/rust/src/database/app/mod.rs index 5c6ec5c8..7a56ff1a 100644 --- a/rust/src/database/app/mod.rs +++ b/rust/src/database/app/mod.rs @@ -4,10 +4,11 @@ */ use crate::error::{Result, TwonlyError}; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, UpdateHookResult}; use sqlx::{AssertSqlSafe, Column, ConnectOptions, Row, SqlitePool, TypeInfo, ValueRef}; use std::collections::BTreeSet; use std::str::FromStr; +use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::broadcast; @@ -15,8 +16,12 @@ mod legacy_import; pub mod tables; pub const APP_DATABASE_FILE: &str = "app_db.sqlite"; -pub const APP_SCHEMA_VERSION: i64 = 2; +pub const APP_SCHEMA_VERSION: i64 = 3; +/// Tables imported from the legacy Drift database. Every entry must exist in +/// Drift schema 25, because a missing table aborts the whole import. Rust-only +/// tables such as `notification_outbox` are deliberately absent: they have no +/// legacy counterpart, and importing stale rows would replay old notifications. pub const APPLICATION_TABLES: &[&str] = &[ "contacts", "groups", @@ -67,15 +72,61 @@ impl AppDatabase { if let Some(key) = encryption_key { options = options.pragma("key", format!("'{key}'")); } + let (changes, _) = broadcast::channel(256); + + // SQLite itself reports which tables a statement touched, so Drift's + // query streams stay correct without every Rust write site having to + // remember an explicit `notify_committed`. Rows are collected as they + // are written and only published once the transaction commits, so a + // rollback never reaches the UI. + let pending: Arc>> = Arc::default(); + let hook_pending = pending.clone(); + let hook_changes = changes.clone(); + let pool = SqlitePoolOptions::new() // The compatibility executor uses statement-based transactions. // Keeping one connection guarantees BEGIN, all statements, and // COMMIT are executed on that same native connection. .max_connections(1) .acquire_timeout(Duration::from_secs(30)) + .after_connect(move |connection, _meta| { + let pending = hook_pending.clone(); + let changes = hook_changes.clone(); + Box::pin(async move { + let mut handle = connection.lock_handle().await?; + + let updated = pending.clone(); + handle.set_update_hook(move |result: UpdateHookResult| { + if let Ok(mut tables) = updated.lock() { + tables.insert(result.table.to_owned()); + } + }); + + let committed = pending.clone(); + handle.set_commit_hook(move || { + let tables = committed + .lock() + .map(|mut tables| std::mem::take(&mut *tables)) + .unwrap_or_default(); + if !tables.is_empty() { + let _ = changes.send(DatabaseChange { tables }); + } + // Never veto the commit. + true + }); + + handle.set_rollback_hook(move || { + if let Ok(mut tables) = pending.lock() { + tables.clear(); + } + }); + + Ok(()) + }) + }) .connect_with(options) .await?; - let (changes, _) = broadcast::channel(256); + Ok(Self { pool, changes }) } @@ -103,6 +154,12 @@ impl AppDatabase { self.changes.subscribe() } + /// Publishes a change immediately, without waiting for a commit. + /// + /// The SQLite hooks installed in [`AppDatabase::new`] already cover every + /// ordinary write. This stays for the cases they cannot see -- most notably + /// `DELETE FROM ` with no `WHERE`, which SQLite's truncate + /// optimization performs without invoking the update hook. pub fn notify_committed<'a>(&self, tables: impl IntoIterator) { let tables = tables.into_iter().map(str::to_owned).collect(); let _ = self.changes.send(DatabaseChange { tables }); @@ -312,3 +369,89 @@ pub struct TableMigrationCount { pub table: String, pub rows: i64, } + +#[cfg(test)] +mod change_notification_tests { + use super::*; + use tempfile::tempdir; + use tokio::sync::broadcast::error::TryRecvError; + + async fn open() -> (tempfile::TempDir, AppDatabase) { + let directory = tempdir().unwrap(); + let path = directory.path().join("app_db.sqlite"); + let database = AppDatabase::new(path.to_str().unwrap(), None, false) + .await + .unwrap(); + database.run_migrations().await.unwrap(); + (directory, database) + } + + fn insert(key: &str) -> String { + format!("INSERT INTO app_metadata(key, value) VALUES('{key}', '1')") + } + + #[tokio::test] + async fn committed_writes_report_their_tables() { + let (_dir, database) = open().await; + let mut changes = database.subscribe(); + + database + .raw_execute(insert("hook"), Vec::new()) + .await + .unwrap(); + + let change = changes.try_recv().unwrap(); + assert!(change.tables.contains("app_metadata")); + } + + #[tokio::test] + async fn rolled_back_writes_report_nothing() { + let (_dir, database) = open().await; + let mut changes = database.subscribe(); + + database + .raw_execute("BEGIN".to_owned(), Vec::new()) + .await + .unwrap(); + database + .raw_execute(insert("discarded"), Vec::new()) + .await + .unwrap(); + database + .raw_execute("ROLLBACK".to_owned(), Vec::new()) + .await + .unwrap(); + + assert_eq!(changes.try_recv().unwrap_err(), TryRecvError::Empty); + } + + #[tokio::test] + async fn a_transaction_reports_every_table_once_on_commit() { + let (_dir, database) = open().await; + let mut changes = database.subscribe(); + + database + .raw_execute("BEGIN".to_owned(), Vec::new()) + .await + .unwrap(); + database + .raw_execute(insert("first"), Vec::new()) + .await + .unwrap(); + database + .raw_execute(insert("second"), Vec::new()) + .await + .unwrap(); + // Nothing may reach the UI before the transaction commits. + assert_eq!(changes.try_recv().unwrap_err(), TryRecvError::Empty); + + database + .raw_execute("COMMIT".to_owned(), Vec::new()) + .await + .unwrap(); + + let change = changes.try_recv().unwrap(); + assert_eq!(change.tables, BTreeSet::from(["app_metadata".to_owned()])); + assert_eq!(changes.try_recv().unwrap_err(), TryRecvError::Empty); + } +} diff --git a/rust/src/database/app/tables/message.rs b/rust/src/database/app/tables/message.rs index 21ee0100..0ead973b 100644 --- a/rust/src/database/app/tables/message.rs +++ b/rust/src/database/app/tables/message.rs @@ -48,11 +48,11 @@ impl Message { contact_id: i64, timestamp: i64, ) -> Result<()> { - let action_at = sqlx::query_scalar::<_, i64>( - "SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?", + let action_at = sqlx::query_scalar!( + r#"SELECT MAX(created_at, ?) AS "action_at!: i64" FROM messages WHERE message_id = ?"#, + timestamp, + message_id, ) - .bind(timestamp) - .bind(message_id) .fetch_optional(&mut **t) .await?; let Some(action_at) = action_at else { diff --git a/rust/src/database/app/tables/receipt.rs b/rust/src/database/app/tables/receipt.rs index 2d9e16bf..6889f428 100644 --- a/rust/src/database/app/tables/receipt.rs +++ b/rust/src/database/app/tables/receipt.rs @@ -20,12 +20,14 @@ pub struct Receipt { pub retry_count: i64, pub last_retry: Option, pub created_at: i64, + pub wake_receiver: i64, } pub struct NewReceipt<'a> { receipt_id: &'a str, contact_id: i64, message: &'a [u8], contact_will_send_receipt: bool, + wake_receiver: bool, } impl Receipt { @@ -149,6 +151,7 @@ impl<'a> NewReceipt<'a> { contact_id, message, contact_will_send_receipt: true, + wake_receiver: false, } } @@ -157,16 +160,22 @@ impl<'a> NewReceipt<'a> { self } + pub fn wake_receiver(mut self, val: bool) -> Self { + self.wake_receiver = val; + self + } + pub async fn insert(&self, transaction: &mut Transaction<'_, Sqlite>) -> Result<()> { sqlx::query!( r#" - INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt) - VALUES (?, ?, ?, ?) + INSERT INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt, wake_receiver) + VALUES (?, ?, ?, ?, ?) "#, self.receipt_id, self.contact_id, self.message, self.contact_will_send_receipt, + self.wake_receiver, ) .execute(&mut **transaction) .await?; @@ -177,13 +186,14 @@ impl<'a> NewReceipt<'a> { pub async fn insert_or_replace(&self, transaction: &mut Transaction<'_, Sqlite>) -> Result<()> { sqlx::query!( r#" - INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt) - VALUES (?, ?, ?, ?) + INSERT OR REPLACE INTO receipts(receipt_id, contact_id, message, contact_will_sends_receipt, wake_receiver) + VALUES (?, ?, ?, ?, ?) "#, self.receipt_id, self.contact_id, self.message, self.contact_will_send_receipt, + self.wake_receiver, ) .execute(&mut **transaction) .await?; diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index a838d50e..938f111b 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 = -1286774374; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1780439173; // Section: executor @@ -2964,6 +2964,49 @@ fn wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl( }, ) } +fn wire__crate__bridge__wrapper__app_database__rust_app_database_changes_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_app_database_changes", + 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_sink = + , flutter_rust_bridge::for_generated::SseCodec>>::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::app_database::RustAppDatabase::changes( + api_sink, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -4462,6 +4505,14 @@ impl SseDecode } } +impl SseDecode for StreamSink, flutter_rust_bridge::for_generated::SseCodec> { + // 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 StreamSink::deserialize(inner); + } +} + impl SseDecode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5433,6 +5484,7 @@ impl SseDecode for crate::user_config::UserConfig { let mut var_passwordLessRecovery = >::sse_decode(deserializer); let mut var_fcmToken = >::sse_decode(deserializer); + let mut var_lastFcmWakeupAt = >::sse_decode(deserializer); let mut var_currentSetupPage = >::sse_decode(deserializer); let mut var_skipSetupPages = ::sse_decode(deserializer); let mut var_hasZoomed = ::sse_decode(deserializer); @@ -5493,6 +5545,7 @@ impl SseDecode for crate::user_config::UserConfig { is_backup_enabled: var_isBackupEnabled, password_less_recovery: var_passwordLessRecovery, fcm_token: var_fcmToken, + last_fcm_wakeup_at: var_lastFcmWakeupAt, current_setup_page: var_currentSetupPage, skip_setup_pages: var_skipSetupPages, has_zoomed: var_hasZoomed, @@ -5598,45 +5651,46 @@ fn pde_ffi_dispatcher_primary_impl( 74 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), 75 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), 76 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -5650,7 +5704,7 @@ fn pde_ffi_dispatcher_sync_impl( // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { 18 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), - 111 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 112 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, @@ -6490,6 +6544,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { self.is_backup_enabled.into_into_dart().into_dart(), self.password_less_recovery.into_into_dart().into_dart(), self.fcm_token.into_into_dart().into_dart(), + self.last_fcm_wakeup_at.into_into_dart().into_dart(), self.current_setup_page.into_into_dart().into_dart(), self.skip_setup_pages.into_into_dart().into_dart(), self.has_zoomed.into_into_dart().into_dart(), @@ -6570,6 +6625,13 @@ impl SseEncode } } +impl SseEncode for StreamSink, flutter_rust_bridge::for_generated::SseCodec> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + impl SseEncode for String { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7385,6 +7447,7 @@ impl SseEncode for crate::user_config::UserConfig { serializer, ); >::sse_encode(self.fcm_token, serializer); + >::sse_encode(self.last_fcm_wakeup_at, serializer); >::sse_encode(self.current_setup_page, serializer); ::sse_encode(self.skip_setup_pages, serializer); ::sse_encode(self.has_zoomed, serializer); diff --git a/rust/src/keys/backup_password_keys.rs b/rust/src/keys/backup_password_keys.rs index f1188d65..65df43af 100644 --- a/rust/src/keys/backup_password_keys.rs +++ b/rust/src/keys/backup_password_keys.rs @@ -4,7 +4,7 @@ */ use crate::error::Result; -use scrypt::{Params, scrypt}; +use scrypt::{scrypt, Params}; use serde::{Deserialize, Serialize}; use zeroize::{Zeroize, ZeroizeOnDrop}; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d8c9daee..8da09e0a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -13,6 +13,7 @@ pub use error::TwonlyError; mod frb_generated; mod keys; pub mod log; +mod native_notifications; pub mod sealed_sender; mod secure_storage; pub mod services; diff --git a/rust/src/log.rs b/rust/src/log.rs index fa15d783..504065d0 100644 --- a/rust/src/log.rs +++ b/rust/src/log.rs @@ -3,7 +3,10 @@ * */ -use crate::bridge::callbacks::{get_callbacks, log::DartWriter}; +use crate::bridge::callbacks::{ + get_callbacks, + log::{set_dart_sink, DartWriter}, +}; use std::fmt; use std::path::Path; use std::sync::{Mutex, OnceLock}; @@ -17,9 +20,23 @@ use tracing_subscriber::{ EnvFilter, Registry, }; -static TRACING_GUARDS: OnceLock>> = OnceLock::new(); +type TracingGuards = (Option, WorkerGuard); +static TRACING_GUARDS: OnceLock>> = OnceLock::new(); static TRACING_INIT: OnceLock<()> = OnceLock::new(); +#[derive(Clone, Copy, Debug, Default)] +pub struct PlainFields; + +impl<'writer> FormatFields<'writer> for PlainFields { + fn format_fields( + &self, + writer: Writer<'writer>, + fields: R, + ) -> fmt::Result { + tracing_subscriber::fmt::format::DefaultFields::new().format_fields(writer, fields) + } +} + #[derive(Clone, Copy, Debug, Default)] pub struct ShortEventFormatter { ansi: bool, @@ -122,11 +139,13 @@ where pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: bool) { let _ = std::fs::create_dir_all(logs_dir); - let mut dart_sink = None; - + // Runs on *every* init, not just the first one: the subscriber is installed + // once per process, but the isolate it logs into can be replaced (hot + // restart, engine restart, background isolate). Re-registering here points + // the already-installed Dart layer at the isolate that is alive now. if is_dart_available { if let Ok(callbacks) = get_callbacks() { - dart_sink = Some((callbacks.logging.get_stream_sink)().await); + set_dart_sink((callbacks.logging.get_stream_sink)().await); } } @@ -135,7 +154,7 @@ pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: let stdout_layer = Layer::new() .with_writer(non_blocking_stdout) - .with_ansi(true) + .with_ansi(false) .event_format(ShortEventFormatter::ansi()); // let file_layer = Layer::new() @@ -151,23 +170,24 @@ pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: "debug,refinery_core=warn,refinery=warn" }; - let registry = Registry::default() + // DartWriter resolves the current sink per write, so the layer is + // installed unconditionally -- it is simply a no-op until an isolate + // registers one. PlainFields separates Dart span fields from stdout fields, + // preventing duplicate fields across layers and keeping Dart fields plain. + let dart_layer = Layer::new() + .with_writer(DartWriter) + .with_ansi(false) + .fmt_fields(PlainFields) + .event_format(ShortEventFormatter::plain()); + + let _ = Registry::default() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new(default_filter)), ) - .with(stdout_layer); - - if let Some(sink) = dart_sink { - let dart_writer = DartWriter { sink }; - let dart_layer = tracing_subscriber::fmt::Layer::new() - .with_writer(dart_writer) - .with_ansi(false) - .event_format(ShortEventFormatter::plain()); - let _ = registry.with(dart_layer).try_init(); - } else { - let _ = registry.try_init(); - } + .with(stdout_layer) + .with(dart_layer) + .try_init(); }); } @@ -191,11 +211,12 @@ fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) { }; let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout()); - if let Some(fg) = file_guard { - TRACING_GUARDS - .set(Mutex::new(Some((fg, stdout_guard)))) - .ok(); - } + // The stdout guard must outlive this function regardless of whether the + // file appender came up -- dropping it shuts the non-blocking writer thread + // down and silently swallows every log line. + TRACING_GUARDS + .set(Mutex::new(Some((file_guard, stdout_guard)))) + .ok(); (non_blocking_stdout, non_blocking_file) } diff --git a/rust/src/native_notifications.rs b/rust/src/native_notifications.rs new file mode 100644 index 00000000..2c861b37 --- /dev/null +++ b/rust/src/native_notifications.rs @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +//! Stable synchronous C entry points for native notification executables. +//! The ABI owns its Tokio runtime so neither Swift nor Kotlin needs Flutter or +//! flutter_rust_bridge to process a background wake-up. + +use crate::bridge::InitConfig; +use crate::context::Context; +use crate::services::notifications::{self, NotificationBatch, NotificationPresentation}; +use serde::Serialize; +use std::ffi::{c_char, CStr, CString}; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::{LazyLock, Mutex, MutexGuard}; + +static NOTIFICATION_WORKER: LazyLock> = LazyLock::new(|| Mutex::new(())); + +#[derive(Serialize)] +struct NativeNotificationResponse { + ok: bool, + batch: Option, + fallback: Option, + error: Option, +} + +fn response_json(response: NativeNotificationResponse) -> *mut c_char { + let json = serde_json::to_string(&response).unwrap_or_else(|error| { + format!( + r#"{{"ok":false,"batch":null,"fallback":null,"error":"serialization failed: {error}"}}"# + ) + }); + CString::new(json) + .expect("JSON serializers must escape interior NUL bytes") + .into_raw() +} + +unsafe fn required_string(pointer: *const c_char, name: &str) -> Result { + if pointer.is_null() { + return Err(format!("{name} is null")); + } + // SAFETY: Native callers promise a valid NUL-terminated string for the + // duration of this synchronous function call. + unsafe { CStr::from_ptr(pointer) } + .to_str() + .map(str::to_owned) + .map_err(|error| format!("{name} is not UTF-8: {error}")) +} + +fn runtime() -> Result { + tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build() + .map_err(|error| format!("could not create notification runtime: {error}")) +} + +fn worker_lock() -> MutexGuard<'static, ()> { + NOTIFICATION_WORKER + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +/// Processes an opaque FCM/APNs wake-up and returns a JSON-encoded +/// `NativeNotificationResponse`. The returned pointer must be released with +/// `twonly_notification_string_free`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn twonly_notification_process( + database_dir: *const c_char, + data_dir: *const c_char, + locale: *const c_char, + deadline_ms: u64, +) -> *mut c_char { + let result = catch_unwind(AssertUnwindSafe(|| { + let _worker = worker_lock(); + let database_dir = unsafe { required_string(database_dir, "database_dir") }?; + let data_dir = unsafe { required_string(data_dir, "data_dir") }?; + let locale = unsafe { required_string(locale, "locale") }?; + let runtime = runtime()?; + runtime + .block_on(notifications::process_wakeup( + InitConfig { + database_dir, + data_dir, + }, + &locale, + deadline_ms, + )) + .map_err(|error| error.to_string()) + })); + + match result { + Ok(Ok(batch)) => response_json(NativeNotificationResponse { + ok: true, + batch: Some(batch), + fallback: Some(notifications::fallback_presentation( + unsafe { required_string(locale, "locale") } + .as_deref() + .unwrap_or("en"), + )), + error: None, + }), + Ok(Err(error)) => response_json(NativeNotificationResponse { + ok: false, + batch: None, + fallback: Some(notifications::fallback_presentation( + unsafe { required_string(locale, "locale") } + .as_deref() + .unwrap_or("en"), + )), + error: Some(error), + }), + Err(_) => response_json(NativeNotificationResponse { + ok: false, + batch: None, + fallback: Some(notifications::fallback_presentation("en")), + error: Some("notification worker panicked".into()), + }), + } +} + +/// Marks successfully scheduled native events as delivered. `event_ids_json` +/// must be a JSON string array. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn twonly_notification_acknowledge( + event_ids_json: *const c_char, +) -> *mut c_char { + let result = catch_unwind(AssertUnwindSafe(|| { + let _worker = worker_lock(); + let event_ids_json = unsafe { required_string(event_ids_json, "event_ids_json") }?; + let event_ids: Vec = + serde_json::from_str(&event_ids_json).map_err(|error| error.to_string())?; + let ctx = Context::get_static() + .map_err(|error| error.to_string())? + .clone(); + runtime()? + .block_on(notifications::acknowledge_batch(&ctx, &event_ids)) + .map_err(|error| error.to_string()) + })); + + match result { + Ok(Ok(())) => response_json(NativeNotificationResponse { + ok: true, + batch: None, + fallback: None, + error: None, + }), + Ok(Err(error)) => response_json(NativeNotificationResponse { + ok: false, + batch: None, + fallback: None, + error: Some(error), + }), + Err(_) => response_json(NativeNotificationResponse { + ok: false, + batch: None, + fallback: None, + error: Some("notification acknowledgement panicked".into()), + }), + } +} + +/// Releases a string returned by a Twonly notification C entry point. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn twonly_notification_string_free(pointer: *mut c_char) { + if !pointer.is_null() { + // SAFETY: The pointer was allocated by `CString::into_raw` above and + // ownership is transferred back exactly once by the native caller. + drop(unsafe { CString::from_raw(pointer) }); + } +} + +#[cfg(target_os = "android")] +mod android_jni { + use super::*; + use jni::objects::{JClass, JString}; + use jni::sys::{jlong, jstring}; + use jni::JNIEnv; + + fn java_string(env: &mut JNIEnv<'_>, value: JString<'_>) -> Result { + env.get_string(&value) + .map(Into::into) + .map_err(|error| format!("invalid Java string: {error}")) + } + + fn return_string(env: &mut JNIEnv<'_>, value: String) -> jstring { + env.new_string(value) + .map(JString::into_raw) + .unwrap_or(std::ptr::null_mut()) + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_eu_twonly_notifications_NativeNotificationBridge_process( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + database_dir: JString<'_>, + data_dir: JString<'_>, + locale: JString<'_>, + deadline_ms: jlong, + ) -> jstring { + let result = (|| { + let database_dir = CString::new(java_string(&mut env, database_dir)?) + .map_err(|error| error.to_string())?; + let data_dir = CString::new(java_string(&mut env, data_dir)?) + .map_err(|error| error.to_string())?; + let locale = + CString::new(java_string(&mut env, locale)?).map_err(|error| error.to_string())?; + // SAFETY: Each CString remains alive for the synchronous ABI call. + let pointer = unsafe { + twonly_notification_process( + database_dir.as_ptr(), + data_dir.as_ptr(), + locale.as_ptr(), + deadline_ms.max(0) as u64, + ) + }; + if pointer.is_null() { + return Err("Rust notification worker returned null".into()); + } + // SAFETY: The C ABI returns a valid owned CString. + let json = unsafe { CStr::from_ptr(pointer) } + .to_string_lossy() + .into_owned(); + unsafe { twonly_notification_string_free(pointer) }; + Ok(json) + })(); + return_string( + &mut env, + result.unwrap_or_else(|error: String| { + format!( + r#"{{"ok":false,"batch":null,"fallback":null,"error":{}}}"#, + serde_json::to_string(&error).unwrap_or_else(|_| "null".into()) + ) + }), + ) + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_eu_twonly_notifications_NativeNotificationBridge_acknowledge( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + event_ids_json: JString<'_>, + ) -> jstring { + let result = (|| { + let event_ids_json = CString::new(java_string(&mut env, event_ids_json)?) + .map_err(|error| error.to_string())?; + let pointer = unsafe { twonly_notification_acknowledge(event_ids_json.as_ptr()) }; + if pointer.is_null() { + return Err("Rust notification acknowledgement returned null".into()); + } + let json = unsafe { CStr::from_ptr(pointer) } + .to_string_lossy() + .into_owned(); + unsafe { twonly_notification_string_free(pointer) }; + Ok(json) + })(); + return_string(&mut env, result.unwrap_or_else(|error: String| error)) + } + + #[unsafe(no_mangle)] + pub extern "system" fn Java_eu_twonly_notifications_NativeNotificationBridge_storeFcmToken( + mut env: JNIEnv<'_>, + _class: JClass<'_>, + database_dir: JString<'_>, + data_dir: JString<'_>, + token: JString<'_>, + ) -> jstring { + let result = (|| { + let _worker = worker_lock(); + let config = InitConfig { + database_dir: java_string(&mut env, database_dir)?, + data_dir: java_string(&mut env, data_dir)?, + }; + let token = java_string(&mut env, token)?; + runtime()? + .block_on(notifications::store_fcm_token(config, token)) + .map_err(|error| error.to_string()) + })(); + let response = match result { + Ok(()) => r#"{"ok":true}"#.to_owned(), + Err(error) => format!( + r#"{{"ok":false,"error":{}}}"#, + serde_json::to_string(&error).unwrap_or_else(|_| "null".into()) + ), + }; + return_string(&mut env, response) + } +} diff --git a/rust/src/sealed_sender.rs b/rust/src/sealed_sender.rs index 1e179d3d..02478b66 100644 --- a/rust/src/sealed_sender.rs +++ b/rust/src/sealed_sender.rs @@ -4,8 +4,8 @@ */ use chacha20poly1305::{ - KeyInit, XChaCha20Poly1305, XNonce, aead::{Aead, Payload}, + KeyInit, XChaCha20Poly1305, XNonce, }; use hkdf::Hkdf; use libsignal_protocol::{IdentityKeyPair, KeyPair, PublicKey}; @@ -319,7 +319,7 @@ fn derive_encryption_key( #[cfg(test)] mod tests { use super::*; - use rand::{SeedableRng, rngs::StdRng}; + use rand::{rngs::StdRng, SeedableRng}; fn test_message() -> proto::Message { proto::Message { diff --git a/rust/src/services/mediafiles.rs b/rust/src/services/mediafiles.rs index 12655fa5..39fcad99 100644 --- a/rust/src/services/mediafiles.rs +++ b/rust/src/services/mediafiles.rs @@ -12,12 +12,11 @@ use chacha20poly1305::aead::{AeadInPlace, KeyInit}; use chacha20poly1305::{ChaCha20Poly1305, Nonce, Tag}; use prost::Message as _; use sha2::{Digest, Sha256}; -use sqlx::{FromRow, Sqlite, Transaction}; +use sqlx::{Sqlite, Transaction}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -#[derive(FromRow)] struct DownloadMedia { media_id: String, media_type: String, @@ -27,7 +26,6 @@ struct DownloadMedia { encryption_nonce: Option>, } -#[derive(FromRow)] struct ReuploadTarget { message_id: String, sender_id: i64, @@ -177,12 +175,13 @@ impl MediaFileService { return Ok(()); } - let media = sqlx::query_as::<_, DownloadMedia>( + let media = sqlx::query_as!( + DownloadMedia, r#"SELECT media_id, type AS media_type, download_token, encryption_key, encryption_mac, encryption_nonce FROM media_files WHERE media_id = ?"#, + media_id, ) - .bind(media_id) .fetch_optional(&database.pool) .await? .ok_or_else(|| TwonlyError::Generic(format!("media {media_id} not found")))?; @@ -290,11 +289,12 @@ impl MediaFileService { .execute(&database.pool) .await?; - let targets = sqlx::query_as::<_, ReuploadTarget>( - r#"SELECT message_id, sender_id FROM messages + let targets = sqlx::query_as!( + ReuploadTarget, + r#"SELECT message_id, sender_id AS "sender_id!: i64" FROM messages WHERE media_id = ? AND opened_at IS NULL AND sender_id IS NOT NULL"#, + media_id, ) - .bind(media_id) .fetch_all(&database.pool) .await?; diff --git a/rust/src/services/messages.rs b/rust/src/services/messages.rs index cfd94628..e56529ce 100644 --- a/rust/src/services/messages.rs +++ b/rust/src/services/messages.rs @@ -303,7 +303,9 @@ impl MessageService { ) -> Result { let local_user_id = self .ctx - .key_manager.lock().await + .key_manager + .lock() + .await .user_id .ok_or_else(|| TwonlyError::Generic("local user ID is unavailable".into()))?; let group_id = Group::direct_chat_id(local_user_id, contact_id); @@ -438,17 +440,25 @@ impl MessageService { .call() .await?; let database = self.ctx.app_db.read().await.clone(); - for message_id in message_ids { + let mut transaction = database.pool.begin().await?; + for message_id in &message_ids { sqlx::query!( "UPDATE messages SET opened_at = ?, opened_by_all = ? WHERE message_id = ?", timestamp / 1000, timestamp / 1000, message_id ) - .execute(&database.pool) + .execute(&mut *transaction) .await?; } - database.notify_committed(["messages"]); + crate::services::notifications::clear_opened_messages( + &mut transaction, + &message_ids, + timestamp / 1000, + ) + .await?; + transaction.commit().await?; + database.notify_committed(["messages", "notification_outbox"]); Ok(()) } diff --git a/rust/src/services/mod.rs b/rust/src/services/mod.rs index 207ca8dd..faa45f9b 100644 --- a/rust/src/services/mod.rs +++ b/rust/src/services/mod.rs @@ -7,3 +7,4 @@ pub mod contacts; pub mod groups; pub mod mediafiles; pub mod messages; +pub mod notifications; diff --git a/rust/src/services/notifications.rs b/rust/src/services/notifications.rs new file mode 100644 index 00000000..e4bb6a5e --- /dev/null +++ b/rust/src/services/notifications.rs @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +use crate::api::proto::client::{self as proto, encrypted_content}; +use crate::api::runtime::ApiRuntime; +use crate::bridge::InitConfig; +use crate::context::{Context, RuntimeMode}; +use crate::error::Result; +use crate::user_config::UserConfig; +use crate::utils::{current_time, milliseconds_to_seconds}; +use serde::{Deserialize, Serialize}; +use sqlx::{Sqlite, Transaction}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock}; + +const MAX_BATCH_SIZE: i64 = 100; +const EN_ARB: &str = include_str!("../../../lib/src/localization/translations/en.arb"); +const DE_ARB: &str = include_str!("../../../lib/src/localization/translations/de.arb"); + +static EN_TRANSLATIONS: LazyLock> = + LazyLock::new(|| parse_arb(EN_ARB, "en")); +static DE_TRANSLATIONS: LazyLock> = + LazyLock::new(|| parse_arb(DE_ARB, "de")); + +pub(crate) fn should_wake_receiver(content: &proto::EncryptedContent) -> bool { + content.text_message.is_some() + || content.additional_data_message.is_some() + || content.group_create.is_some() + || content + .media + .as_ref() + .and_then(|value| encrypted_content::media::Type::try_from(value.r#type).ok()) + .is_some_and(|kind| kind != encrypted_content::media::Type::Reupload) + || content.reaction.as_ref().is_some_and(|value| !value.remove) + || content + .media_update + .as_ref() + .and_then(|value| encrypted_content::media_update::Type::try_from(value.r#type).ok()) + .is_some_and(|kind| { + matches!( + kind, + encrypted_content::media_update::Type::Stored + | encrypted_content::media_update::Type::Reopened + ) + }) + || content + .contact_request + .as_ref() + .and_then(|value| encrypted_content::contact_request::Type::try_from(value.r#type).ok()) + .is_some_and(|kind| { + matches!( + kind, + encrypted_content::contact_request::Type::Request + | encrypted_content::contact_request::Type::Accept + ) + }) +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct NotificationAddition { + pub event_id: String, + pub notification_id: String, + pub conversation_id: Option, + pub sender_id: i64, + pub sender_name: String, + pub title: String, + pub body: String, + pub conversation_name: Option, + pub is_group: bool, + pub message_id: Option, + pub kind: String, + pub content: Option, + pub created_at: i64, + pub avatar_path: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct NotificationBatch { + pub additions: Vec, + pub removals: Vec, + pub badge_count: i64, + pub completed: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct NotificationPresentation { + pub title: String, + pub body: String, +} + +pub fn fallback_presentation(locale: &str) -> NotificationPresentation { + NotificationPresentation { + title: translation(locale, "notificationCategoryMessageTitle").to_owned(), + body: translation(locale, "notificationCategoryMessageDesc").to_owned(), + } +} + +#[derive(Debug)] +struct NotificationDraft { + event_id: String, + notification_id: String, + conversation_id: Option, + sender_id: i64, + message_id: Option, + kind: &'static str, + content: Option, + created_at: i64, +} + +impl NotificationDraft { + async fn insert(self, transaction: &mut Transaction<'_, Sqlite>) -> Result<()> { + sqlx::query!( + r#" + INSERT OR IGNORE INTO notification_outbox( + event_id, notification_id, conversation_id, sender_id, + message_id, kind, content, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "#, + self.event_id, + self.notification_id, + self.conversation_id, + self.sender_id, + self.message_id, + self.kind, + self.content, + self.created_at, + ) + .execute(&mut **transaction) + .await?; + Ok(()) + } +} + +/// Records the user-visible consequence of an encrypted message in the same +/// transaction that commits that message. Transport retries are deduplicated +/// by the receipt-derived event ID. +pub(crate) async fn record_incoming_event( + transaction: &mut Transaction<'_, Sqlite>, + from_user_id: i64, + receipt_id: &str, + content: &proto::EncryptedContent, +) -> Result<()> { + let blocked = sqlx::query_scalar!( + "SELECT blocked FROM contacts WHERE user_id = ?", + from_user_id + ) + .fetch_optional(&mut **transaction) + .await? + .unwrap_or(0) + != 0; + if blocked { + return Ok(()); + } + + let conversation_id = content.group_id.clone(); + let now = current_time().timestamp(); + let draft = if let Some(message) = content.text_message.as_ref() { + Some(NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: message.sender_message_id.clone(), + conversation_id, + sender_id: from_user_id, + message_id: Some(message.sender_message_id.clone()), + kind: if message.quote_message_id.is_some() { + "response" + } else { + "text" + }, + content: None, + created_at: milliseconds_to_seconds(message.timestamp), + }) + } else if let Some(media) = content.media.as_ref() { + let media_type = encrypted_content::media::Type::try_from(media.r#type)?; + if media_type == encrypted_content::media::Type::Reupload { + None + } else { + let kind = match media_type { + _ if media.requires_authentication => "twonly", + encrypted_content::media::Type::Image => "image", + encrypted_content::media::Type::Video => "video", + encrypted_content::media::Type::Gif => "image", + encrypted_content::media::Type::Audio => "audio", + encrypted_content::media::Type::Reupload => unreachable!(), + }; + Some(NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: media.sender_message_id.clone(), + conversation_id, + sender_id: from_user_id, + message_id: Some(media.sender_message_id.clone()), + kind, + content: None, + created_at: milliseconds_to_seconds(media.timestamp), + }) + } + } else if let Some(message) = content.additional_data_message.as_ref() { + Some(NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: message.sender_message_id.clone(), + conversation_id, + sender_id: from_user_id, + message_id: Some(message.sender_message_id.clone()), + kind: "text", + content: None, + created_at: milliseconds_to_seconds(message.timestamp), + }) + } else if let Some(reaction) = content.reaction.as_ref().filter(|value| !value.remove) { + Some(NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: receipt_id.to_owned(), + conversation_id, + sender_id: from_user_id, + message_id: Some(reaction.target_message_id.clone()), + kind: "reaction", + content: Some(reaction.emoji.clone()), + created_at: now, + }) + } else if let Some(update) = content.media_update.as_ref() { + let update_type = encrypted_content::media_update::Type::try_from(update.r#type)?; + let kind = match update_type { + encrypted_content::media_update::Type::Stored => Some("stored_media"), + encrypted_content::media_update::Type::Reopened => Some("reopened_media"), + encrypted_content::media_update::Type::DecryptionError => None, + }; + kind.map(|kind| NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: receipt_id.to_owned(), + conversation_id, + sender_id: from_user_id, + message_id: Some(update.target_message_id.clone()), + kind, + content: None, + created_at: now, + }) + } else if let Some(request) = content.contact_request.as_ref() { + let request_type = encrypted_content::contact_request::Type::try_from(request.r#type)?; + let kind = match request_type { + encrypted_content::contact_request::Type::Request => Some("contact_request"), + encrypted_content::contact_request::Type::Accept => Some("accept_request"), + encrypted_content::contact_request::Type::Reject => None, + }; + kind.map(|kind| NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: receipt_id.to_owned(), + conversation_id, + sender_id: from_user_id, + message_id: None, + kind, + content: None, + created_at: now, + }) + } else if let Some(create) = content.group_create.as_ref() { + Some(NotificationDraft { + event_id: receipt_id.to_owned(), + notification_id: receipt_id.to_owned(), + conversation_id, + sender_id: from_user_id, + message_id: None, + kind: "added_to_group", + content: create.group_name.clone(), + created_at: now, + }) + } else { + None + }; + + if let Some(draft) = draft { + draft.insert(transaction).await?; + } + Ok(()) +} + +pub async fn pending_batch(ctx: &Arc, locale: &str) -> Result { + let database = ctx.app_db.read().await.clone(); + // A foreground chat can mark a message as opened before the native push + // worker gets around to rendering its durable outbox row. Clear those + // stale rows first so they neither produce an alert nor inflate the badge. + let cleared_at = current_time().timestamp(); + let cleared = sqlx::query( + r#" + UPDATE notification_outbox + SET cleared_at = ? + WHERE cleared_at IS NULL + AND kind IN ('text', 'response', 'image', 'video', 'audio', 'twonly') + AND EXISTS ( + SELECT 1 + FROM messages + WHERE messages.message_id = notification_outbox.message_id + AND messages.opened_at IS NOT NULL + ) + "#, + ) + .bind(cleared_at) + .execute(&database.pool) + .await?; + if cleared.rows_affected() != 0 { + database.notify_committed(["notification_outbox"]); + } + let rows = sqlx::query_as!( + PendingRow, + r#" + SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id, + COALESCE(c.display_name, c.username) AS "sender_name!: String", + c.avatar_svg_compressed, c.sender_profile_counter, + g.group_name AS conversation_name, + COALESCE(g.is_direct_chat, 0) AS "is_direct_chat!: i64", + n.message_id, n.kind, n.content, n.created_at, + target_message.type AS target_message_type, + target_media.type AS target_media_type + FROM notification_outbox n + JOIN contacts c ON c.user_id = n.sender_id + LEFT JOIN groups g ON g.group_id = n.conversation_id + LEFT JOIN messages target_message ON target_message.message_id = n.message_id + LEFT JOIN media_files target_media ON target_media.media_id = target_message.media_id + WHERE n.delivered_at IS NULL AND n.cleared_at IS NULL + ORDER BY n.created_at, n.event_id + LIMIT ? + "#, + MAX_BATCH_SIZE, + ) + .fetch_all(&database.pool) + .await?; + + let badge_count = sqlx::query_scalar!( + r#"SELECT COUNT(*) AS "count: i64" FROM notification_outbox WHERE cleared_at IS NULL"# + ) + .fetch_one(&database.pool) + .await?; + + let mut additions = Vec::with_capacity(rows.len()); + for row in rows { + let title = row.sender_name.clone(); + let body = localized_body(locale, &row); + let is_group = row.conversation_id.is_some() && row.is_direct_chat == 0; + let avatar_path = match notification_avatar_path( + ctx, + row.sender_id, + row.sender_profile_counter, + row.avatar_svg_compressed.as_deref(), + ) { + Ok(path) => path.map(|path| path.display().to_string()), + Err(error) => { + tracing::warn!( + sender_id = row.sender_id, + "failed to prepare notification avatar: {error}" + ); + None + } + }; + additions.push(NotificationAddition { + event_id: row.event_id, + notification_id: row.notification_id, + conversation_id: row.conversation_id, + sender_id: row.sender_id, + sender_name: row.sender_name, + title, + body, + conversation_name: row.conversation_name, + is_group, + message_id: row.message_id, + kind: row.kind, + content: row.content, + created_at: row.created_at, + avatar_path, + }); + } + + Ok(NotificationBatch { + additions, + removals: Vec::new(), + badge_count, + completed: true, + }) +} + +/// Runs the bounded native notification lifecycle. In a dedicated extension or +/// killed-app process this owns a short-lived background WebSocket. If Flutter +/// is already alive, it waits for the existing foreground connection to commit +/// the incoming message instead of opening a competing session. +pub async fn process_wakeup( + config: InitConfig, + locale: &str, + deadline_ms: u64, +) -> Result { + Context::init_notification(config).await?; + let ctx = Context::get_static()?.clone(); + + // The FCM health check used to be fed by the Dart background isolate, which + // no longer runs. Record the wake-up here so both platforms report it. + if let Err(error) = UserConfig::update(&ctx, |user| { + user.last_fcm_wakeup_at = Some(current_time().timestamp()); + }) { + tracing::warn!("could not record the FCM wake-up timestamp: {error}"); + } + + let deadline = std::time::Duration::from_millis(deadline_ms.clamp(1_000, 28_000)); + + let completed = if ctx.runtime_mode == RuntimeMode::Notification { + let generation = ctx.mailbox_generation(); + ApiRuntime::connect(&ctx).await?; + tokio::time::timeout(deadline, ctx.wait_for_mailbox_after(generation)) + .await + .is_ok() + } else { + let initial = pending_batch(&ctx, locale).await?; + if !initial.additions.is_empty() { + return Ok(initial); + } + let generation = ctx.incoming_generation(); + tokio::time::timeout(deadline, ctx.wait_for_incoming_after(generation)) + .await + .is_ok() + }; + + // Give concurrently acknowledged batches a small window to commit their + // durable notification rows before the native caller renders the result. + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let mut batch = pending_batch(&ctx, locale).await?; + batch.completed = completed; + + if ctx.runtime_mode == RuntimeMode::Notification { + ApiRuntime::close(&ctx).await?; + } + Ok(batch) +} + +/// Persists a token refresh from the native Android FCM callback. The normal +/// authenticated Rust lifecycle uploads it and clears `update_fcm_token`. +pub async fn store_fcm_token(config: InitConfig, token: String) -> Result<()> { + Context::init_notification(config).await?; + let ctx = Context::get_static()?; + UserConfig::update(ctx, |user| { + user.fcm_token = Some(token); + user.update_fcm_token = true; + })?; + Ok(()) +} + +pub async fn acknowledge_batch(ctx: &Arc, event_ids: &[String]) -> Result<()> { + if event_ids.is_empty() { + return Ok(()); + } + let database = ctx.app_db.read().await.clone(); + let mut transaction = database.pool.begin().await?; + let delivered_at = current_time().timestamp(); + for event_id in event_ids { + sqlx::query!( + "UPDATE notification_outbox SET delivered_at = ? WHERE event_id = ? AND delivered_at IS NULL", + delivered_at, + event_id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + database.notify_committed(["notification_outbox"]); + Ok(()) +} + +pub async fn clear_conversation(ctx: &Arc, conversation_id: &str) -> Result> { + let database = ctx.app_db.read().await.clone(); + let mut transaction = database.pool.begin().await?; + let notification_ids = sqlx::query_scalar!( + "SELECT notification_id FROM notification_outbox WHERE conversation_id = ? AND cleared_at IS NULL", + conversation_id, + ) + .fetch_all(&mut *transaction) + .await?; + let cleared_at = current_time().timestamp(); + sqlx::query!( + "UPDATE notification_outbox SET cleared_at = ? WHERE conversation_id = ? AND cleared_at IS NULL", + cleared_at, + conversation_id, + ) + .execute(&mut *transaction) + .await?; + transaction.commit().await?; + database.notify_committed(["notification_outbox"]); + Ok(notification_ids) +} + +/// Clears only notifications representing the messages that were actually +/// opened. Events that merely refer to the same message, such as reactions or +/// media-status updates, remain independent notifications. +pub(crate) async fn clear_opened_messages( + transaction: &mut Transaction<'_, Sqlite>, + message_ids: &[String], + cleared_at: i64, +) -> Result<()> { + for message_id in message_ids { + sqlx::query( + r#" + UPDATE notification_outbox + SET cleared_at = ? + WHERE message_id = ? + AND cleared_at IS NULL + AND kind IN ('text', 'response', 'image', 'video', 'audio', 'twonly') + "#, + ) + .bind(cleared_at) + .bind(message_id) + .execute(&mut **transaction) + .await?; + } + Ok(()) +} + +struct PendingRow { + event_id: String, + notification_id: String, + conversation_id: Option, + sender_id: i64, + sender_name: String, + avatar_svg_compressed: Option>, + sender_profile_counter: i64, + conversation_name: Option, + is_direct_chat: i64, + message_id: Option, + kind: String, + content: Option, + created_at: i64, + target_message_type: Option, + target_media_type: Option, +} + +fn parse_arb(source: &str, locale: &str) -> HashMap { + let values: HashMap = serde_json::from_str(source) + .unwrap_or_else(|error| panic!("bundled {locale}.arb is invalid: {error}")); + values + .into_iter() + .filter_map(|(key, value)| value.as_str().map(|value| (key, value.to_owned()))) + .collect() +} + +fn translation(locale: &str, key: &str) -> &'static str { + let language = locale + .split(['-', '_']) + .next() + .unwrap_or("en") + .to_ascii_lowercase(); + let translations = if language == "de" { + &*DE_TRANSLATIONS + } else { + &*EN_TRANSLATIONS + }; + translations + .get(key) + .or_else(|| EN_TRANSLATIONS.get(key)) + .map(String::as_str) + .unwrap_or_else(|| panic!("notification translation {key} is missing from en.arb")) +} + +fn localized_body(locale: &str, row: &PendingRow) -> String { + let in_group = if row.conversation_id.is_some() && row.is_direct_chat == 0 { + row.conversation_name + .as_deref() + .map(|name| format!(" {} {}", translation(locale, "notificationFillerIn"), name)) + .unwrap_or_default() + } else { + String::new() + }; + + let key = match row.kind.as_str() { + "text" => "notificationText", + "response" => "notificationResponse", + "twonly" => "notificationTwonly", + "video" => "notificationVideo", + "image" => "notificationImage", + "audio" => "notificationAudio", + "added_to_group" => "notificationAddedToGroup", + "contact_request" => "notificationContactRequest", + "accept_request" => "notificationAcceptRequest", + "stored_media" => "notificationStoredMediaFile", + "reopened_media" => "notificationReopenedMedia", + "reaction" => match ( + row.target_message_type.as_deref(), + row.target_media_type.as_deref(), + ) { + (Some("text"), _) => "notificationReactionToText", + (Some("media"), Some("video")) => "notificationReactionToVideo", + (Some("media"), Some("audio")) => "notificationReactionToAudio", + (Some("media"), Some("image")) => "notificationReactionToImage", + _ => "notificationReaction", + }, + _ => "notificationText", + }; + + translation(locale, key) + .replace("{inGroup}", &in_group) + .replace("{groupname}", row.content.as_deref().unwrap_or_default()) + .replace("{reaction}", row.content.as_deref().unwrap_or_default()) +} + +fn notification_avatar_path( + ctx: &Context, + sender_id: i64, + profile_counter: i64, + svg: Option<&[u8]>, +) -> Result> { + let Some(svg) = svg else { + return Ok(None); + }; + let directory = Path::new(&ctx.config.data_dir).join("notification_avatars"); + std::fs::create_dir_all(&directory)?; + let output = directory.join(format!("{sender_id}-{profile_counter}.png")); + if output.exists() { + return Ok(Some(output)); + } + + let options = resvg::usvg::Options::default(); + let tree = resvg::usvg::Tree::from_data(svg, &options).map_err(|error| { + crate::error::TwonlyError::Generic(format!("invalid avatar SVG: {error}")) + })?; + let original = tree.size(); + let max_dimension = original.width().max(original.height()); + let scale = (256.0 / max_dimension).min(1.0); + let width = (original.width() * scale).round().max(1.0) as u32; + let height = (original.height() * scale).round().max(1.0) as u32; + let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height) + .ok_or_else(|| crate::error::TwonlyError::Generic("invalid avatar dimensions".into()))?; + resvg::render( + &tree, + resvg::tiny_skia::Transform::from_scale(scale, scale), + &mut pixmap.as_mut(), + ); + let png = pixmap.encode_png().map_err(|error| { + crate::error::TwonlyError::Generic(format!("avatar PNG encoding failed: {error}")) + })?; + let temporary = directory.join(format!(".{sender_id}-{profile_counter}.tmp")); + std::fs::write(&temporary, png)?; + std::fs::rename(&temporary, &output)?; + Ok(Some(output)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pending_row(kind: &str) -> PendingRow { + PendingRow { + event_id: "event".into(), + notification_id: "notification".into(), + conversation_id: None, + sender_id: 7, + sender_name: "Alice".into(), + avatar_svg_compressed: None, + sender_profile_counter: 0, + conversation_name: None, + is_direct_chat: 1, + message_id: None, + kind: kind.into(), + content: None, + created_at: 0, + target_message_type: None, + target_media_type: None, + } + } + + #[test] + fn loads_notification_strings_from_arb_and_falls_back_to_english() { + let row = pending_row("contact_request"); + assert_eq!( + localized_body("de-DE", &row), + "möchte sich mit dir vernetzen." + ); + assert_eq!(localized_body("fr-FR", &row), "wants to connect with you."); + } + + #[test] + fn interpolates_group_and_reaction_placeholders() { + let mut group = pending_row("text"); + group.conversation_id = Some("group".into()); + group.conversation_name = Some("Friends".into()); + group.is_direct_chat = 0; + assert_eq!(localized_body("en", &group), "sent a message in Friends."); + + let mut reaction = pending_row("reaction"); + reaction.content = Some("🔥".into()); + reaction.target_message_type = Some("media".into()); + reaction.target_media_type = Some("audio".into()); + assert_eq!( + localized_body("de", &reaction), + "hat mit 🔥 auf deine Sprachnachricht reagiert." + ); + } +} diff --git a/rust/src/signal/engine.rs b/rust/src/signal/engine.rs index cb7e3b6f..95311f08 100644 --- a/rust/src/signal/engine.rs +++ b/rust/src/signal/engine.rs @@ -8,10 +8,10 @@ use crate::error::{Result, TwonlyError}; use crate::user_config::UserConfig; use chrono::{Duration, Utc}; use libsignal_protocol::{ - CiphertextMessageType, DeviceId, GenericSignedPreKey, IdentityKey, IdentityKeyPair, - IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle, PreKeyId, PreKeySignalMessage, - PreKeyStore, ProtocolAddress, PublicKey, SignalMessage, SignedPreKeyId, SignedPreKeyStore, - Timestamp, message_encrypt, process_prekey_bundle, + message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey, + IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle, + PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SignalMessage, + SignedPreKeyId, SignedPreKeyStore, Timestamp, }; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -183,21 +183,33 @@ impl RustSignalEngine { ) .fetch_one(&store.pool) .await?; - if id > 16_777_215 { 1 } else { id + 1 } + if id > 16_777_215 { + 1 + } else { + id + 1 + } }; let signed_pre_key_id: u32 = { let id = sqlx::query_scalar!( r#"SELECT COALESCE(MAX(signed_pre_key_id), 0) AS "id!: u32" FROM signal_signed_pre_keys"#, ).fetch_one(&store.pool).await?; - if id > 16_777_215 { 1 } else { id + 1 } + if id > 16_777_215 { + 1 + } else { + id + 1 + } }; let kyber_pre_key_id: u32 = { let id = sqlx::query_scalar!( r#"SELECT COALESCE(MAX(kyber_pre_key_id), 0) AS "id!: u32" FROM signal_kyber_pre_keys"#, ).fetch_one(&store.pool).await?; - if id > 16_777_215 { 1 } else { id + 1 } + if id > 16_777_215 { + 1 + } else { + id + 1 + } }; let pre_key_pair = libsignal_protocol::KeyPair::generate(&mut csprng); @@ -325,7 +337,11 @@ impl RustSignalEngine { r#"SELECT COALESCE(MAX(kyber_pre_key_id), 0) AS "id!: u32" FROM signal_kyber_pre_keys"#, ).fetch_one(&store.pool).await?; - if id > 16_777_215 { 1 } else { id } + if id > 16_777_215 { + 1 + } else { + id + } }; let mut pre_key_id: u32 = { @@ -335,7 +351,11 @@ impl RustSignalEngine { .fetch_one(&store.pool) .await?; - if id > 16_777_215 { 1 } else { id } + if id > 16_777_215 { + 1 + } else { + id + } }; for _ in 0..30 { diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index 806b811e..c6904fbd 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -321,6 +321,12 @@ pub struct UserConfig { pub password_less_recovery: Option, #[frb(non_final)] pub fcm_token: Option, + /// Unix seconds of the last opaque FCM/APNs wake-up that reached the native + /// notification worker. Recorded in Rust because Flutter is no longer + /// started for background delivery on either platform. + #[serde(default)] + #[frb(non_final)] + pub last_fcm_wakeup_at: Option, #[frb(non_final)] pub current_setup_page: Option, #[serde(default)] diff --git a/rust/src/user_discovery.rs b/rust/src/user_discovery.rs index 67f9ef0d..df5de504 100644 --- a/rust/src/user_discovery.rs +++ b/rust/src/user_discovery.rs @@ -100,7 +100,7 @@ impl UserDiscovery { if user.public_identity_key.as_deref() != Some(announcement.announced_public_key.as_slice()) { - tracing::error!( + tracing::warn!( user_id = announcement.announced_user_id, "server returned a different identity key for announced user" ); @@ -187,13 +187,13 @@ impl UserDiscovery { let database = ctx.app_db.read().await.clone(); let mut transaction = database.pool.begin().await?; self.initialize_or_update( - config.user_discovery_threshold, - user_id, - public_key, - config.user_discovery_share_promotion, - &mut transaction, - ) - .await?; + config.user_discovery_threshold, + user_id, + public_key, + config.user_discovery_share_promotion, + &mut transaction, + ) + .await?; transaction.commit().await?; database.notify_committed(["user_discovery_shares"]); Ok(()) @@ -459,7 +459,7 @@ impl UserDiscovery { ) -> Result<()> { for message in messages { let Ok(message) = UserDiscoveryMessage::decode(message.as_slice()) else { - tracing::error!("Could not parse the message. Continue to the next message..."); + tracing::warn!("Could not parse the message. Continue to the next message..."); continue; }; let Some(version) = message.version else { @@ -524,7 +524,7 @@ impl UserDiscovery { let old_message = UserDiscoveryMessage::decode(current_promotion.as_slice())?; let Some(old_promotion) = old_message.user_discovery_promotion else { - tracing::error!("A contact should only have a promotion message..."); + tracing::warn!("A contact should only have a promotion message..."); return Ok(()); }; @@ -682,7 +682,7 @@ impl UserDiscovery { tracing::info!("Got a user discovery announcement from {contact_id}."); if uda.threshold as usize != uda.verification_shares.len() + 1 { - tracing::error!( + tracing::warn!( "UDA contains to few shares to verify: {} != {} + 1.", uda.threshold, uda.verification_shares.len(), @@ -909,7 +909,7 @@ impl UserDiscovery { let asd = AnnouncementShareDecrypted::decode(secret.as_slice())?; if let Some(signed_data) = asd.signed_data { if udp.public_id != signed_data.public_id { - tracing::error!( + tracing::warn!( "Mismatch of the announced public id and the signed public id " ); return Ok(()); diff --git a/rust/tests/api.rs b/rust/tests/api.rs index eedcc010..9a66a888 100644 --- a/rust/tests/api.rs +++ b/rust/tests/api.rs @@ -1,17 +1,19 @@ -#[path = "api/tester.rs"] -mod tester; #[path = "api/contacts.rs"] mod contacts; #[path = "api/group_resilience.rs"] mod group_resilience; #[path = "api/media.rs"] mod media; +#[path = "api/notifications.rs"] +mod notifications; #[path = "api/recovery.rs"] mod recovery; #[path = "api/server_api.rs"] mod server_api; #[path = "api/session_recovery.rs"] mod session_recovery; +#[path = "api/tester.rs"] +mod tester; #[path = "api/user_discovery.rs"] mod user_discovery; @@ -161,6 +163,7 @@ async fn test_connect_to_dev_server() -> anyhow::Result<()> { tester_b .wait_for_text_message(&message_id, tester_a.user_id, "Initial text") .await?; + tester_a.wait_for_message_ack_by_server(&message_id).await?; // Verify draft was cleared { diff --git a/rust/tests/api/contacts.rs b/rust/tests/api/contacts.rs index 86240ff3..540bdefd 100644 --- a/rust/tests/api/contacts.rs +++ b/rust/tests/api/contacts.rs @@ -75,7 +75,10 @@ async fn test_unknown_sender_auto_contact_discovery() -> anyhow::Result<()> { ) .fetch_one(&signal_db_b.pool) .await?; - assert_eq!(identity_exists, 1, "signal identity must be recorded for unknown sender"); + assert_eq!( + identity_exists, 1, + "signal identity must be recorded for unknown sender" + ); Ok(()) } diff --git a/rust/tests/api/group_resilience.rs b/rust/tests/api/group_resilience.rs index 84f9998d..c5d4464f 100644 --- a/rust/tests/api/group_resilience.rs +++ b/rust/tests/api/group_resilience.rs @@ -84,7 +84,10 @@ async fn test_group_membership_error_healing() -> anyhow::Result<()> { // Wait a brief moment for group join to be acknowledged, then ensure queued receipts are retransmitted tokio::time::sleep(std::time::Duration::from_millis(500)).await; - let _ = rust_lib_twonly::api::messages::incoming::messages::retransmit_queued_receipts(&tester_a.context).await; + let _ = rust_lib_twonly::api::messages::incoming::messages::retransmit_queued_receipts( + &tester_a.context, + ) + .await; tester_b .wait_for_text_message(&msg_id, tester_a.user_id, "Message triggering heal") @@ -112,7 +115,10 @@ async fn test_add_hidden_contact() -> anyhow::Result<()> { .await?; assert_eq!(contact.username, tester_b.username); - assert_eq!(contact.deleted_by_user, 1, "hidden contact must have deleted_by_user=1"); + assert_eq!( + contact.deleted_by_user, 1, + "hidden contact must have deleted_by_user=1" + ); Ok(()) } @@ -165,8 +171,12 @@ async fn test_admin_and_non_admin_leave_group() -> anyhow::Result<()> { .await? }; - tester_b.wait_for_group_exists(&group_id, group_name).await?; - tester_c.wait_for_group_exists(&group_id, group_name).await?; + tester_b + .wait_for_group_exists(&group_id, group_name) + .await?; + tester_c + .wait_for_group_exists(&group_id, group_name) + .await?; // Fetch missing public key for tester_b { @@ -178,7 +188,9 @@ async fn test_admin_and_non_admin_leave_group() -> anyhow::Result<()> { ) .execute(&db_a.pool) .await?; - GroupService::new(&tester_a.context).fetch_missing_group_public_keys().await?; + GroupService::new(&tester_a.context) + .fetch_missing_group_public_keys() + .await?; tokio::time::sleep(std::time::Duration::from_millis(500)).await; } @@ -242,4 +254,3 @@ async fn test_admin_and_non_admin_leave_group() -> anyhow::Result<()> { Ok(()) } - diff --git a/rust/tests/api/media.rs b/rust/tests/api/media.rs index ca60540d..a97848ba 100644 --- a/rust/tests/api/media.rs +++ b/rust/tests/api/media.rs @@ -115,7 +115,10 @@ async fn test_media_lifecycle_actions_and_reupload() -> anyhow::Result<()> { } tokio::time::sleep(std::time::Duration::from_millis(100)).await; } - assert!(!media_id_on_b.is_empty(), "media_id must be generated on receiver"); + assert!( + !media_id_on_b.is_empty(), + "media_id must be generated on receiver" + ); tester_b .wait_for_media_download_state(&media_id_on_b, "pending") .await?; diff --git a/rust/tests/api/notifications.rs b/rust/tests/api/notifications.rs new file mode 100644 index 00000000..a16c7fee --- /dev/null +++ b/rust/tests/api/notifications.rs @@ -0,0 +1,502 @@ +//! Integration coverage for the native notification path. +//! +//! Messages travel through the real dev server, are decrypted and committed by +//! the normal incoming pipeline, and are then asserted through the exact +//! `services::notifications` API that the iOS Notification Service Extension +//! and the Android WorkManager job call. Nothing here writes outbox rows by +//! hand, so classification, deduplication, claiming and clearing are all +//! exercised end to end. + +use crate::Tester; +use rust_lib_twonly::bridge::api::ApiConnectionState; +use rust_lib_twonly::database::app::tables::Group; +use rust_lib_twonly::services::contacts::ContactService; +use rust_lib_twonly::services::groups::GroupService; +use rust_lib_twonly::services::messages::MessageService; + +async fn ready_tester() -> anyhow::Result { + let mut tester = Tester::new().await?; + tester.wait_until(ApiConnectionState::Connected).await?; + tester.register_and_authenticate().await?; + tester.wait_until(ApiConnectionState::Authenticated).await?; + Ok(tester) +} + +#[tokio::test] +async fn test_notification_outbox_end_to_end() -> anyhow::Result<()> { + let _ = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .with_ansi(true) + .event_format(rust_lib_twonly::log::ShortEventFormatter::ansi()) + .try_init(); + + let mut tester_a = ready_tester().await?; + let tester_b = ready_tester().await?; + tracing::info!( + a = tester_a.user_id, + b = tester_b.user_id, + "Notification testers are ready" + ); + + let group_id = Group::direct_chat_id(tester_a.user_id, tester_b.user_id); + + // + // Contact requests are user visible, so they must reach the outbox before + // any message has been exchanged. + // + { + ContactService::new(&tester_a.context) + .request_by_username(tester_b.username.clone(), true) + .await?; + + let request = tester_b + .wait_for_notification("contact_request", tester_a.user_id) + .await?; + assert_eq!(request.title, tester_a.username); + assert_eq!(request.body, "wants to connect with you."); + assert_eq!(request.sender_name, tester_a.username); + assert!(!request.is_group); + assert!(request.message_id.is_none()); + // No avatar has been shared yet, so the native layer must fall back to + // a notification without an image instead of failing. + assert!(request.avatar_path.is_none()); + + ContactService::new(&tester_b.context) + .accept_request(tester_a.user_id, true) + .await?; + let accepted = tester_a + .wait_for_notification("accept_request", tester_b.user_id) + .await?; + assert_eq!(accepted.body, "is now connected with you."); + + tester_a + .wait_for_contact_state(tester_b.user_id, true, false) + .await?; + } + + // + // Claiming a batch: acknowledgement removes rows from the pending set but + // leaves the badge alone, and repeating it is a no-op. Duplicate FCM + // deliveries rely on both properties. + // + { + let batch = tester_b.notification_batch("en").await?; + assert!(!batch.additions.is_empty()); + let badge_before = batch.badge_count; + assert_eq!( + badge_before, + batch.additions.len() as i64, + "every pending event counts toward the badge" + ); + + let event_ids: Vec = batch + .additions + .iter() + .map(|addition| addition.event_id.clone()) + .collect(); + tester_b.acknowledge_notifications(&event_ids).await?; + + let after = tester_b.notification_batch("en").await?; + assert!( + after.additions.is_empty(), + "acknowledged events must not be offered again" + ); + assert_eq!( + after.badge_count, badge_before, + "delivery is not the same as the user having read the message" + ); + + // Acknowledging the same batch twice happens whenever the native layer + // is killed between scheduling and acknowledging. + tester_b.acknowledge_notifications(&event_ids).await?; + assert!(tester_b + .notification_batch("en") + .await? + .additions + .is_empty()); + tester_b.acknowledge_notifications(&[]).await?; + } + + // + // A plain text message, and the stable identifiers the native layer needs + // to make retries idempotent. + // + let first_message_id = { + let message_id = MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "Notify me".into(), None) + .await?; + + let text = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + assert_eq!(text.body, "sent a message."); + assert_eq!(text.message_id.as_deref(), Some(message_id.as_str())); + assert_eq!( + text.notification_id, message_id, + "the notification id must be derived from the message so a redelivery replaces it" + ); + assert_eq!(text.conversation_id.as_deref(), Some(group_id.as_str())); + assert!( + !text.is_group, + "a direct chat must not be rendered as a group" + ); + + // The envelope produced exactly one outbox row; a redelivered receipt + // is ignored by the primary key. + assert_eq!( + tester_b.notification_rows_for_event(&text.event_id).await?, + 1 + ); + + tester_b + .acknowledge_notifications(&[text.event_id.clone()]) + .await?; + message_id + }; + + // + // If the foreground chat opens a message before (or after) the native + // worker runs, that message must disappear from the durable batch and the + // badge. Its stable notification id lets the platform also withdraw an + // alert that was already displayed. + // + { + let message_id = MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "Open before alert".into(), None) + .await?; + let notification = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + assert_eq!(notification.notification_id, message_id); + let badge_before = tester_b.notification_batch("en").await?.badge_count; + + MessageService::new(&tester_b.context) + .notify_opened(tester_a.user_id, vec![message_id.clone()]) + .await?; + + let after = tester_b.notification_batch("en").await?; + assert!( + after + .additions + .iter() + .all(|addition| addition.message_id.as_deref() != Some(message_id.as_str())), + "an opened message must not be rendered as a notification" + ); + assert_eq!( + after.badge_count, + badge_before - 1, + "opening a message must reduce the notification badge" + ); + } + + // + // Localization is owned by Rust: the same row renders in the caller's + // language, and an unsupported locale falls back to English. + // + { + MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "Zweite Nachricht".into(), None) + .await?; + let pending = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + + let german = tester_b.notification_batch("de-DE").await?; + let german = german + .additions + .iter() + .find(|addition| addition.event_id == pending.event_id) + .expect("the pending event is rendered in every locale"); + assert_eq!(german.body, "hat eine Nachricht gesendet."); + + let unsupported = tester_b.notification_batch("fr").await?; + let unsupported = unsupported + .additions + .iter() + .find(|addition| addition.event_id == pending.event_id) + .expect("an unsupported locale still yields a notification"); + assert_eq!(unsupported.body, "sent a message."); + + tester_b + .acknowledge_notifications(&[pending.event_id.clone()]) + .await?; + } + + // + // Once the sender has shared an avatar, Rust rasterizes it and hands the + // native layer a real file instead of a callback into Flutter. + // + { + tester_a.update_profile( + None, + Some("Alice Notify".into()), + Some("".into()), + )?; + MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "Now with avatar".into(), None) + .await?; + tester_b + .wait_for_contact_avatar_exists(tester_a.user_id) + .await?; + tester_b + .wait_for_contact_display_name(tester_a.user_id, "Alice Notify") + .await?; + + // The avatar is resolved when the batch is read, so it also decorates + // events that were recorded before the profile arrived. + let mut with_avatar = None; + for _ in 0..100 { + let batch = tester_b.notification_batch("en").await?; + if let Some(addition) = batch + .additions + .into_iter() + .find(|addition| addition.sender_id == tester_a.user_id) + { + if addition.avatar_path.is_some() { + with_avatar = Some(addition); + break; + } + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + let with_avatar = with_avatar.expect("a shared avatar must reach the notification batch"); + assert_eq!(with_avatar.title, "Alice Notify"); + let avatar_path = with_avatar.avatar_path.clone().unwrap(); + assert!( + std::path::Path::new(&avatar_path).is_file(), + "the rendered avatar must exist on disk at {avatar_path}" + ); + + tester_b + .acknowledge_notifications(&[with_avatar.event_id.clone()]) + .await?; + } + + // + // Classification: a quoted reply and a reaction must not read like a plain + // message, and the reaction body carries the emoji. + // + { + MessageService::new(&tester_a.context) + .insert_and_send_text( + group_id.clone(), + "Quoting you".into(), + Some(first_message_id.clone()), + ) + .await?; + let reply = tester_b + .wait_for_notification("response", tester_a.user_id) + .await?; + assert_eq!(reply.body, "has responded."); + tester_b + .acknowledge_notifications(&[reply.event_id.clone()]) + .await?; + + // B owns a message that A can react to. + let owned_by_b = MessageService::new(&tester_b.context) + .insert_and_send_text(group_id.clone(), "React to this".into(), None) + .await?; + tester_a + .wait_for_text_message(&owned_by_b, tester_b.user_id, "React to this") + .await?; + tester_a + .acknowledge_notifications( + &tester_a + .notification_batch("en") + .await? + .additions + .iter() + .map(|addition| addition.event_id.clone()) + .collect::>(), + ) + .await?; + + MessageService::new(&tester_a.context) + .react(group_id.clone(), owned_by_b.clone(), "👍".into(), false) + .await?; + let reaction = tester_b + .wait_for_notification("reaction", tester_a.user_id) + .await?; + assert_eq!(reaction.body, "has reacted with 👍 to your message."); + assert_eq!(reaction.message_id.as_deref(), Some(owned_by_b.as_str())); + tester_b + .acknowledge_notifications(&[reaction.event_id.clone()]) + .await?; + + // Removing a reaction is not user visible and must stay silent. + MessageService::new(&tester_a.context) + .react(group_id.clone(), owned_by_b.clone(), "👍".into(), true) + .await?; + tester_b + .wait_for_reaction_deleted(&owned_by_b, tester_a.user_id, "👍") + .await?; + MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "After the removal".into(), None) + .await?; + let next = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + let pending_kinds: Vec = tester_b + .notification_batch("en") + .await? + .additions + .into_iter() + .map(|addition| addition.kind) + .collect(); + assert!( + !pending_kinds.contains(&"reaction".to_owned()), + "removing a reaction must not raise a notification, got {pending_kinds:?}" + ); + tester_b + .acknowledge_notifications(&[next.event_id.clone()]) + .await?; + } + + // + // Group messages carry the conversation name so the native layer can render + // "sent a message in ". + // + let group_conversation_id = { + let group_name = "Notify Group"; + GroupService::new(&tester_a.context) + .create_group(group_name.into(), vec![tester_b.user_id]) + .await?; + let group_conversation_id = { + let database = tester_a.context.app_db.read().await.clone(); + sqlx::query_scalar!( + "SELECT group_id FROM groups WHERE is_direct_chat = 0 ORDER BY rowid DESC LIMIT 1" + ) + .fetch_one(&database.pool) + .await? + }; + tester_b + .wait_for_group_exists(&group_conversation_id, group_name) + .await?; + + let added = tester_b + .wait_for_notification("added_to_group", tester_a.user_id) + .await?; + assert_eq!(added.body, format!("has added you to \"{group_name}\"")); + + MessageService::new(&tester_a.context) + .insert_and_send_text(group_conversation_id.clone(), "Hello group".into(), None) + .await?; + let group_text = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + assert_eq!(group_text.body, format!("sent a message in {group_name}.")); + assert!(group_text.is_group); + assert_eq!( + group_text.conversation_id.as_deref(), + Some(group_conversation_id.as_str()) + ); + assert_eq!(group_text.conversation_name.as_deref(), Some(group_name)); + + group_conversation_id + }; + + // + // Opening a conversation clears it: the native layer is told which + // notifications to withdraw, and the badge drops. + // + { + let before = tester_b.notification_batch("en").await?; + let group_events: Vec = before + .additions + .iter() + .filter(|addition| addition.conversation_id.as_deref() == Some(&group_conversation_id)) + .map(|addition| addition.notification_id.clone()) + .collect(); + assert!( + !group_events.is_empty(), + "the group conversation must have pending notifications to clear" + ); + + let removed = tester_b + .clear_notification_conversation(&group_conversation_id) + .await?; + for notification_id in &group_events { + assert!( + removed.contains(notification_id), + "cleared conversation must report {notification_id} for withdrawal" + ); + } + + let after = tester_b.notification_batch("en").await?; + assert!( + after + .additions + .iter() + .all(|addition| addition.conversation_id.as_deref() + != Some(&group_conversation_id)), + "a cleared conversation must not be offered again" + ); + assert_eq!( + after.badge_count, + before.badge_count - removed.len() as i64, + "clearing a conversation must reduce the badge" + ); + + // Clearing twice must not report the same notifications again. + assert!(tester_b + .clear_notification_conversation(&group_conversation_id) + .await? + .is_empty()); + } + + // + // A blocked contact is still decrypted and committed, but must never + // produce a notification. + // + { + let acknowledge: Vec = tester_b + .notification_batch("en") + .await? + .additions + .iter() + .map(|addition| addition.event_id.clone()) + .collect(); + tester_b.acknowledge_notifications(&acknowledge).await?; + let badge_before = tester_b.notification_batch("en").await?.badge_count; + + tester_b.set_contact_blocked(tester_a.user_id, true).await?; + + let blocked_message_id = MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "You blocked me".into(), None) + .await?; + tester_b + .wait_for_text_message(&blocked_message_id, tester_a.user_id, "You blocked me") + .await?; + + assert_eq!( + tester_b + .notification_rows_for_message(&blocked_message_id) + .await?, + 0, + "a blocked contact must not reach the notification outbox" + ); + let after = tester_b.notification_batch("en").await?; + assert!(after.additions.is_empty()); + assert_eq!(after.badge_count, badge_before); + + // Unblocking restores notifications for later messages. + tester_b + .set_contact_blocked(tester_a.user_id, false) + .await?; + let unblocked_message_id = MessageService::new(&tester_a.context) + .insert_and_send_text(group_id.clone(), "Unblocked again".into(), None) + .await?; + let unblocked = tester_b + .wait_for_notification("text", tester_a.user_id) + .await?; + assert_eq!( + unblocked.message_id.as_deref(), + Some(unblocked_message_id.as_str()) + ); + } + + Ok(()) +} diff --git a/rust/tests/api/server_api.rs b/rust/tests/api/server_api.rs index 727d46b0..e449677b 100644 --- a/rust/tests/api/server_api.rs +++ b/rust/tests/api/server_api.rs @@ -31,12 +31,12 @@ async fn test_server_account_and_user_endpoints() -> anyhow::Result<()> { // 2. get_user_id_from_username (handshake endpoint) { let tester_handshake = Tester::new().await?; - tester_handshake.wait_until(ApiConnectionState::Connected).await?; - let user_id = Server::get_user_id_from_username( - &tester_handshake.context, - tester_b.username.clone(), - ) - .await?; + tester_handshake + .wait_until(ApiConnectionState::Connected) + .await?; + let user_id = + Server::get_user_id_from_username(&tester_handshake.context, tester_b.username.clone()) + .await?; match user_id { ServerResult::Ok(id) => assert_eq!(id, tester_b.user_id), ServerResult::ErrorCode(code) => { diff --git a/rust/tests/api/tester.rs b/rust/tests/api/tester.rs index f20fdf3b..29081b3d 100644 --- a/rust/tests/api/tester.rs +++ b/rust/tests/api/tester.rs @@ -3,10 +3,11 @@ use rand::SeedableRng; use rust_lib_twonly::api::{ApiRuntime, Server}; use rust_lib_twonly::bridge::api::{ApiConnectionState, ServerResult}; use rust_lib_twonly::context::Context; +use rust_lib_twonly::services::notifications::{self, NotificationAddition, NotificationBatch}; use sha2::{Digest, Sha256}; use std::sync::Arc; use tempfile::TempDir; -use tokio::time::{Duration, sleep}; +use tokio::time::{sleep, Duration}; pub(crate) struct Tester { pub context: Arc, @@ -197,6 +198,26 @@ impl Tester { )) } + pub async fn wait_for_message_ack_by_server(&self, message_id: &str) -> anyhow::Result<()> { + for _ in 0..100 { + let database = self.context.app_db.read().await.clone(); + let acknowledged_at = sqlx::query_scalar::<_, Option>( + "SELECT ack_by_server FROM messages WHERE message_id = ?", + ) + .bind(message_id) + .fetch_optional(&database.pool) + .await? + .flatten(); + if acknowledged_at.is_some() { + return Ok(()); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "message {message_id} was not acknowledged by the server" + )) + } + pub async fn wait_for_reaction( &self, message_id: &str, @@ -491,6 +512,85 @@ impl Tester { Err(anyhow::anyhow!("contact {user_id} did not receive avatar")) } + /// Reads the receiver's notification outbox through the very API the iOS + /// Notification Service Extension and the Android worker call. + pub async fn notification_batch(&self, locale: &str) -> anyhow::Result { + Ok(notifications::pending_batch(&self.context, locale).await?) + } + + /// Waits until an undelivered notification of `kind` from `sender_id` is + /// pending, and returns it. Incoming messages are committed asynchronously, + /// so every notification assertion has to poll. + pub async fn wait_for_notification( + &self, + kind: &str, + sender_id: i64, + ) -> anyhow::Result { + for _ in 0..100 { + let batch = self.notification_batch(&self.lang_code).await?; + if let Some(addition) = batch + .additions + .into_iter() + .find(|addition| addition.kind == kind && addition.sender_id == sender_id) + { + return Ok(addition); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "no pending {kind} notification from {sender_id} arrived" + )) + } + + /// Marks pending notifications as delivered, mirroring what the native + /// layer does once it has actually scheduled them. + pub async fn acknowledge_notifications(&self, event_ids: &[String]) -> anyhow::Result<()> { + Ok(notifications::acknowledge_batch(&self.context, event_ids).await?) + } + + pub async fn clear_notification_conversation( + &self, + conversation_id: &str, + ) -> anyhow::Result> { + Ok(notifications::clear_conversation(&self.context, conversation_id).await?) + } + + /// Number of rows the outbox holds for one receipt, used to prove that a + /// redelivered envelope cannot notify twice. + pub async fn notification_rows_for_event(&self, event_id: &str) -> anyhow::Result { + let database = self.context.app_db.read().await.clone(); + Ok(sqlx::query_scalar!( + "SELECT COUNT(*) FROM notification_outbox WHERE event_id = ?", + event_id + ) + .fetch_one(&database.pool) + .await?) + } + + pub async fn notification_rows_for_message(&self, message_id: &str) -> anyhow::Result { + let database = self.context.app_db.read().await.clone(); + Ok(sqlx::query_scalar!( + "SELECT COUNT(*) FROM notification_outbox WHERE message_id = ?", + message_id + ) + .fetch_one(&database.pool) + .await?) + } + + pub async fn set_contact_blocked(&self, user_id: i64, blocked: bool) -> anyhow::Result<()> { + let database = self.context.app_db.read().await.clone(); + let blocked = i64::from(blocked); + sqlx::query!( + "UPDATE contacts SET blocked = ? WHERE user_id = ?", + blocked, + user_id + ) + .execute(&database.pool) + .await?; + database.notify_committed(["contacts"]); + Ok(()) + } + pub async fn wait_for_quoted_text_message( &self, message_id: &str, diff --git a/rust_builder/ios/Classes/TwonlyNotifications.h b/rust_builder/ios/Classes/TwonlyNotifications.h new file mode 100644 index 00000000..c40a441f --- /dev/null +++ b/rust_builder/ios/Classes/TwonlyNotifications.h @@ -0,0 +1,25 @@ +#ifndef TWONLY_NOTIFICATIONS_H +#define TWONLY_NOTIFICATIONS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +char *twonly_notification_process( + const char *database_dir, + const char *data_dir, + const char *locale, + uint64_t deadline_ms +); + +char *twonly_notification_acknowledge(const char *event_ids_json); + +void twonly_notification_string_free(char *pointer); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/rust_builder/ios/rust_lib_twonly.podspec b/rust_builder/ios/rust_lib_twonly.podspec index 50e8f7ce..2a03bbe9 100644 --- a/rust_builder/ios/rust_lib_twonly.podspec +++ b/rust_builder/ios/rust_lib_twonly.podspec @@ -19,7 +19,6 @@ A new Flutter FFI plugin project. # `../src/*` so that the C sources can be shared among all target platforms. s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.dependency 'Flutter' s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. @@ -42,4 +41,4 @@ A new Flutter FFI plugin project. 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_twonly.a', } -end \ No newline at end of file +end diff --git a/scripts/generate_proto.sh b/scripts/generate_proto.sh index 52309111..6f976095 100755 --- a/scripts/generate_proto.sh +++ b/scripts/generate_proto.sh @@ -21,8 +21,7 @@ protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "passwordless_reco mkdir "$GENERATED_DIR/user_discovery/" &>/dev/null protoc --proto_path="./rust/src/user_discovery/" --dart_out="$GENERATED_DIR/user_discovery/" "types.proto" -protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" -protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" +protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "http_requests.proto" # Definitions for the Server API diff --git a/test/callbacks/logging_callbacks_test.dart b/test/callbacks/logging_callbacks_test.dart new file mode 100644 index 00000000..f3c19703 --- /dev/null +++ b/test/callbacks/logging_callbacks_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/src/callbacks/logging.callbacks.dart'; + +void main() { + group('LoggingCallbacks ANSI stripping', () { + test('removes raw ANSI color escape sequences', () { + LogRecord? captured; + final sub = Logger.root.onRecord.listen((record) { + captured = record; + }); + + const rawAnsiLog = + '12:34:56 INFO dir/test.rs:42 \x1b[3mreceipt_id\x1b[0m\x1b[2m=\x1b[0m"d9891084-0f7c-4f30-958d-d8619df5a91c" Handling incoming message: FlameSync'; + + LoggingCallbacks.handleRustLog(rawAnsiLog); + + expect(captured, isNotNull); + expect(captured!.loggerName, 'dir/test.rs:42'); + expect( + captured!.message, + 'receipt_id="d9891084-0f7c-4f30-958d-d8619df5a91c" Handling incoming message: FlameSync', + ); + + sub.cancel(); + }); + + test('removes escaped caret ANSI notation', () { + LogRecord? captured; + final sub = Logger.root.onRecord.listen((record) { + captured = record; + }); + + const caretLog = + r'12:34:56 INFO dir/test.rs:42 \^[[3mreceipt_id\^[[0m\^[[2m=\^[[0m"d9891084" \^[[3mkind\^[[0m\^[[2m=\^[[0m"FlameSync" Handling incoming message'; + + LoggingCallbacks.handleRustLog(caretLog); + + expect(captured, isNotNull); + expect( + captured!.message, + 'receipt_id="d9891084" kind="FlameSync" Handling incoming message', + ); + + sub.cancel(); + }); + }); +}