From aad97bdd9e698cc012e5b2d954aa4d2de4dfa971 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Wed, 1 Jul 2026 20:27:05 +0200 Subject: [PATCH] Add deterministic id on MLS related models --- .../1.json | 32 +- .../compose/database/dao/ChatMessageDao.kt | 3 +- .../database/dao/GiftWrapPayloadDao.kt | 2 +- .../database/dao/MarmotKeyPackageBundleDao.kt | 4 +- .../at/torch/compose/database/dao/NostrDao.kt | 586 +++++++++++------- .../compose/database/model/ChatMessage.kt | 3 +- .../torch/compose/database/model/ChatRoom.kt | 2 +- .../compose/database/model/GiftWrapPayload.kt | 7 +- .../compose/database/model/GiftWrapSeal.kt | 1 + .../database/model/MarmotKeyPackage.kt | 4 +- .../database/model/MarmotKeyPackageBundle.kt | 50 +- .../compose/database/model/NostrEvent.kt | 1 + .../repository/DatabaseChatRepository.kt | 22 +- .../repository/DatabaseMarmotRepository.kt | 159 ++++- .../repository/DatabaseNostrRepository.kt | 109 +--- .../MarmotMissingKeyPackageBundleException.kt | 4 + ...rmotNoMatchingKeyPackageBundleException.kt | 4 - ...eEventMissingKeyPackageEventIdException.kt | 4 + .../MarmotWelcomeMissingGroupIdException.kt | 4 + .../compose/repository/MarmotRepository.kt | 3 - .../compose/repository/NostrRepository.kt | 5 +- .../ui/composable/ChatRoomDetailScreen.kt | 2 +- .../ui/composable/navigation/TorchNavHost.kt | 10 +- .../ui/view/model/CreateProfileViewModel.kt | 4 +- .../compose/ui/view/model/NotaryViewModel.kt | 2 +- .../ui/view/model/SynchronizationViewModel.kt | 1 + gradle/libs.versions.toml | 2 +- 27 files changed, 633 insertions(+), 397 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotMissingKeyPackageBundleException.kt delete mode 100644 composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotNoMatchingKeyPackageBundleException.kt create mode 100644 composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeEventMissingKeyPackageEventIdException.kt create mode 100644 composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeMissingGroupIdException.kt diff --git a/composeApp/schemas/at.torch.compose.database.TorchDatabase/1.json b/composeApp/schemas/at.torch.compose.database.TorchDatabase/1.json index 6d08baaf..7c009c84 100644 --- a/composeApp/schemas/at.torch.compose.database.TorchDatabase/1.json +++ b/composeApp/schemas/at.torch.compose.database.TorchDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "f33937ebefe679fe03b71b3930472139", + "identityHash": "8dd191b21e518a19f39b50a5f5b4a432", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -282,7 +282,7 @@ }, { "tableName": "ChatMessage", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `giftWrapPayloadId` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `giftWrapPayloadId` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -305,7 +305,7 @@ { "fieldPath": "giftWrapPayloadId", "columnName": "giftWrapPayloadId", - "affinity": "INTEGER", + "affinity": "TEXT", "notNull": true }, { @@ -608,7 +608,7 @@ }, { "tableName": "ChatRoom", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `mlsGroupState` TEXT, `initialGiftWrapPayloadId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `mlsGroupState` TEXT, `initialGiftWrapPayloadId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -635,7 +635,7 @@ { "fieldPath": "initialGiftWrapPayloadId", "columnName": "initialGiftWrapPayloadId", - "affinity": "INTEGER" + "affinity": "TEXT" }, { "fieldPath": "createdAt", @@ -893,12 +893,12 @@ }, { "tableName": "GiftWrapPayload", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", "columnName": "id", - "affinity": "INTEGER", + "affinity": "TEXT", "notNull": true }, { @@ -971,7 +971,7 @@ } ], "primaryKey": { - "autoGenerate": true, + "autoGenerate": false, "columnNames": [ "id" ] @@ -1117,12 +1117,12 @@ }, { "tableName": "MarmotKeyPackage", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`publicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`publicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", "columnName": "id", - "affinity": "INTEGER", + "affinity": "TEXT", "notNull": true }, { @@ -1167,7 +1167,7 @@ } ], "primaryKey": { - "autoGenerate": true, + "autoGenerate": false, "columnNames": [ "id" ] @@ -1188,12 +1188,12 @@ }, { "tableName": "MarmotKeyPackageBundle", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` TEXT NOT NULL, `ncryptsecInitPrivateKey` TEXT NOT NULL, `ncryptsecEncryptionPrivateKey` TEXT NOT NULL, `ncryptsecSignaturePrivateKey` TEXT NOT NULL, `consumed` INTEGER NOT NULL, `rotated` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `tlsEncodedMarmotKeyPackage` BLOB NOT NULL, `ncryptsecInitPrivateKey` TEXT NOT NULL, `ncryptsecEncryptionPrivateKey` TEXT NOT NULL, `ncryptsecSignaturePrivateKey` TEXT NOT NULL, `consumed` INTEGER NOT NULL, `rotated` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", "columnName": "id", - "affinity": "INTEGER", + "affinity": "TEXT", "notNull": true }, { @@ -1205,7 +1205,7 @@ { "fieldPath": "tlsEncodedMarmotKeyPackage", "columnName": "tlsEncodedMarmotKeyPackage", - "affinity": "TEXT", + "affinity": "BLOB", "notNull": true }, { @@ -1268,7 +1268,7 @@ } ], "primaryKey": { - "autoGenerate": true, + "autoGenerate": false, "columnNames": [ "id" ] @@ -3044,7 +3044,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f33937ebefe679fe03b71b3930472139')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8dd191b21e518a19f39b50a5f5b4a432')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/ChatMessageDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/ChatMessageDao.kt index 1df890ac..78e83f68 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/ChatMessageDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/ChatMessageDao.kt @@ -4,6 +4,7 @@ import androidx.room3.Dao import androidx.room3.Query import androidx.room3.Transaction import androidx.room3.Upsert +import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.flow.Flow @Dao @@ -17,7 +18,7 @@ interface ChatMessageDao { fun getChatMessagesByChatRoomId(chatRoomId: String): List @Query("SELECT * FROM ChatMessage WHERE giftWrapPayloadId = :giftWrapPayloadId ORDER BY createdAt DESC") - fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: Long): at.torch.compose.database.model.ChatMessage? + fun getChatMessagesByGiftWrapPayloadId(giftWrapPayloadId: HexKey): at.torch.compose.database.model.ChatMessage? @Upsert suspend fun upsert(chatMessage: at.torch.compose.database.model.ChatMessage): Long diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/GiftWrapPayloadDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/GiftWrapPayloadDao.kt index 2723d5e0..d2a39873 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/GiftWrapPayloadDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/GiftWrapPayloadDao.kt @@ -14,5 +14,5 @@ interface GiftWrapPayloadDao { fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow @Upsert - suspend fun upsert(giftWrapPayload: at.torch.compose.database.model.GiftWrapPayload): Long + suspend fun upsert(giftWrapPayload: at.torch.compose.database.model.GiftWrapPayload) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotKeyPackageBundleDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotKeyPackageBundleDao.kt index 02f3bf65..93c91f77 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotKeyPackageBundleDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotKeyPackageBundleDao.kt @@ -7,8 +7,8 @@ import at.torch.compose.database.model.MarmotKeyPackageBundle @Dao interface MarmotKeyPackageBundleDao { - @Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :keyPackageEventId ORDER BY createdAt DESC") - fun getMarmotKeyPackageBundleByKeyPackageEventIdId(keyPackageEventId: String): MarmotKeyPackageBundle? + @Query("SELECT * FROM MarmotKeyPackageBundle WHERE id = :id ORDER BY createdAt DESC") + fun getMarmotKeyPackageBundleById(id: String): MarmotKeyPackageBundle? @Upsert suspend fun upsert(marmotKeyPackageBundle: MarmotKeyPackageBundle) diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/NostrDao.kt index 8c8ad47e..29d7f5ba 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/NostrDao.kt @@ -18,10 +18,8 @@ import at.torch.compose.database.model.types.SynchronizationFilter import at.torch.compose.exceptions.GiftWrapImpersonationException import at.torch.compose.exceptions.GiftWrapSealDecryptionException import at.torch.compose.exceptions.GiftWrapUnsealException -import at.torch.compose.exceptions.MarmotMissingKeyPackageEventIdException -import at.torch.compose.exceptions.MarmotMissingNostrGroupDataExtension -import at.torch.compose.exceptions.MarmotNoMatchingKeyPackageBundleException -import at.torch.compose.exceptions.MarmotWelcomeMismatchedGroupIdException +import at.torch.compose.exceptions.MarmotMissingKeyPackageBundleException +import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdException import at.torch.compose.extensions.toHex import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent @@ -39,6 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlin.io.encoding.Base64 @@ -127,7 +126,8 @@ abstract class NostrDao( logger.i("This is a placeholder profile might need to get synced...: ${profile.publicKey}") // Setup the sync here... } else { - return + // TODO: Check if event got indexed... +// return } } @@ -388,257 +388,397 @@ abstract class NostrDao( logger.e("Impersonation", giftWrapImpersonation) throw giftWrapImpersonation // Once this is thrown the database transaction fails and nothing gets saved to the DB... } - val result = database.giftWrapPayloadDao().upsert( + + database.giftWrapPayloadDao().upsert( decryptedGiftWrapPayload ) - val giftWrapPayload = if (result != -1L) { - decryptedGiftWrapPayload.copy( - id = result + if (decryptedGiftWrapPayload.kind == ChatMessageEvent.KIND) { + logger.d("chatMessageEvent: $decryptedGiftWrapPayload") + // Handle normal nip-17 messages... + val localChatRoom = database.chatRoomDao().findChatRoomById( + decryptedGiftWrapPayload.chatRoomId ) - } else { - decryptedGiftWrapPayload - } - // TODO: Handle MLS message/groups - if (giftWrapPayload.kind == WelcomeEvent.KIND) { - logger.d("Welcome Event") + if (localChatRoom == null) { + val userPublicKey = activeKeyPair.pubKey.toHex() + database.chatRoomDao().upsert( + ChatRoom( + id = decryptedGiftWrapPayload.chatRoomId, + userPublicKey = userPublicKey, + subject = decryptedGiftWrapPayload.parseSubject(), + createdAt = decryptedGiftWrapPayload.createdAt, + initialGiftWrapPayloadId = decryptedGiftWrapPayload.id, + mlsGroupState = null + ) + ) + + // Add participants... + val participants = decryptedGiftWrapPayload.participantPTags( + NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile... + ).map { + Participant( + participantPublicKey = it.pubKey, + chatRoomId = decryptedGiftWrapPayload.chatRoomId, + relayHint = it.relayHint?.url + ) + } + + // Sync missing participant profiles... + participants.forEach { participant -> + val giftWrapParticipantProfile = + database.profileDao() + .getProfileByPublicKey(participant.participantPublicKey) + + if (giftWrapParticipantProfile == null) { + // Save a placeholder + database.profileDao().insertPlaceholderProfile( + Profile( + displayName = "LOADING...", + publicKey = participant.participantPublicKey, + createdAt = GENESIS_AT, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, + ) + ) + + val recommendRelayUrl = + giftWrapMessage.receiverRelayHit + + if (recommendRelayUrl != null) { + if (profilePublicKeysToSync[recommendRelayUrl] == null) { + profilePublicKeysToSync[recommendRelayUrl] = + mutableSetOf() + } + profilePublicKeysToSync[recommendRelayUrl]?.add( + participant.participantPublicKey + ) + } else { + if (profilePublicKeysToSync[relayURL] == null) { + profilePublicKeysToSync[relayURL] = + mutableSetOf() + } + profilePublicKeysToSync[relayURL]?.add( + participant.participantPublicKey + ) + } + } + } + + database.participantDao().upsert( + participants + ) + + participants.filter { it.participantPublicKey != userPublicKey } + .forEach { participant -> + val publicKey = participant.participantPublicKey + + val chatMessageRelayListEvent = + database.nostrEventDao() + .getAuthoredNostrEvents( + kinds = arrayOf( + ChatMessageRelayListEvent.KIND + ), + authors = arrayOf( + publicKey + ), + since = GENESIS_AT, + limit = 5 + ).firstOrNull()?.let { nostrEvent -> + if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) { + logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent") + null + } else { + ChatMessageRelayListEvent( + id = nostrEvent.id, + tags = nostrEvent.tags, + pubKey = nostrEvent.pubKey, + content = nostrEvent.content, + createdAt = nostrEvent.createdAt.epochSeconds, + sig = nostrEvent.sig + ) + } + } + + if (chatMessageRelayListEvent != null) { + // Sync messages from this relay that were sent by us + val synchronizationFilter = + SynchronizationFilter( + kinds = arrayOf( + GiftWrapEvent.KIND, + ), + authors = arrayOf( + userPublicKey + ), + tags = mapOf( + Pair( + "p", + listOf(participant.participantPublicKey) + ) + ), + limit = 50 + ) + database.negentropySynchronizeRequestDao() + .insert( + chatMessageRelayListEvent.relays() + .map { normalizedRelayUrl -> + NegentropySynchronizeRequest( + id = NegentropySynchronizeRequest.computeId( + relayURL = normalizedRelayUrl.url, + synchronizationFilter = synchronizationFilter + ), + purpose = "sent-messages", + synchronizationFilter = synchronizationFilter, + relayURL = normalizedRelayUrl.url, + level = 0 + ) + } + ) + } else { + logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey") + + // Sync ChatMessageRelayListEvent publicKey... + // TODO: Get relayHint form participant... + if (profilePublicKeysToSync.containsKey( + relayURL + ) + ) { + profilePublicKeysToSync[relayURL] = + mutableSetOf() + } + profilePublicKeysToSync[relayURL]?.add( + participant.participantPublicKey + ) + } + } + } else { + decryptedGiftWrapPayload.parseSubject()?.let { subject -> + database.chatRoomDao().upsert( + localChatRoom.chatRoom.copy( + subject = subject, + updatedAt = decryptedGiftWrapPayload.createdAt + ) + ) + } + } + + } else if (decryptedGiftWrapPayload.kind == WelcomeEvent.KIND) { val welcomeEvent = WelcomeEvent( - id = "", // TODO: Need the ID as string in decryptedGiftWrapPayload... + id = decryptedGiftWrapPayload.id, // TODO: Need the ID as string in decryptedGiftWrapPayload... pubKey = decryptedGiftWrapPayload.publicKey, content = decryptedGiftWrapPayload.content, createdAt = decryptedGiftWrapPayload.createdAt.epochSeconds, tags = decryptedGiftWrapPayload.tags, sig = "" ) + logger.d("welcomeEvent: $welcomeEvent") - welcomeEvent.nostrGroupId()?.let { chatRoomId -> - val localChatRoom = database.chatRoomDao().findChatRoomById( - chatRoomId + // TODO: Find keypackage and decrypt welcome... + // TODO: Get keyPackage from + val keyPackageEventId = welcomeEvent.keyPackageEventId() ?: throw MarmotWelcomeEventMissingKeyPackageEventIdException( + "Couldn't find keyPackageEventId for $welcomeEvent" + ) + val marmotKeyPackageBundle = database.marmotKeyPackageBundleDao().getMarmotKeyPackageBundleById( + keyPackageEventId + ) ?: throw MarmotMissingKeyPackageBundleException( + "Couldn't find marmotKeyPackageBundle for $welcomeEvent" + ) + + val welcomeBytes = Base64.decode( + welcomeEvent.welcomeBase64() + ) + + val initPrivateKey = marmotKeyPackageBundle.ncryptsecInitPrivateKey.hexToByteArray() // TODO: Decrypt + val signaturePrivateKey = marmotKeyPackageBundle.ncryptsecSignaturePrivateKey.hexToByteArray() // TODO: Decrypt + val encryptionPrivateKey = marmotKeyPackageBundle.ncryptsecEncryptionPrivateKey.hexToByteArray() // TODO: Decrypt + + val tlsReader = TlsReader( + data = marmotKeyPackageBundle.tlsEncodedMarmotKeyPackage + ) + + val group = MlsGroup.processWelcome( + welcomeBytes, + bundle = KeyPackageBundle( + keyPackage = MlsKeyPackage.decodeTls( + tlsReader + ), + initPrivateKey = initPrivateKey, + signaturePrivateKey = signaturePrivateKey, + encryptionPrivateKey = encryptionPrivateKey + ) + ) + + val nostrGroupId = group.groupId.toHex() + + val localChatRoom = database.chatRoomDao().findChatRoomById( + nostrGroupId + ) + + if (localChatRoom == null) { + + logger.d("processWelcome: welcomeBytes=${welcomeBytes.size}B looking up KeyPackage by ref=${keyPackageEventId.take(8)}…") + + + // TODO: Save the chat room We can now s + val userPublicKey = activeKeyPair.pubKey.toHex() + database.chatRoomDao().upsert( + ChatRoom( + id = nostrGroupId, + userPublicKey = userPublicKey, + subject = group.currentMarmotData()?.name, + initialGiftWrapPayloadId = decryptedGiftWrapPayload.id, + createdAt = decryptedGiftWrapPayload.createdAt, + mlsGroupState = group.saveState().encodeTls().toHex() + ) ) - if (localChatRoom == null) { - - val welcomeBytes = Base64.decode( - welcomeEvent.welcomeBase64() + database.marmotKeyPackageBundleDao().upsert( + marmotKeyPackageBundle.copy( + consumed = true ) - val keyPackageEventId = welcomeEvent.keyPackageEventId() - if (keyPackageEventId == null) { - throw MarmotMissingKeyPackageEventIdException( - "WelcomeEvent ${welcomeEvent.id} missing KeyPackage event ID tag" + ) + + // TODO: Add participants... by processing group.members() + val normalizedRelayUrl = NormalizedRelayUrl(relayURL) + + val participants = group.members().mapNotNull { (leafIndex, leafNode) -> + val pubkey = + when (val cred = leafNode.credential) { + is Credential.Basic -> cred.identity.toHexKey() + else -> null + } + if (pubkey != null) { + Participant( + participantPublicKey = pubkey, + chatRoomId = nostrGroupId, + relayHint = normalizedRelayUrl.url ) + } else { + null } + } - logger.d("processWelcome: welcomeBytes=${welcomeBytes.size}B looking up KeyPackage by ref=${keyPackageEventId.take(8)}…") + // Sync missing participant profiles... + participants.forEach { participant -> + val giftWrapParticipantProfile = database.profileDao().getProfileByPublicKey(participant.participantPublicKey) - // TODO: Check if we have already joined group with keyPackage - val marmotKeyPackageBundle = database.marmotKeyPackageBundleDao().getMarmotKeyPackageBundleByKeyPackageEventIdId( - keyPackageEventId - ) - - if (marmotKeyPackageBundle == null) { - // We were unable to find the keyPackageBundle for this invite... - throw MarmotNoMatchingKeyPackageBundleException( - "NO matching KeyPackageBundle for eventId=${keyPackageEventId.take(8)}…" - ) - } - - val initPrivateKey = marmotKeyPackageBundle.ncryptsecInitPrivateKey.hexToByteArray() // TODO: Decrypt - val signaturePrivateKey = marmotKeyPackageBundle.ncryptsecSignaturePrivateKey.hexToByteArray() // TODO: Decrypt - val encryptionPrivateKey = marmotKeyPackageBundle.ncryptsecEncryptionPrivateKey.hexToByteArray() // TODO: Decrypt - - val tlsReader = TlsReader( - data = marmotKeyPackageBundle.tlsEncodedMarmotKeyPackage.hexToByteArray() - ) - - val group = MlsGroup.processWelcome( - welcomeBytes, - bundle = KeyPackageBundle( - keyPackage = MlsKeyPackage.decodeTls( - tlsReader - ), - initPrivateKey = initPrivateKey, - signaturePrivateKey = signaturePrivateKey, - encryptionPrivateKey = encryptionPrivateKey - ) - ) - - val derivedId = group.currentMarmotData()?.nostrGroupId - - if (derivedId == null) { - throw MarmotMissingNostrGroupDataExtension( - "The WelcomeEvent ${welcomeEvent.id} is missing the nostrGroupData extension" - ) - } - - if (derivedId != chatRoomId) { - throw MarmotWelcomeMismatchedGroupIdException( - "The hint group Id $chatRoomId doesn't match the derived group id $derivedId" - ) - } - - // TODO: Save the chat room We can now s - val userPublicKey = activeKeyPair.pubKey.toHex() - database.chatRoomDao().upsert( - ChatRoom( - id = chatRoomId, - userPublicKey = userPublicKey, - subject = group.currentMarmotData()?.name, - initialGiftWrapPayloadId = giftWrapPayload.id, - createdAt = decryptedGiftWrapPayload.createdAt, - mlsGroupState = group.saveState().encodeTls().toHex() - ) - ) - - database.marmotKeyPackageBundleDao().upsert( - marmotKeyPackageBundle.copy( - consumed = true - ) - ) - - // TODO: Add participants... by processing group.members() - val normalizedRelayUrl = NormalizedRelayUrl(relayURL) - - val participants = group.members().mapNotNull { (leafIndex, leafNode) -> - val pubkey = - when (val cred = leafNode.credential) { - is Credential.Basic -> cred.identity.toHexKey() - else -> null - } - if (pubkey != null) { - Participant( - participantPublicKey = pubkey, - chatRoomId = chatRoomId, - relayHint = normalizedRelayUrl.url + if (giftWrapParticipantProfile == null) { + // Save a placeholder + database.profileDao().insertPlaceholderProfile( + Profile( + displayName = "LOADING...", + publicKey = participant.participantPublicKey, + createdAt = GENESIS_AT, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, ) + ) + + val recommendRelayUrl = giftWrapMessage.receiverRelayHit + + if (recommendRelayUrl != null) { + if (profilePublicKeysToSync[recommendRelayUrl] == null) { + profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf() + } + profilePublicKeysToSync[recommendRelayUrl]?.add(participant.participantPublicKey) } else { - null - } - } - - // Sync missing participant profiles... - participants.forEach { participant -> - val giftWrapParticipantProfile = database.profileDao().getProfileByPublicKey(participant.participantPublicKey) - - if (giftWrapParticipantProfile == null) { - // Save a placeholder - database.profileDao().insertPlaceholderProfile( - Profile( - displayName = "LOADING...", - publicKey = participant.participantPublicKey, - createdAt = GENESIS_AT, - nostrEventId = nostrEvent.id, // Will get overwriting by sync, - ) - ) - - val recommendRelayUrl = giftWrapMessage.receiverRelayHit - - if (recommendRelayUrl != null) { - if (profilePublicKeysToSync[recommendRelayUrl] == null) { - profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf() - } - profilePublicKeysToSync[recommendRelayUrl]?.add(participant.participantPublicKey) - } else { - if (profilePublicKeysToSync[relayURL] == null) { - profilePublicKeysToSync[relayURL] = mutableSetOf() - } - profilePublicKeysToSync[relayURL]?.add( - participant.participantPublicKey - ) - } - } - } - - database.participantDao().upsert( - participants - ) - - participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant -> - val publicKey = participant.participantPublicKey - - val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents( - kinds = arrayOf( - ChatMessageRelayListEvent.KIND - ), - authors = arrayOf( - publicKey - ), - since = GENESIS_AT, - limit = 5 - ).firstOrNull()?.let { nostrEvent -> - if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) { - logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent") - null - } else { - ChatMessageRelayListEvent( - id = nostrEvent.id, - tags = nostrEvent.tags, - pubKey = nostrEvent.pubKey, - content = nostrEvent.content, - createdAt = nostrEvent.createdAt.epochSeconds, - sig = nostrEvent.sig - ) - } - } - - if (chatMessageRelayListEvent != null) { - // Sync messages from this relay that were sent by us - val synchronizationFilter = SynchronizationFilter( - kinds = arrayOf( - GiftWrapEvent.KIND, - ), - authors = arrayOf( - userPublicKey - ), - tags = mapOf( - Pair("p", listOf(participant.participantPublicKey)) - ), - limit = 50 - ) - database.negentropySynchronizeRequestDao().insert( - chatMessageRelayListEvent.relays().map { normalizedRelayUrl -> - NegentropySynchronizeRequest( - id = NegentropySynchronizeRequest.computeId( - relayURL = normalizedRelayUrl.url, - synchronizationFilter = synchronizationFilter - ), - purpose = "sent-messages", - synchronizationFilter = synchronizationFilter, - relayURL = normalizedRelayUrl.url, - level = 0 - ) - } - ) - } else { - logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey") - - // Sync ChatMessageRelayListEvent publicKey... - // TODO: Get relayHint form participant... - if (profilePublicKeysToSync.containsKey(relayURL)) { + if (profilePublicKeysToSync[relayURL] == null) { profilePublicKeysToSync[relayURL] = mutableSetOf() } - profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey) + profilePublicKeysToSync[relayURL]?.add( + participant.participantPublicKey + ) } } } - // Where is the decoded welcome message... - // TODO: Sync GroupEvents/Messages... - database.chatMessageDao().upsert( - ChatMessage( - giftWrapPayloadId = giftWrapPayload.id, - senderPublicKey = giftWrapPayload.publicKey, - isUserMessage = activeKeyPair.pubKey.toHex() == giftWrapPayload.publicKey, - chatRoomId = chatRoomId, - createdAt = giftWrapPayload.createdAt, - content = "Coming soon.", // TODO: Figure out what to do here... - ) + database.participantDao().upsert( + participants ) + + participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant -> + val publicKey = participant.participantPublicKey + + val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents( + kinds = arrayOf( + ChatMessageRelayListEvent.KIND + ), + authors = arrayOf( + publicKey + ), + since = GENESIS_AT, + limit = 5 + ).firstOrNull()?.let { nostrEvent -> + if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) { + logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent") + null + } else { + ChatMessageRelayListEvent( + id = nostrEvent.id, + tags = nostrEvent.tags, + pubKey = nostrEvent.pubKey, + content = nostrEvent.content, + createdAt = nostrEvent.createdAt.epochSeconds, + sig = nostrEvent.sig + ) + } + } + + if (chatMessageRelayListEvent != null) { + // Sync messages from this relay that were sent by us + val synchronizationFilter = SynchronizationFilter( + kinds = arrayOf( + GiftWrapEvent.KIND, + ), + authors = arrayOf( + userPublicKey + ), + tags = mapOf( + Pair("p", listOf(participant.participantPublicKey)) + ), + limit = 50 + ) + database.negentropySynchronizeRequestDao().insert( + chatMessageRelayListEvent.relays().map { normalizedRelayUrl -> + NegentropySynchronizeRequest( + id = NegentropySynchronizeRequest.computeId( + relayURL = normalizedRelayUrl.url, + synchronizationFilter = synchronizationFilter + ), + purpose = "sent-messages", + synchronizationFilter = synchronizationFilter, + relayURL = normalizedRelayUrl.url, + level = 0 + ) + } + ) + } else { + logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey") + + // Sync ChatMessageRelayListEvent publicKey... + // TODO: Get relayHint form participant... + if (profilePublicKeysToSync.containsKey(relayURL)) { + profilePublicKeysToSync[relayURL] = mutableSetOf() + } + profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey) + } + } } + + // Where is the decoded welcome message... + // TODO: Sync GroupEvents/Messages... + database.chatMessageDao().upsert( + ChatMessage( + giftWrapPayloadId = decryptedGiftWrapPayload.id, + senderPublicKey = decryptedGiftWrapPayload.publicKey, + isUserMessage = activeKeyPair.pubKey.toHex() == decryptedGiftWrapPayload.publicKey, + chatRoomId = nostrGroupId, + createdAt = decryptedGiftWrapPayload.createdAt, + content = "Coming soon.", // TODO: Figure out what to do here... + ) + ) // TODO: Check encoding... } else { - logger.w("Unsupported event: $giftWrapPayload") + logger.w("Unsupported event: $decryptedGiftWrapPayload") } } } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatMessage.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatMessage.kt index a1b6d1de..c6456d1d 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatMessage.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatMessage.kt @@ -7,6 +7,7 @@ import at.torch.compose.database.model.traits.LocalStoreEntity import at.torch.compose.database.model.traits.SoftDeletableEntity import at.torch.compose.database.model.traits.TimestampedEntity import at.torch.compose.database.model.traits.UserViewableEntity +import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlin.time.Clock import kotlin.time.Instant @@ -33,7 +34,7 @@ data class ChatMessage( val senderPublicKey: String, val isUserMessage: Boolean, - val giftWrapPayloadId: Long, + val giftWrapPayloadId: HexKey, val chatRoomId: String, val replyToMessageId: Long? = null, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatRoom.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatRoom.kt index 9e92ff46..e1f5757a 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatRoom.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/ChatRoom.kt @@ -67,7 +67,7 @@ data class ChatRoom( // TODO: Add a LastChatMessageRelation // val lastChatMessageId: String? = null, // Might want to focus on the other last messages... - val initialGiftWrapPayloadId: Long? = null, + val initialGiftWrapPayloadId: HexKey? = null, override val createdAt: Instant = Clock.System.now(), override val updatedAt: Instant = createdAt, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapPayload.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapPayload.kt index 30114d3a..3f81f084 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapPayload.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapPayload.kt @@ -5,6 +5,7 @@ import androidx.room3.ForeignKey import androidx.room3.Ignore import androidx.room3.PrimaryKey import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -24,8 +25,8 @@ import kotlin.time.Instant ], ) data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload... - @PrimaryKey(autoGenerate = true) - val id: Long = 0, + @PrimaryKey + val id: HexKey, /** * Sender Public Key @@ -34,7 +35,7 @@ data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload... val chatRoomId: String, - /** + /**` * Payload Event Kind */ val kind: Kind, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapSeal.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapSeal.kt index 8b6e3d2b..c6e9469a 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapSeal.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/GiftWrapSeal.kt @@ -94,6 +94,7 @@ data class GiftWrapSeal( logger.d("giftWrapPayload: ${giftWrapPayload.toJson()}") val giftWrapPayload = GiftWrapPayload( + id = id, giftWrapSealId = id, publicKey = giftWrapPayload.pubKey, kind = giftWrapPayload.kind, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackage.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackage.kt index 8e1ea915..5849bb03 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackage.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackage.kt @@ -22,8 +22,8 @@ import kotlin.time.Instant ] ) data class MarmotKeyPackage( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, + @PrimaryKey + val id: HexKey, val publicKey: HexKey, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackageBundle.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackageBundle.kt index ed175b13..a1fbafba 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackageBundle.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotKeyPackageBundle.kt @@ -26,13 +26,13 @@ import kotlin.time.Instant ], ) data class MarmotKeyPackageBundle( - @PrimaryKey(autoGenerate = true) - val id: Long = 0, + @PrimaryKey + val id: HexKey, val publicKey: HexKey, - // TODO: have the keyPackageEventId - val tlsEncodedMarmotKeyPackage: String, + val tlsEncodedMarmotKeyPackage: ByteArray, + val ncryptsecInitPrivateKey: String, val ncryptsecEncryptionPrivateKey: String, val ncryptsecSignaturePrivateKey: String, @@ -45,4 +45,44 @@ data class MarmotKeyPackageBundle( override val savedAt: Instant = Clock.System.now(), override val deletedAt: Instant? = null, override val broadcastedAt: Instant? = null, -): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity \ No newline at end of file +): TimestampedEntity, LocalStoreEntity, BroadcastableEntity, SoftDeletableEntity { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as MarmotKeyPackageBundle + + if (consumed != other.consumed) return false + if (rotated != other.rotated) return false + if (id != other.id) return false + if (publicKey != other.publicKey) return false + if (!tlsEncodedMarmotKeyPackage.contentEquals(other.tlsEncodedMarmotKeyPackage)) return false + if (ncryptsecInitPrivateKey != other.ncryptsecInitPrivateKey) return false + if (ncryptsecEncryptionPrivateKey != other.ncryptsecEncryptionPrivateKey) return false + if (ncryptsecSignaturePrivateKey != other.ncryptsecSignaturePrivateKey) return false + if (createdAt != other.createdAt) return false + if (updatedAt != other.updatedAt) return false + if (savedAt != other.savedAt) return false + if (deletedAt != other.deletedAt) return false + if (broadcastedAt != other.broadcastedAt) return false + + return true + } + + override fun hashCode(): Int { + var result = consumed.hashCode() + result = 31 * result + rotated.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + publicKey.hashCode() + result = 31 * result + tlsEncodedMarmotKeyPackage.contentHashCode() + result = 31 * result + ncryptsecInitPrivateKey.hashCode() + result = 31 * result + ncryptsecEncryptionPrivateKey.hashCode() + result = 31 * result + ncryptsecSignaturePrivateKey.hashCode() + result = 31 * result + createdAt.hashCode() + result = 31 * result + updatedAt.hashCode() + result = 31 * result + savedAt.hashCode() + result = 31 * result + (deletedAt?.hashCode() ?: 0) + result = 31 * result + (broadcastedAt?.hashCode() ?: 0) + return result + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/NostrEvent.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/NostrEvent.kt index 85c12124..012eeee6 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/NostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/NostrEvent.kt @@ -212,6 +212,7 @@ data class NostrEvent( } return null } catch (e: Throwable) { + logger.e("GiftWrapMessage error: ", e) return null } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt index 160f0ddb..b5cf5c2c 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt @@ -1,9 +1,12 @@ package at.torch.compose.database.repository +import at.torch.compose.database.model.ChatMessage +import at.torch.compose.database.model.GiftWrapPayload import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync @@ -14,6 +17,8 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlin.time.Clock +import kotlin.time.Instant class DatabaseChatRepository( private val database: at.torch.compose.database.TorchDatabase, @@ -104,17 +109,28 @@ class DatabaseChatRepository( ) } - val giftWrapPayloadId = database.giftWrapPayloadDao().upsert( - _root_ide_package_.at.torch.compose.database.model.GiftWrapPayload( + val createdAt = Clock.System.now().epochSeconds + val giftWrapPayloadId = EventHasher.hashId( + pubKey = localChatRoom.chatRoom.userPublicKey, + createdAt = createdAt, + tags = receiverTags.toTypedArray(), + content = text, + kind = messageType + ) + + database.giftWrapPayloadDao().upsert( + GiftWrapPayload( + id = giftWrapPayloadId, kind = messageType, tags = receiverTags.toTypedArray(), + createdAt = Instant.fromEpochSeconds(createdAt), content = text, chatRoomId = localChatRoom.chatRoom.id, publicKey = localChatRoom.chatRoom.userPublicKey ) ) database.chatMessageDao().upsert( - _root_ide_package_.at.torch.compose.database.model.ChatMessage( + ChatMessage( content = text, chatRoomId = localChatRoom.chatRoom.id, senderPublicKey = localChatRoom.chatRoom.userPublicKey, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseMarmotRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseMarmotRepository.kt index 1e2dd513..18859a5d 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseMarmotRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseMarmotRepository.kt @@ -2,13 +2,31 @@ package at.torch.compose.database.repository import at.torch.compose.database.TorchDatabase import at.torch.compose.database.model.MarmotKeyPackageBundle +import at.torch.compose.database.model.UnsignedNostrEvent import at.torch.compose.extensions.toHex +import at.torch.compose.nostr.Relays import at.torch.compose.repository.MarmotRepository +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRotationManager.Companion.KEY_PACKAGE_LIFETIME_SECONDS +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageUtils import com.vitorpamplona.quartz.marmot.mls.codec.TlsWriter +import com.vitorpamplona.quartz.marmot.mls.crypto.Ed25519 +import com.vitorpamplona.quartz.marmot.mls.crypto.MlsCryptoProvider +import com.vitorpamplona.quartz.marmot.mls.crypto.X25519 import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle +import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage +import com.vitorpamplona.quartz.marmot.mls.tree.Capabilities +import com.vitorpamplona.quartz.marmot.mls.tree.Credential +import com.vitorpamplona.quartz.marmot.mls.tree.Extension +import com.vitorpamplona.quartz.marmot.mls.tree.LeafNode +import com.vitorpamplona.quartz.marmot.mls.tree.LeafNodeSource +import com.vitorpamplona.quartz.marmot.mls.tree.Lifetime import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope +import kotlin.io.encoding.Base64 class DatabaseMarmotRepository( val database: TorchDatabase, @@ -16,24 +34,141 @@ class DatabaseMarmotRepository( ): MarmotRepository { override suspend fun saveMarmotKeyPackageBundle( publicKey: HexKey, - keyPackageBundle: KeyPackageBundle, nsecPassword: String ) { - val tlsWriter = TlsWriter() + // KeyPackage... + val dTag = KeyPackageUtils.generateRandomDTag() // TODO: Persist this... + val identity = publicKey.hexToByteArray() + val bundle = generateKeyPackage(identity, dTag) - keyPackageBundle.keyPackage.encodeTls(tlsWriter) + val keyPackageBytes = bundle.keyPackage.toTlsBytes() + val keyPackageBase64 = Base64.encode(keyPackageBytes) + val keyPackageRef = bundle.keyPackage.reference().toHexKey() - val nip49 = Nip49() + val keyPackageTemplate = KeyPackageEvent.build( + keyPackageBase64 = keyPackageBase64, + dTagSlot = dTag, + keyPackageRef = keyPackageRef, + relays = Relays.DefaultDMRelayList, + ) - database.marmotKeyPackageBundleDao().upsert( - MarmotKeyPackageBundle( - publicKey = publicKey, - tlsEncodedMarmotKeyPackage = tlsWriter.toByteArray().toHex(), - ncryptsecInitPrivateKey = keyPackageBundle.initPrivateKey.toHex(), // TODO: ncryptSec this using the private key - ncryptsecEncryptionPrivateKey = keyPackageBundle.encryptionPrivateKey.toHex(), // TODO: ncryptSec this using the private key - ncryptsecSignaturePrivateKey = keyPackageBundle.signaturePrivateKey.toHex() // TODO: ncryptSec this using the private key + database.unsignedNostrEventDao().upsert( + UnsignedNostrEvent( + pubKey = publicKey, + kind = keyPackageTemplate.kind, + tags = keyPackageTemplate.tags, + content = keyPackageTemplate.content ) ) + val id = EventHasher.hashIdBytes( + pubKey =publicKey, + createdAt = keyPackageTemplate.createdAt, + kind = keyPackageTemplate.kind, + tags = keyPackageTemplate.tags, + content = keyPackageTemplate.content + ) + + database.marmotKeyPackageBundleDao().upsert( + MarmotKeyPackageBundle( + id = id.toHexKey(), + publicKey = publicKey, + tlsEncodedMarmotKeyPackage = bundle.keyPackage.toTlsBytes(), + ncryptsecInitPrivateKey = bundle.initPrivateKey.toHex(), // TODO: ncryptSec this using the private key + ncryptsecEncryptionPrivateKey = bundle.encryptionPrivateKey.toHex(), // TODO: ncryptSec this using the private key + ncryptsecSignaturePrivateKey = bundle.signaturePrivateKey.toHex() // TODO: ncryptSec this using the private key + ) + ) } + + + private fun generateKeyPackage( + identity: ByteArray, + dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT, + ): KeyPackageBundle { + val initKp = X25519.generateKeyPair() + val encKp = X25519.generateKeyPair() + val sigKp = Ed25519.generateKeyPair() + + val leafNode = + buildKeyPackageLeafNode( + encryptionKey = encKp.publicKey, + signatureKey = sigKp.publicKey, + identity = identity, + signingKey = sigKp.privateKey, + ) + + val unsigned = + MlsKeyPackage( + initKey = initKp.publicKey, + leafNode = leafNode, + // LastResort (0x000A) marks this as a last-resort KeyPackage per MLS Extensions draft. + // MDK always marks KeyPackages as last-resort via .mark_as_last_resort(). + extensions = listOf( + Extension( + extensionType = 0x000A, + extensionData = ByteArray(0) + ) + ), + signature = ByteArray(0), + ) + val keyPackage = + unsigned.copy( + signature = + MlsCryptoProvider.signWithLabel( + sigKp.privateKey, + "KeyPackageTBS", + unsigned.encodeTbs(), + ), + ) + + val bundle = KeyPackageBundle( + keyPackage, + initPrivateKey = initKp.privateKey, + encryptionPrivateKey = encKp.privateKey, + signaturePrivateKey = sigKp.privateKey + ) + + return bundle + } + + + private fun buildKeyPackageLeafNode( + encryptionKey: ByteArray, + signatureKey: ByteArray, + identity: ByteArray, + signingKey: ByteArray, + ): LeafNode { + val now = TimeUtils.now() + val unsigned = + LeafNode( + encryptionKey = encryptionKey, + signatureKey = signatureKey, + credential = Credential.Basic(identity), + capabilities = + Capabilities( + extensions = + listOf( + 0x000A, // LastResort (required by OpenMLS validation) + 0xF2EE, // NostrGroupData (required by group's RequiredCapabilities) + ), + proposals = + listOf( + 0x000A, // SelfRemove (required by group's RequiredCapabilities) + ), + ), + leafNodeSource = LeafNodeSource.KEY_PACKAGE, + lifetime = Lifetime( + notBefore = now, + notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS + ), + extensions = emptyList(), + signature = ByteArray(0), + ) + + val tbs = unsigned.encodeTbs(groupId = null, leafIndex = null) + val signature = MlsCryptoProvider.signWithLabel(signingKey, "LeafNodeTBS", tbs) + return unsigned.copy(signature = signature) + } + } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseNostrRepository.kt index 70cd5818..52d0eb14 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseNostrRepository.kt @@ -114,7 +114,7 @@ class DatabaseNostrRepository( publicKey: HexKey, name: String?, biography: String? - ): KeyPackageBundle { + ) { val profileEventTemplate = MetadataEvent.createNew( name = name, about = biography, @@ -202,113 +202,6 @@ class DatabaseNostrRepository( content = "" ) ) - - // KeyPackage... - KeyPackageUtils.PRIMARY_SLOT - val dTag = KeyPackageUtils.generateRandomDTag() // TODO: Persist this... - val identity = publicKey.hexToByteArray() - val bundle = generateKeyPackage(identity, dTag) - - val keyPackageBytes = bundle.keyPackage.toTlsBytes() - val keyPackageBase64 = Base64.encode(keyPackageBytes) - val keyPackageRef = bundle.keyPackage.reference().toHexKey() - - val keyPackageTemplate = KeyPackageEvent.build( - keyPackageBase64 = keyPackageBase64, - dTagSlot = dTag, - keyPackageRef = keyPackageRef, - relays = Relays.DefaultDMRelayList, - ) - - unsignedNostrEvents.add( - UnsignedNostrEvent( - pubKey = publicKey, - kind = keyPackageTemplate.kind, - tags = keyPackageTemplate.tags, - content = keyPackageTemplate.content - ) - ) - - saveUnsignedNostrEvents(unsignedNostrEvents) - - return bundle - } - - private fun generateKeyPackage( - identity: ByteArray, - dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT, - ): KeyPackageBundle { - val initKp = X25519.generateKeyPair() - val encKp = X25519.generateKeyPair() - val sigKp = Ed25519.generateKeyPair() - - val leafNode = - buildKeyPackageLeafNode( - encryptionKey = encKp.publicKey, - signatureKey = sigKp.publicKey, - identity = identity, - signingKey = sigKp.privateKey, - ) - - val unsigned = - MlsKeyPackage( - initKey = initKp.publicKey, - leafNode = leafNode, - // LastResort (0x000A) marks this as a last-resort KeyPackage per MLS Extensions draft. - // MDK always marks KeyPackages as last-resort via .mark_as_last_resort(). - extensions = listOf(Extension(extensionType = 0x000A, extensionData = ByteArray(0))), - signature = ByteArray(0), - ) - val keyPackage = - unsigned.copy( - signature = - MlsCryptoProvider.signWithLabel( - sigKp.privateKey, - "KeyPackageTBS", - unsigned.encodeTbs(), - ), - ) - - val bundle = KeyPackageBundle(keyPackage, initKp.privateKey, encKp.privateKey, sigKp.privateKey) - // TODO: Persist this... - - return bundle - } - - - private fun buildKeyPackageLeafNode( - encryptionKey: ByteArray, - signatureKey: ByteArray, - identity: ByteArray, - signingKey: ByteArray, - ): LeafNode { - val now = TimeUtils.now() - val unsigned = - LeafNode( - encryptionKey = encryptionKey, - signatureKey = signatureKey, - credential = Credential.Basic(identity), - capabilities = - Capabilities( - extensions = - listOf( - 0x000A, // LastResort (required by OpenMLS validation) - 0xF2EE, // NostrGroupData (required by group's RequiredCapabilities) - ), - proposals = - listOf( - 0x000A, // SelfRemove (required by group's RequiredCapabilities) - ), - ), - leafNodeSource = LeafNodeSource.KEY_PACKAGE, - lifetime = Lifetime(notBefore = now, notAfter = now + KEY_PACKAGE_LIFETIME_SECONDS), - extensions = emptyList(), - signature = ByteArray(0), - ) - - val tbs = unsigned.encodeTbs(groupId = null, leafIndex = null) - val signature = MlsCryptoProvider.signWithLabel(signingKey, "LeafNodeTBS", tbs) - return unsigned.copy(signature = signature) } override suspend fun signInToProfile( diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotMissingKeyPackageBundleException.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotMissingKeyPackageBundleException.kt new file mode 100644 index 00000000..ae17c85e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotMissingKeyPackageBundleException.kt @@ -0,0 +1,4 @@ +package at.torch.compose.exceptions + +class MarmotMissingKeyPackageBundleException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotNoMatchingKeyPackageBundleException.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotNoMatchingKeyPackageBundleException.kt deleted file mode 100644 index 781ae651..00000000 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotNoMatchingKeyPackageBundleException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package at.torch.compose.exceptions - -class MarmotNoMatchingKeyPackageBundleException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeEventMissingKeyPackageEventIdException.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeEventMissingKeyPackageEventIdException.kt new file mode 100644 index 00000000..772f7e2b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeEventMissingKeyPackageEventIdException.kt @@ -0,0 +1,4 @@ +package at.torch.compose.exceptions + +class MarmotWelcomeEventMissingKeyPackageEventIdException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeMissingGroupIdException.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeMissingGroupIdException.kt new file mode 100644 index 00000000..d845356e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/exceptions/MarmotWelcomeMissingGroupIdException.kt @@ -0,0 +1,4 @@ +package at.torch.compose.exceptions + +class MarmotWelcomeMissingGroupIdException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/MarmotRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/MarmotRepository.kt index edc44f81..b99ca1ba 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/MarmotRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/MarmotRepository.kt @@ -1,13 +1,11 @@ package at.torch.compose.repository -import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey interface MarmotRepository { suspend fun saveMarmotKeyPackageBundle( publicKey: HexKey, - keyPackageBundle: KeyPackageBundle, nsecPassword: String ) @@ -15,7 +13,6 @@ interface MarmotRepository { val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository { override suspend fun saveMarmotKeyPackageBundle( publicKey: HexKey, - keyPackageBundle: KeyPackageBundle, nsecPassword: String ) { TODO("Not yet implemented") diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/NostrRepository.kt index 2e3337ca..cc984c70 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/NostrRepository.kt @@ -1,7 +1,6 @@ package at.torch.compose.repository import at.torch.compose.database.model.intermdiate.LocalAccount -import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -28,7 +27,7 @@ interface NostrRepository { publicKey: HexKey, name: String?, biography: String? - ): KeyPackageBundle + ) suspend fun signInToProfile( publicKey: HexKey @@ -163,7 +162,7 @@ interface NostrRepository { publicKey: HexKey, name: String?, biography: String? - ): KeyPackageBundle { + ) { TODO("") } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt index 327ce4e8..30d5d031 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/ChatRoomDetailScreen.kt @@ -287,7 +287,7 @@ private fun ChatRoomDetailScreenPreview() { id = "", userPublicKey = "", subject = "Message title", - initialGiftWrapPayloadId = 0, + initialGiftWrapPayloadId = "sdfaer", mlsGroupState = null ), ) diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt index f35e5bdb..8e030718 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt @@ -25,6 +25,7 @@ import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute import at.torch.compose.ui.composable.navigation.routes.LoadingRoute import at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute import at.torch.compose.ui.composable.navigation.routes.ShareProfileRoute +import at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute import at.torch.compose.ui.composable.navigation.routes.SovereignWalletStartupRoute import at.torch.compose.ui.composable.navigation.routes.WriteNewNoteRoute import at.torch.compose.ui.view.state.NavigationUIState @@ -101,6 +102,7 @@ fun TorchNavHost( scope = applicationIOScope ) ) + // TODO: Produce a notary UI Element... val synchronizationViewModel: at.torch.compose.ui.view.model.SynchronizationViewModel = viewModel( factory = at.torch.compose.ui.view.model.SynchronizationViewModel.factory( @@ -203,7 +205,7 @@ fun TorchNavHost( } is NavigationUIState.ProfileLoaded -> { navController.navigate( - route = at.torch.compose.ui.composable.navigation.routes.SocialPreconditionRoute( + route = SocialPreconditionRoute( activeUserPubkey = state.publicKey ) ) { @@ -339,7 +341,7 @@ fun TorchNavHost( }, onNavigateToNostrEvent = { hexKey -> navController.navigate( - route = at.torch.compose.ui.composable.navigation.routes.NostrEventDetailRoute( + route = NostrEventDetailRoute( activeUserPublicKey = route.activeUserPublicKey, nostrEventId = hexKey ) @@ -383,8 +385,8 @@ fun TorchNavHost( at.torch.compose.ui.composable.UnannouncedProfileScreen() } - composable { backStackEntry -> - val route = backStackEntry.toRoute() + composable { backStackEntry -> + val route = backStackEntry.toRoute() at.torch.compose.ui.composable.SocialPreconditionScreen( onNavigateToSkipForNow = { navController.navigate( diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/CreateProfileViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/CreateProfileViewModel.kt index 41a1965c..c9045567 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/CreateProfileViewModel.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/CreateProfileViewModel.kt @@ -105,7 +105,8 @@ class CreateProfileViewModel( ) val pubkey = localKeyManager.nostrPublicKey() - val keyPackageBundle = nostrRepository.createNewProfile( + + nostrRepository.createNewProfile( pubkey, name = createProfileFormState.nameField.textFieldState.text.toString(), biography = createProfileFormState.biographyField.textFieldState.text.toString() @@ -113,7 +114,6 @@ class CreateProfileViewModel( marmotRepository.saveMarmotKeyPackageBundle( publicKey = pubkey, - keyPackageBundle, nsecPassword = localKeyManager.nsecPassword() ) } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/NotaryViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/NotaryViewModel.kt index 0030741f..281efd4f 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/NotaryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/NotaryViewModel.kt @@ -54,7 +54,7 @@ class NotaryViewModel( init { scope.launch { activeWalletStateFlow.collectLatest { activeWallet -> - logger.d("activeWallet: $activeWallet") + logger.d("Notary ActiveWallet: $activeWallet") activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey -> val keyPair = KeyPair( diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SynchronizationViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SynchronizationViewModel.kt index 65cc3a04..99ae2a02 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SynchronizationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/SynchronizationViewModel.kt @@ -74,6 +74,7 @@ class SynchronizationViewModel( init { scope.launch { activeWalletStateFlow.collectLatest { activeWallet -> + logger.d("Synchronization ActiveWallet: $activeWallet") activeWallet?.business?.walletManager?.keyManager?.value?.nostrPrivateKey()?.let { nostrPrivateKey -> val keyPair = KeyPair( privKey = nostrPrivateKey.value.toByteArray() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a22d92c7..860979a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ okhttp = "5.3.2" okio = "3.17.0" pagingCommon = "3.5.0" qrose = "1.1.2" -quartz = "1.10.0" +quartz = "1.12.6" room3 = "3.0.0-alpha06" sqldelight = "2.3.2" sqlite = "2.6.2"