Add marmot commit result
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package at.torch.compose.database.dao
|
||||
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import at.torch.compose.database.model.MarmotCommitResult
|
||||
import at.torch.compose.database.model.MarmotKeyPackage
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.bitcoin.PublicKey
|
||||
|
||||
@Dao
|
||||
interface MarmotCommitResultDao {
|
||||
@Upsert
|
||||
suspend fun upsert(marmotCommitResult: MarmotCommitResult)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import at.torch.compose.database.model.ChatMessageBroadcastNostrEventRequestRela
|
||||
import at.torch.compose.database.model.ChatMessageNostrEventRelation
|
||||
import at.torch.compose.database.model.ChatRoom
|
||||
import at.torch.compose.database.model.GiftWrapPayload
|
||||
import at.torch.compose.database.model.MarmotCommitResult
|
||||
import at.torch.compose.database.model.MarmotGroupEvent
|
||||
import at.torch.compose.database.model.MarmotInnerEvent
|
||||
import at.torch.compose.database.model.MarmotInnerEvent.Companion.disappearingExpiration
|
||||
@@ -27,12 +28,10 @@ import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
|
||||
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
|
||||
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.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
@@ -41,7 +40,6 @@ import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.io.bytestring.encode
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.time.Instant
|
||||
|
||||
@@ -58,7 +56,6 @@ abstract class MarmotOutboundDao(
|
||||
peerKeyPackage: MarmotKeyPackage,
|
||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
||||
): String {
|
||||
// TODO: Might move this logic to a marmotOutboudDao for consistency...
|
||||
val groupId = RandomInstance.bytes(32).toHexKey()
|
||||
val mlsGroup = MlsGroup.create(
|
||||
identity = userPublicKey.hexToByteArray(),
|
||||
@@ -91,10 +88,11 @@ abstract class MarmotOutboundDao(
|
||||
userPublicKey = userPublicKey,
|
||||
peerPublicKey = peerPublicKey,
|
||||
peerKeyPackage = peerKeyPackage,
|
||||
relays = relays
|
||||
relays = relays,
|
||||
isOneMemberInitialGroupCreation = true
|
||||
)
|
||||
|
||||
// TODO: Update chatRoom
|
||||
// Update chatRoom
|
||||
database.chatRoomDao().upsert(
|
||||
chatRoom.copy(
|
||||
mlsGroupState = mlsGroup.saveState().encodeTls().toHex(),
|
||||
@@ -110,7 +108,8 @@ abstract class MarmotOutboundDao(
|
||||
userPublicKey: HexKey,
|
||||
peerPublicKey: HexKey,
|
||||
peerKeyPackage: MarmotKeyPackage,
|
||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url }
|
||||
relays: List<String> = Relays.DefaultDMRelayList.map { it.url },
|
||||
isOneMemberInitialGroupCreation: Boolean
|
||||
) {
|
||||
val keyPackage = MlsKeyPackage.decodeTls(
|
||||
TlsReader(
|
||||
@@ -162,6 +161,22 @@ abstract class MarmotOutboundDao(
|
||||
)
|
||||
)
|
||||
|
||||
// TODO: Save commitResult... in case we need to broadcast welcomeEvent after relay acknowledgement...
|
||||
// database.marmotCommitResultDao().upsert(
|
||||
// MarmotCommitResult(
|
||||
// id = commitEvent.id,
|
||||
// isOneMemberInitialGroupCreation = isOneMemberInitialGroupCreation,
|
||||
// chatRoomId = groupId,
|
||||
// commitBytes = commitResult.commitBytes,
|
||||
// preCommitExporterSecret = commitResult.preCommitExporterSecret,
|
||||
// welcomeBytes = commitResult.welcomeBytes,
|
||||
// framedCommitBytes = commitResult.preCommitExporterSecret,
|
||||
// groupInfoBytes = commitResult.groupInfoBytes,
|
||||
// userPublicKey = userPublicKey,
|
||||
// createdAt = Instant.fromEpochSeconds(commitEvent.createdAt)
|
||||
// )
|
||||
// )
|
||||
|
||||
database.broadcastNostrEventRequestDao().insert(
|
||||
relays.map {
|
||||
BroadcastNostrEventRequest(
|
||||
@@ -171,50 +186,52 @@ abstract class MarmotOutboundDao(
|
||||
}
|
||||
)
|
||||
|
||||
// Send welcome event to participant
|
||||
commitResult.welcomeBytes?.let { welcomeBytes ->
|
||||
val welcomeBase64 = Base64.encode(
|
||||
source = welcomeBytes
|
||||
)
|
||||
|
||||
// GiftWrap...
|
||||
val welcomeEvent = WelcomeEvent.build(
|
||||
welcomeBase64 = welcomeBase64,
|
||||
keyPackageEventId = peerKeyPackage.id,
|
||||
relays = relays.map { NormalizedRelayUrl(it) },
|
||||
nostrGroupId = groupId,
|
||||
)
|
||||
// Compute welcomeEventId
|
||||
val welcomeEventId = EventHasher.hashId(
|
||||
pubKey = userPublicKey,
|
||||
kind = welcomeEvent.kind,
|
||||
tags = welcomeEvent.tags,
|
||||
createdAt = welcomeEvent.createdAt,
|
||||
content = welcomeEvent.content
|
||||
)
|
||||
|
||||
GiftWrapPayload(
|
||||
id = welcomeEventId,
|
||||
publicKey = userPublicKey,
|
||||
chatRoomId = groupId,
|
||||
kind = welcomeEvent.kind,
|
||||
tags = welcomeEvent.tags,
|
||||
content = welcomeEvent.content,
|
||||
createdAt = Instant.fromEpochSeconds(welcomeEvent.createdAt)
|
||||
)
|
||||
|
||||
// Save chatMessage for the invite...
|
||||
database.chatMessageDao().upsert(
|
||||
ChatMessage(
|
||||
content = "Invited participant to chat", // use profile.humanReadable...
|
||||
chatRoomId = groupId,
|
||||
senderPublicKey = userPublicKey,
|
||||
isUserMessage = true, // TODO: This is information message...
|
||||
giftWrapPayloadId = welcomeEventId,
|
||||
marmotGroupEventId = null,
|
||||
marmotInnerEventId = null
|
||||
if (isOneMemberInitialGroupCreation) {
|
||||
// Send welcome event to participant
|
||||
commitResult.welcomeBytes?.let { welcomeBytes ->
|
||||
val welcomeBase64 = Base64.encode(
|
||||
source = welcomeBytes
|
||||
)
|
||||
)
|
||||
|
||||
// GiftWrap...
|
||||
val welcomeEvent = WelcomeEvent.build(
|
||||
welcomeBase64 = welcomeBase64,
|
||||
keyPackageEventId = peerKeyPackage.id,
|
||||
relays = relays.map { NormalizedRelayUrl(it) },
|
||||
nostrGroupId = groupId,
|
||||
)
|
||||
// Compute welcomeEventId
|
||||
val welcomeEventId = EventHasher.hashId(
|
||||
pubKey = userPublicKey,
|
||||
kind = welcomeEvent.kind,
|
||||
tags = welcomeEvent.tags,
|
||||
createdAt = welcomeEvent.createdAt,
|
||||
content = welcomeEvent.content
|
||||
)
|
||||
|
||||
GiftWrapPayload(
|
||||
id = welcomeEventId,
|
||||
publicKey = userPublicKey,
|
||||
chatRoomId = groupId,
|
||||
kind = welcomeEvent.kind,
|
||||
tags = welcomeEvent.tags,
|
||||
content = welcomeEvent.content,
|
||||
createdAt = Instant.fromEpochSeconds(welcomeEvent.createdAt)
|
||||
)
|
||||
|
||||
// Save chatMessage for the invite...
|
||||
database.chatMessageDao().upsert(
|
||||
ChatMessage(
|
||||
content = "Invited participant to chat", // use profile.humanReadable...
|
||||
chatRoomId = groupId,
|
||||
senderPublicKey = userPublicKey,
|
||||
isUserMessage = true, // TODO: This is information message...
|
||||
giftWrapPayloadId = welcomeEventId,
|
||||
marmotGroupEventId = null,
|
||||
marmotInnerEventId = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import kotlin.time.Instant
|
||||
)
|
||||
],
|
||||
)
|
||||
data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
|
||||
data class GiftWrapPayload(
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package at.torch.compose.database.model
|
||||
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.Ignore
|
||||
import androidx.room3.PrimaryKey
|
||||
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 co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData
|
||||
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.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
|
||||
],
|
||||
)
|
||||
data class MarmotCommitResult( // TODO: Rename this to GiftWrapPayload...
|
||||
@PrimaryKey
|
||||
val id: HexKey,
|
||||
|
||||
val userPublicKey: HexKey,
|
||||
|
||||
val chatRoomId: HexKey,
|
||||
|
||||
val isOneMemberInitialGroupCreation: Boolean,
|
||||
|
||||
/**
|
||||
* commitBytes is the raw TLS-encoded Commit struct (RFC 9420 §12.4)
|
||||
*/
|
||||
val commitBytes: ByteArray,
|
||||
val welcomeBytes: ByteArray?,
|
||||
val groupInfoBytes: ByteArray?,
|
||||
/**
|
||||
* Fully-framed commit ready for the MIP-03 outer ChaCha20 encryption.
|
||||
* Wire format: MlsMessage(version=mls10, wireFormat=mls_public_message, payload=PublicMessage(...)).
|
||||
*/
|
||||
val framedCommitBytes: ByteArray = commitBytes,
|
||||
/**
|
||||
* `MLS-Exporter("marmot", "group-event", 32)` evaluated at the **pre-commit**
|
||||
* epoch (N) — the key the group had when this commit was computed, before
|
||||
* local state advanced to N+1. Publishers of the kind:445 MUST ChaCha20-wrap
|
||||
* the commit with this key (RFC 9420 §12.4 and MDK parity). Using the
|
||||
* post-commit (N+1) key makes the commit unreadable to existing members
|
||||
* still at epoch N — the exact scenario that caused Eden to stall at
|
||||
* epoch 1 and never decrypt any of David's subsequent messages.
|
||||
*
|
||||
* Empty by default for the test-only entry points that don't need it.
|
||||
*/
|
||||
val preCommitExporterSecret: ByteArray = ByteArray(0),
|
||||
|
||||
/**
|
||||
* Current time
|
||||
*/
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = createdAt,
|
||||
override val savedAt: Instant = Clock.System.now(),
|
||||
override val viewedAt: Instant? = null,
|
||||
override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
|
||||
): TimestampedEntity,
|
||||
LocalStoreEntity,
|
||||
UserViewableEntity,
|
||||
SoftDeletableEntity {
|
||||
@Ignore
|
||||
private val logger = Logger.withTag(TAG)
|
||||
|
||||
|
||||
companion object {
|
||||
const val TAG = "MarmotCommitResult"
|
||||
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || this::class != other::class) return false
|
||||
|
||||
other as MarmotCommitResult
|
||||
|
||||
if (isOneMemberInitialGroupCreation != other.isOneMemberInitialGroupCreation) return false
|
||||
if (id != other.id) return false
|
||||
if (userPublicKey != other.userPublicKey) return false
|
||||
if (chatRoomId != other.chatRoomId) return false
|
||||
if (!commitBytes.contentEquals(other.commitBytes)) return false
|
||||
if (!welcomeBytes.contentEquals(other.welcomeBytes)) return false
|
||||
if (!groupInfoBytes.contentEquals(other.groupInfoBytes)) return false
|
||||
if (!framedCommitBytes.contentEquals(other.framedCommitBytes)) return false
|
||||
if (!preCommitExporterSecret.contentEquals(other.preCommitExporterSecret)) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (updatedAt != other.updatedAt) return false
|
||||
if (savedAt != other.savedAt) return false
|
||||
if (viewedAt != other.viewedAt) return false
|
||||
if (deletedAt != other.deletedAt) return false
|
||||
if (logger != other.logger) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isOneMemberInitialGroupCreation.hashCode()
|
||||
result = 31 * result + id.hashCode()
|
||||
result = 31 * result + userPublicKey.hashCode()
|
||||
result = 31 * result + chatRoomId.hashCode()
|
||||
result = 31 * result + commitBytes.contentHashCode()
|
||||
result = 31 * result + (welcomeBytes?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (groupInfoBytes?.contentHashCode() ?: 0)
|
||||
result = 31 * result + framedCommitBytes.contentHashCode()
|
||||
result = 31 * result + preCommitExporterSecret.contentHashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + updatedAt.hashCode()
|
||||
result = 31 * result + savedAt.hashCode()
|
||||
result = 31 * result + (viewedAt?.hashCode() ?: 0)
|
||||
result = 31 * result + (deletedAt?.hashCode() ?: 0)
|
||||
result = 31 * result + logger.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
|
||||
|
||||
|
||||
companion object {
|
||||
const val TAG = "GiftWrapPayload"
|
||||
const val TAG = "MarmotInnerEvent"
|
||||
|
||||
/**
|
||||
* Compute the NIP-40 expiration timestamp for outbound application messages,
|
||||
|
||||
Reference in New Issue
Block a user