From 225606d724b214ebfe839a861bbe258fad9e71ca Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Fri, 3 Jul 2026 22:50:50 +0200 Subject: [PATCH] Save inbound marmotInnerEvent --- .../torch/compose/database/TorchDatabase.kt | 96 +++++++++----- .../database/dao/MarmotInnerEventDao.kt | 14 ++ .../at/torch/compose/database/dao/NostrDao.kt | 4 +- .../compose/database/model/ChatMessage.kt | 35 ++++- .../database/model/MarmotInnerEvent.kt | 6 +- .../repository/DatabaseChatRepository.kt | 120 +++++++++++++----- 6 files changed, 207 insertions(+), 68 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotInnerEventDao.kt diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt index d9091da7..1e7df39f 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/TorchDatabase.kt @@ -5,45 +5,77 @@ import androidx.room3.RoomDatabase import androidx.room3.TypeConverters import androidx.room3.immediateTransaction import androidx.room3.useWriterConnection +import at.torch.compose.database.dao.MarmotInnerEventDao import at.torch.compose.database.dao.MarmotKeyPackageBundleDao import at.torch.compose.database.dao.MarmotKeyPackageDao +import at.torch.compose.database.model.BroadcastNostrEventReceipt +import at.torch.compose.database.model.BroadcastNostrEventRequest +import at.torch.compose.database.model.ChatMessage +import at.torch.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation +import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation +import at.torch.compose.database.model.ChatMessageNostrEventRelation +import at.torch.compose.database.model.ChatRoom +import at.torch.compose.database.model.Connection +import at.torch.compose.database.model.GiftWrapMessage +import at.torch.compose.database.model.GiftWrapPayload +import at.torch.compose.database.model.GiftWrapSeal +import at.torch.compose.database.model.InReplyToRelation +import at.torch.compose.database.model.MarmotKeyPackage +import at.torch.compose.database.model.MarmotKeyPackageBundle +import at.torch.compose.database.model.Mention +import at.torch.compose.database.model.NegentropySynchronizeRequest +import at.torch.compose.database.model.NegentropySynchronizeResult +import at.torch.compose.database.model.NostrEvent +import at.torch.compose.database.model.NostrEventRelay +import at.torch.compose.database.model.Participant +import at.torch.compose.database.model.Post +import at.torch.compose.database.model.Profile +import at.torch.compose.database.model.QuotedRelation +import at.torch.compose.database.model.Reaction +import at.torch.compose.database.model.RecentSearch +import at.torch.compose.database.model.Relay +import at.torch.compose.database.model.RepostedRelation +import at.torch.compose.database.model.SynchronizeNostrEventRequest +import at.torch.compose.database.model.SynchronizeNostrEventResult +import at.torch.compose.database.model.UnsignedNostrEvent +import at.torch.compose.database.model.Zap import kotlin.time.Instant val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L) @Database( entities = [ - at.torch.compose.database.model.BroadcastNostrEventReceipt::class, - at.torch.compose.database.model.BroadcastNostrEventRequest::class, - at.torch.compose.database.model.Connection::class, - at.torch.compose.database.model.ChatMessage::class, - at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation::class, - at.torch.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation::class, - at.torch.compose.database.model.ChatMessageNostrEventRelation::class, - at.torch.compose.database.model.ChatRoom::class, - at.torch.compose.database.model.GiftWrapMessage::class, - at.torch.compose.database.model.GiftWrapSeal::class, - at.torch.compose.database.model.GiftWrapPayload::class, - at.torch.compose.database.model.InReplyToRelation::class, - at.torch.compose.database.model.MarmotKeyPackage::class, - at.torch.compose.database.model.MarmotKeyPackageBundle::class, - at.torch.compose.database.model.Mention::class, - at.torch.compose.database.model.NegentropySynchronizeRequest::class, - at.torch.compose.database.model.NegentropySynchronizeResult::class, - at.torch.compose.database.model.NostrEvent::class, - at.torch.compose.database.model.NostrEventRelay::class, - at.torch.compose.database.model.Participant::class, - at.torch.compose.database.model.Post::class, - at.torch.compose.database.model.Profile::class, - at.torch.compose.database.model.QuotedRelation::class, - at.torch.compose.database.model.Reaction::class, - at.torch.compose.database.model.RecentSearch::class, - at.torch.compose.database.model.Relay::class, - at.torch.compose.database.model.RepostedRelation::class, - at.torch.compose.database.model.SynchronizeNostrEventRequest::class, - at.torch.compose.database.model.SynchronizeNostrEventResult::class, - at.torch.compose.database.model.UnsignedNostrEvent::class, - at.torch.compose.database.model.Zap::class + BroadcastNostrEventReceipt::class, + BroadcastNostrEventRequest::class, + Connection::class, + ChatMessage::class, + ChatMessageBroadcastNostrEventRequestRelation::class, + ChatMessageBroadcastNostrEventReceiptRelation::class, + ChatMessageNostrEventRelation::class, + ChatRoom::class, + GiftWrapMessage::class, + GiftWrapSeal::class, + GiftWrapPayload::class, + InReplyToRelation::class, + MarmotKeyPackage::class, + MarmotKeyPackageBundle::class, + Mention::class, + NegentropySynchronizeRequest::class, + NegentropySynchronizeResult::class, + NostrEvent::class, + NostrEventRelay::class, + Participant::class, + Post::class, + Profile::class, + QuotedRelation::class, + Reaction::class, + RecentSearch::class, + Relay::class, + RepostedRelation::class, + SynchronizeNostrEventRequest::class, + SynchronizeNostrEventResult::class, + UnsignedNostrEvent::class, + Zap::class ], version = 1 ) @@ -72,6 +104,8 @@ abstract class TorchDatabase: RoomDatabase() { abstract fun inReplyToRelationDao(): at.torch.compose.database.dao.InReplyToRelationDao + abstract fun marmotInnerEventDao(): MarmotInnerEventDao + abstract fun marmotKeyPackageDao(): MarmotKeyPackageDao abstract fun marmotKeyPackageBundleDao(): MarmotKeyPackageBundleDao diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotInnerEventDao.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotInnerEventDao.kt new file mode 100644 index 00000000..d58c3ffd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/dao/MarmotInnerEventDao.kt @@ -0,0 +1,14 @@ +package at.torch.compose.database.dao + +import androidx.room3.Dao +import androidx.room3.Query +import androidx.room3.Upsert +import at.torch.compose.database.model.MarmotInnerEvent +import kotlinx.coroutines.flow.Flow + +@Dao +interface MarmotInnerEventDao { + + @Upsert + suspend fun upsert(marmotInnerEvent: MarmotInnerEvent) +} \ No newline at end of file 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 d9a0ce93..0de50268 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 @@ -43,6 +43,7 @@ import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState import com.vitorpamplona.quartz.marmot.mls.messages.KeyPackageBundle import com.vitorpamplona.quartz.marmot.mls.messages.MlsKeyPackage import com.vitorpamplona.quartz.marmot.mls.tree.Credential +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent @@ -376,6 +377,7 @@ abstract class NostrDao( )?.let { groupEventResult -> ChatMessage.fromGroupEventResult( + database = database, activeKeyPair = activeKeyPair, groupEvent = groupEvent, groupEventResult = groupEventResult, @@ -385,7 +387,7 @@ abstract class NostrDao( ) } - // TODO: Process none chatMessages... + } 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 48eb5294..c99ec57f 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 @@ -3,6 +3,7 @@ package at.torch.compose.database.model import androidx.room3.Entity import androidx.room3.ForeignKey import androidx.room3.PrimaryKey +import at.torch.compose.database.TorchDatabase import at.torch.compose.database.model.traits.LocalStoreEntity import at.torch.compose.database.model.traits.SoftDeletableEntity import at.torch.compose.database.model.traits.TimestampedEntity @@ -33,6 +34,18 @@ import kotlin.time.Instant childColumns = ["giftWrapPayloadId"], onDelete = ForeignKey.CASCADE, ), + ForeignKey( + entity = GroupEvent::class, + parentColumns = ["id"], + childColumns = ["groupEventId"], + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = MarmotInnerEvent::class, + parentColumns = ["id"], + childColumns = ["marmotInnerEventId"], + onDelete = ForeignKey.CASCADE, + ), ], ) data class ChatMessage( @@ -44,6 +57,7 @@ data class ChatMessage( val giftWrapPayloadId: HexKey?, val groupEventId: HexKey?, + val marmotInnerEventId: HexKey?, val chatRoomId: String, val replyToMessageId: Long? = null, @@ -60,7 +74,8 @@ data class ChatMessage( ): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { companion object { - fun fromGroupEventResult( + suspend fun fromGroupEventResult( + database: TorchDatabase, activeKeyPair: KeyPair, groupEvent: GroupEvent, groupEventResult: GroupEventResult @@ -73,12 +88,25 @@ data class ChatMessage( throw GiftWrapImpersonationException("" + "The innerEvent pubkey (${event?.pubKey}) doesn't match the groupEvent pubkey ${groupEvent.pubKey}") } + database.marmotInnerEventDao().upsert( + MarmotInnerEvent( + id = event.id, + publicKey = event.pubKey, + marmotGroupEventId = groupEvent.id, + tags = event.tags, + content = event.content, + chatRoomId = groupEventResult.groupId, + kind = event.kind, + createdAt = Instant.fromEpochSeconds(event.createdAt) + ) + ) when (event.kind) { ChatEvent.KIND -> { ChatMessage( giftWrapPayloadId = null, messageType = "message", groupEventId = groupEvent.id, + marmotInnerEventId = event.id, senderPublicKey = event.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, chatRoomId = groupEventResult.groupId, @@ -91,6 +119,7 @@ data class ChatMessage( giftWrapPayloadId = null, messageType = "unsupported", groupEventId = groupEvent.id, + marmotInnerEventId = event.id, senderPublicKey = groupEvent.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, chatRoomId = groupEventResult.groupId, @@ -104,6 +133,7 @@ data class ChatMessage( ChatMessage( giftWrapPayloadId = null, groupEventId = groupEvent.id, + marmotInnerEventId = null, messageType = "pendingCommit", senderPublicKey = groupEvent.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, @@ -116,6 +146,7 @@ data class ChatMessage( ChatMessage( giftWrapPayloadId = null, groupEventId = groupEvent.id, + marmotInnerEventId = null, messageType = "processedCommit", senderPublicKey = groupEvent.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, @@ -155,6 +186,7 @@ data class ChatMessage( ChatMessage( giftWrapPayloadId = null, groupEventId = groupEvent.id, + marmotInnerEventId = null, messageType = "proposalStaged", senderPublicKey = groupEvent.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, @@ -167,6 +199,7 @@ data class ChatMessage( ChatMessage( giftWrapPayloadId = null, groupEventId = groupEvent.id, + marmotInnerEventId = null, messageType = "undecryptableOuterLayer", senderPublicKey = groupEvent.pubKey, isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotInnerEvent.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotInnerEvent.kt index d30afcf2..6f97f269 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotInnerEvent.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/model/MarmotInnerEvent.kt @@ -41,13 +41,13 @@ import kotlin.time.Instant ], ) data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload... - @PrimaryKey(autoGenerate = true) - val id: Long = 0, + @PrimaryKey + val id: HexKey, /** * Sender Public Key */ - val publicKey: String, + val publicKey: HexKey, val chatRoomId: String, 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 64e58a28..6f480060 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 @@ -2,7 +2,10 @@ package at.torch.compose.database.repository import at.torch.compose.database.model.ChatMessage import at.torch.compose.database.model.GiftWrapPayload +import at.torch.compose.database.model.MarmotInnerEvent import co.touchlab.kermit.Logger +import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup +import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Kind @@ -11,9 +14,11 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nipC7Chats.ChatEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -99,46 +104,97 @@ class DatabaseChatRepository( messageType: Kind ) { logger.i("sendChatMessage($text): $localChatRoom") - - val receiverTags = localChatRoom.localParticipants.filter { participant -> - participant.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey // TODO: Allow this where it's notes to self... - }.map { localParticipant -> - PTag.assemble( - localParticipant.participant.participantPublicKey, - localParticipant.participant.relayHint?.let { NormalizedRelayUrl(it) } + val mlsGroup = localChatRoom.chatRoom.mlsGroupState?.let { mlsGroupState -> + MlsGroup.restore( + MlsGroupState.decodeTls( + mlsGroupState.hexToByteArray() + ) ) } + if (mlsGroup != null) { + // Create ChatRumor... + val createdAt = Clock.System.now().epochSeconds + val marmotInnerEventKind = if (messageType == ChatMessageEvent.KIND) { + ChatEvent.KIND // nip-17 chatMessageEvent.kind is in MLS chatMessage.kind + } else { + messageType + } - val createdAt = Clock.System.now().epochSeconds - val giftWrapPayloadId = EventHasher.hashId( - pubKey = localChatRoom.chatRoom.userPublicKey, - createdAt = createdAt, - tags = receiverTags.toTypedArray(), - content = text, - kind = messageType - ) + val marmotInnerEventId = EventHasher.hashId( + pubKey = localChatRoom.chatRoom.userPublicKey, + createdAt = createdAt, + tags = emptyArray(), + content = text, + kind = marmotInnerEventKind + ) - database.giftWrapPayloadDao().upsert( - GiftWrapPayload( - id = giftWrapPayloadId, - kind = messageType, + database.marmotInnerEventDao().upsert( + MarmotInnerEvent( + id = marmotInnerEventId, + publicKey = localChatRoom.chatRoom.userPublicKey, + createdAt = Instant.fromEpochSeconds(createdAt), + tags = emptyArray(), + content = text, + chatRoomId = localChatRoom.chatRoom.id, + kind = marmotInnerEventKind + ) + ) + + database.chatMessageDao().upsert( + ChatMessage( + content = text, + chatRoomId = localChatRoom.chatRoom.id, + senderPublicKey = localChatRoom.chatRoom.userPublicKey, + isUserMessage = true, + giftWrapPayloadId = null, + groupEventId = null, + marmotInnerEventId = marmotInnerEventId, + ) + ) + } else { + // Send GiftWrapMessage + val receiverTags = localChatRoom.localParticipants.filter { participant -> + participant.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey // TODO: Allow this where it's notes to self... + }.map { localParticipant -> + PTag.assemble( + localParticipant.participant.participantPublicKey, + localParticipant.participant.relayHint?.let { NormalizedRelayUrl(it) } + ) + } + + val createdAt = Clock.System.now().epochSeconds + val giftWrapPayloadId = EventHasher.hashId( + pubKey = localChatRoom.chatRoom.userPublicKey, + createdAt = createdAt, tags = receiverTags.toTypedArray(), - createdAt = Instant.fromEpochSeconds(createdAt), content = text, - chatRoomId = localChatRoom.chatRoom.id, - publicKey = localChatRoom.chatRoom.userPublicKey + kind = messageType ) - ) - database.chatMessageDao().upsert( - ChatMessage( - content = text, - chatRoomId = localChatRoom.chatRoom.id, - senderPublicKey = localChatRoom.chatRoom.userPublicKey, - isUserMessage = true, - giftWrapPayloadId = giftWrapPayloadId, - groupEventId = null + + 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( + ChatMessage( + content = text, + chatRoomId = localChatRoom.chatRoom.id, + senderPublicKey = localChatRoom.chatRoom.userPublicKey, + isUserMessage = true, + giftWrapPayloadId = giftWrapPayloadId, + groupEventId = null, + marmotInnerEventId = null + ) + ) + } } override fun updateChatRoomSubject(