Save inbound marmotInnerEvent

This commit is contained in:
Kgothatso Ngako
2026-07-03 22:50:50 +02:00
parent 0f1d64baec
commit 225606d724
6 changed files with 207 additions and 68 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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...
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(