Observe unprocessed marmot inner events
This commit is contained in:
@@ -5,6 +5,7 @@ import androidx.room3.RoomDatabase
|
||||
import androidx.room3.TypeConverters
|
||||
import androidx.room3.immediateTransaction
|
||||
import androidx.room3.useWriterConnection
|
||||
import at.torch.compose.database.dao.MarmotOutboundDao
|
||||
import at.torch.compose.database.dao.MarmotGroupEventDao
|
||||
import at.torch.compose.database.dao.MarmotInnerEventDao
|
||||
import at.torch.compose.database.dao.MarmotKeyPackageBundleDao
|
||||
@@ -109,6 +110,8 @@ abstract class TorchDatabase: RoomDatabase() {
|
||||
|
||||
abstract fun inReplyToRelationDao(): at.torch.compose.database.dao.InReplyToRelationDao
|
||||
|
||||
abstract fun marmotOutboundDao(): MarmotOutboundDao
|
||||
|
||||
abstract fun marmotGroupEventDao(): MarmotGroupEventDao
|
||||
abstract fun marmotInnerEventDao(): MarmotInnerEventDao
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface MarmotInnerEventDao {
|
||||
|
||||
@Query("SELECT * FROM MarmotInnerEvent WHERE publicKey = :publicKey AND marmotGroupEventId IS NULL")
|
||||
fun observeUnprocessedMarmotInnerEvents(publicKey: String): Flow<MarmotInnerEvent?>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(marmotInnerEvent: MarmotInnerEvent)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package at.torch.compose.database.dao
|
||||
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Transaction
|
||||
import at.torch.compose.database.TorchDatabase
|
||||
import at.torch.compose.database.model.BroadcastNostrEventRequest
|
||||
import at.torch.compose.database.model.MarmotGroupEvent
|
||||
import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration
|
||||
import at.torch.compose.database.model.NostrEvent
|
||||
import at.torch.compose.database.model.intermdiate.LocalChatRoom
|
||||
import at.torch.compose.extensions.exporterSecret
|
||||
import at.torch.compose.nostr.Relays
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEventEncryption
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Dao
|
||||
abstract class MarmotOutboundDao(
|
||||
private val database: TorchDatabase
|
||||
) {
|
||||
val logger = Logger.withTag(TAG)
|
||||
|
||||
@Transaction
|
||||
suspend fun encryptAndSendMarmotInnerEvent(
|
||||
localChatRoom: LocalChatRoom,
|
||||
mlsGroup: MlsGroup,
|
||||
innerEvent: Event,
|
||||
nostrSignerSync: NostrSignerSync
|
||||
) {
|
||||
val plainTextBytes = innerEvent.toJson().encodeToByteArray()
|
||||
|
||||
// Step 1: MLS encrypt
|
||||
val mlsCipherText = mlsGroup.encrypt(plainTextBytes)
|
||||
|
||||
// Step 2: Outer ChaCha20-Poly1305 encryption
|
||||
val exportKey = mlsGroup.exporterSecret()
|
||||
val encryptedContent = GroupEventEncryption.encrypt(
|
||||
mlsMessageBytes = mlsCipherText,
|
||||
groupKey = exportKey
|
||||
)
|
||||
|
||||
// Step 3: Build the GroupEvent template (auto-apply NIP-40 expiration
|
||||
// if the group has disappearing_message_secs configured per MIP-01/03).
|
||||
val createdAt = TimeUtils.now()
|
||||
val expirationTime = disappearingExpiration(mlsGroup, createdAt)
|
||||
|
||||
val template = GroupEvent.build(
|
||||
encryptedContentBase64 = encryptedContent,
|
||||
nostrGroupId = localChatRoom.chatRoom.id,
|
||||
createdAt = createdAt,
|
||||
) {
|
||||
if (expirationTime != null) expiration(expirationTime)
|
||||
}
|
||||
|
||||
// Step 4: Sign with a fresh ephemeral keypair
|
||||
val ephemeralSigner = NostrSignerInternal(KeyPair())
|
||||
val groupEvent: GroupEvent = ephemeralSigner.sign(template)
|
||||
|
||||
database.nostrEventDao().upsert(
|
||||
NostrEvent(
|
||||
id = groupEvent.id,
|
||||
tags = template.tags,
|
||||
createdAt = Instant.fromEpochSeconds(groupEvent.createdAt),
|
||||
content = template.content,
|
||||
sig = groupEvent.sig,
|
||||
kind = groupEvent.kind,
|
||||
pubKey = groupEvent.pubKey
|
||||
)
|
||||
)
|
||||
database.marmotGroupEventDao().upsert(
|
||||
MarmotGroupEvent(
|
||||
id = groupEvent.id,
|
||||
createdAt = Instant.fromEpochSeconds(groupEvent.createdAt),
|
||||
encryptedContent = groupEvent.encryptedContent(),
|
||||
chatRoomId = localChatRoom.chatRoom.id,
|
||||
nostrEventId = groupEvent.id,
|
||||
userPublicKey = nostrSignerSync.pubKey,
|
||||
publicKey = nostrSignerSync.pubKey
|
||||
)
|
||||
)
|
||||
|
||||
// Sync broadcast to all the required relays...
|
||||
database.broadcastNostrEventRequestDao().insert(
|
||||
Relays.DefaultDMRelayList.map { // TODO: Get these from localChatRoom...
|
||||
BroadcastNostrEventRequest(
|
||||
nostrEventId = groupEvent.id,
|
||||
relayURL = it.url
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NostrNip17Dao"
|
||||
}
|
||||
}
|
||||
@@ -339,7 +339,7 @@ abstract class NostrDao(
|
||||
database.profileDao().upsert(profile)
|
||||
}
|
||||
|
||||
// TODO: Support MLSGroupEvent handling...
|
||||
// MLSGroupEvent handling...
|
||||
nostrEvent.toGroupEvent(
|
||||
userPublicKey = activeKeyPair.pubKey.toHex()
|
||||
)?.let { groupEvent ->
|
||||
@@ -624,7 +624,8 @@ abstract class NostrDao(
|
||||
|
||||
// Sync ChatMessageRelayListEvent publicKey...
|
||||
// TODO: Get relayHint form participant...
|
||||
if (profilePublicKeysToSync.containsKey(
|
||||
if (
|
||||
profilePublicKeysToSync.containsKey(
|
||||
relayURL
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
package at.torch.compose.database.repository
|
||||
|
||||
import at.torch.compose.database.TorchDatabase
|
||||
import at.torch.compose.database.model.MarmotInnerEvent
|
||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||
import at.torch.compose.database.model.UnsignedNostrEvent
|
||||
import at.torch.compose.exceptions.MarmotMissingChatGroupException
|
||||
import at.torch.compose.extensions.exporterSecret
|
||||
import at.torch.compose.extensions.toHex
|
||||
import at.torch.compose.nostr.Relays
|
||||
import at.torch.compose.repository.MarmotRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.GroupEventResult
|
||||
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.mip03GroupMessages.CommitOrdering
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEventEncryption
|
||||
import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
|
||||
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.framing.ContentType
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.MlsMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.PrivateMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.PublicMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.WireFormat
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.DecryptedMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.RetainedEpochSecrets
|
||||
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.schedule.KeySchedule
|
||||
import com.vitorpamplona.quartz.marmot.mls.schedule.SecretTree
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Capabilities
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Credential
|
||||
import com.vitorpamplona.quartz.marmot.mls.tree.Extension
|
||||
@@ -42,8 +27,12 @@ import com.vitorpamplona.quartz.marmot.mls.tree.Lifetime
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.time.Instant
|
||||
|
||||
@@ -112,6 +101,43 @@ class DatabaseMarmotRepository(
|
||||
return database.marmotKeyPackageBundleDao().getAllMarmotKeyPackageBundles(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<MarmotInnerEvent?> {
|
||||
return database.marmotInnerEventDao().observeUnprocessedMarmotInnerEvents(publicKey)
|
||||
}
|
||||
|
||||
override suspend fun encryptAndSendMarmotInnerEvent(
|
||||
marmotInnerEvent: MarmotInnerEvent,
|
||||
nostrSignerSync: NostrSignerSync
|
||||
) {
|
||||
database.chatRoomDao().findChatRoomById(marmotInnerEvent.chatRoomId)?.let { localChatRoom ->
|
||||
localChatRoom.chatRoom.mlsGroupState?.let {
|
||||
val mlsGroup = MlsGroup.restore(
|
||||
MlsGroupState.decodeTls(
|
||||
localChatRoom.chatRoom.mlsGroupState.hexToByteArray()
|
||||
)
|
||||
)
|
||||
|
||||
val innerEvent = RumorAssembler.assembleRumor(
|
||||
pubKey = nostrSignerSync.pubKey,
|
||||
ev = EventTemplate(
|
||||
createdAt = marmotInnerEvent.createdAt.epochSeconds,
|
||||
content = marmotInnerEvent.content,
|
||||
tags = marmotInnerEvent.tags,
|
||||
kind = marmotInnerEvent.kind
|
||||
)
|
||||
)
|
||||
|
||||
database.marmotOutboundDao().encryptAndSendMarmotInnerEvent(
|
||||
localChatRoom = localChatRoom,
|
||||
mlsGroup = mlsGroup,
|
||||
innerEvent = innerEvent,
|
||||
nostrSignerSync = nostrSignerSync
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateKeyPackage(
|
||||
identity: ByteArray,
|
||||
dTagSlot: String = KeyPackageUtils.PRIMARY_SLOT,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package at.torch.compose.repository
|
||||
|
||||
import at.torch.compose.database.model.MarmotInnerEvent
|
||||
import at.torch.compose.database.model.MarmotKeyPackageBundle
|
||||
import com.vitorpamplona.quartz.marmot.GroupEventResult
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.MlsMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface MarmotRepository {
|
||||
|
||||
@@ -16,6 +15,10 @@ interface MarmotRepository {
|
||||
|
||||
suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle>
|
||||
|
||||
suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<MarmotInnerEvent?>
|
||||
|
||||
suspend fun encryptAndSendMarmotInnerEvent(marmotInnerEvent: MarmotInnerEvent, nostrSignerSync: NostrSignerSync)
|
||||
|
||||
companion object {
|
||||
val NO_OP_MARMOT_KEY_PACKAGE_BUNDLE = object: MarmotRepository {
|
||||
override suspend fun publishMarmotKeyPackageBundle(
|
||||
@@ -28,6 +31,17 @@ interface MarmotRepository {
|
||||
override suspend fun getMarmotKeyPackageBundles(publicKey: HexKey): List<MarmotKeyPackageBundle> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun observeUnprocessedMarmotInnerEvents(publicKey: HexKey): Flow<MarmotInnerEvent?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun encryptAndSendMarmotInnerEvent(
|
||||
marmotInnerEvent: MarmotInnerEvent,
|
||||
nostrSignerSync: NostrSignerSync
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import at.torch.compose.repository.MarmotRepository
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
@@ -25,6 +26,7 @@ class NotaryViewModel(
|
||||
val activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
val nostrRepository: at.torch.compose.repository.NostrRepository,
|
||||
val chatRepository: at.torch.compose.repository.ChatRepository,
|
||||
val marmotRepository: MarmotRepository,
|
||||
val scope: CoroutineScope,
|
||||
): ViewModel() {
|
||||
|
||||
@@ -36,6 +38,7 @@ class NotaryViewModel(
|
||||
activeWalletStateFlow: StateFlow<ActiveWallet?>,
|
||||
nostrRepository: at.torch.compose.repository.NostrRepository,
|
||||
chatRepository: at.torch.compose.repository.ChatRepository,
|
||||
marmotRepository: MarmotRepository,
|
||||
scope: CoroutineScope,
|
||||
): ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
@@ -43,6 +46,7 @@ class NotaryViewModel(
|
||||
activeWalletStateFlow = activeWalletStateFlow,
|
||||
nostrRepository = nostrRepository,
|
||||
chatRepository = chatRepository,
|
||||
marmotRepository = marmotRepository,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
@@ -63,6 +67,7 @@ class NotaryViewModel(
|
||||
|
||||
observeUnsignedNostrEvents(keyPair)
|
||||
observeUnsealedGiftWrapPayloads(keyPair)
|
||||
observeUnprocessedMarmotInnerEvents(keyPair)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,4 +143,29 @@ class NotaryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeUnprocessedMarmotInnerEvents(
|
||||
keyPair: KeyPair
|
||||
) {
|
||||
val tempSigner = NostrSignerSync(
|
||||
keyPair
|
||||
)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
logger.i { "observeUnsealedGiftWrapPayloads: ${keyPair.pubKey.toHexKey()}" }
|
||||
marmotRepository.observeUnprocessedMarmotInnerEvents(
|
||||
publicKey = keyPair.pubKey.toHexKey()
|
||||
).distinctUntilChanged().collect { marmotInnerEventOrNull ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
marmotInnerEventOrNull?.let { marmotInnerEvent ->
|
||||
logger.d("encrypt and broadcast: $marmotInnerEvent")
|
||||
|
||||
marmotRepository.encryptAndSendMarmotInnerEvent(
|
||||
marmotInnerEvent,
|
||||
nostrSignerSync = tempSigner
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user