feat: apply the nip30303 event a submission carries, keeping its author

Teach the receiving side to open an envelope before anything starts
sending one. In that order a client that has this can already handle
submissions from a client that does not yet send them; the reverse would
turn every artifact, dialect and chapter into an "unsupported" row for
anyone who had not updated.

Despite the name, MarmotInboundManager does not dispatch on inner-event
kinds -- it decrypts MLS and hands back a GroupEventResult. The kind
dispatch has always lived in ChatMessage.fromGroupEventResult, so that
is where support for a new kind goes.

The `when (event.kind)` body becomes applyInnerEvent, which takes the
event to apply separately from how it arrived:

    event                 the nip30303 event, written by whoever wrote
                          it -- possibly nobody in this group
    marmotInnerEventId    the row the group actually delivered
    senderPublicKey       the member who delivered it
    createdAt             when they did

For a plain nip30303 event those all come from the one event, which is
exactly the old behaviour. For a submission they come from the envelope
while `event` is the payload. Entity rows take their author from the
payload via fromXEvent, so the chat line says who added something and
the row says who wrote it -- the point of the envelope, made real at the
only place it can be.

createdAt deliberately follows the envelope rather than the payload: a
submitted archive translation can be years old, and sorting the group's
transcript by when the source was written would file "X added a
translation" somewhere nobody will scroll to.

The stored MarmotInnerEvent stays the outer event -- that is what the
group sent -- and gains payloadEventId naming what it carries. The
payload is not given a row of its own: it is recoverable from the
submission's content, and a second row with a null marmotGroupEventId
would look to the outbound pipeline like something waiting to be sent.
Nullable column, so AutoMigration(4, 5) is all it needs; rumors queued
before this read back null, which is correct, since none of them were
submissions.

Two submissions are stored but not applied, because there is nothing in
them to make a row from: one whose payload will not parse, and one
carrying another submission. Both surface as "unsupported" rather than
disappearing.

The unsupported fallback also stops attributing to groupEvent.pubKey,
which is the ephemeral key every kind:445 is signed with and so names
nobody.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-05 20:21:25 +02:00
parent fa380e94e1
commit ce77b77240
4 changed files with 5429 additions and 288 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -164,7 +164,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
UnsignedNostrEvent::class,
Zap::class
],
version = 4,
version = 5,
autoMigrations = [
// v2 only adds the DkgSession/DkgParticipantMessage tables, so Room can
// generate the migration itself — nothing existing changes shape.
@@ -173,11 +173,17 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
// additions need no default and drop no data, so Room generates this one
// too. Rituals already in flight come back with all three null, which reads
// as "not approved yet" and simply asks the member for each step.
AutoMigration(from = 2, to = 3)
AutoMigration(from = 2, to = 3),
// v4 changes no schema at all -- it rewrites `dkgApprovalNeeded` chat rows
// into one type per ritual step. Data, not shape, so it is a manual
// migration passed to the builder rather than an entry here. See
// MIGRATION_3_4.
//
// v5 adds the nullable MarmotInnerEvent.payloadEventId, which names the
// nip30303 event a SubmissionEvent rumor carries. Rumors queued before
// this come back null, which reads as "not a submission" -- correct,
// since none of them were.
AutoMigration(from = 4, to = 5)
]
)
@ColumnTypeConverters(MantraConverters::class)

View File

@@ -22,6 +22,7 @@ import press.mantra.compose.nostr.nip30303.ArtifactVersionEvent
import press.mantra.compose.nostr.nip30303.ChapterEvent
import press.mantra.compose.nostr.nip30303.ChunkEvent
import press.mantra.compose.nostr.nip30303.DialectEvent
import press.mantra.compose.nostr.nip30303.SubmissionEvent
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionContributorListEvent
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
import press.mantra.compose.nostr.nip30303.TranslationChapterEvent
@@ -199,6 +200,25 @@ data class ChatMessage(
val event = Event.fromJsonOrNull(groupEventResult.innerEventJson)
?: throw MarmotUnprocessableInnerEventException("Can't process ${groupEventResult.innerEventJson}")
// A submission is an envelope: the nip30303 event it delivers
// is in its content, written by whoever wrote it. Everything
// else on the wire is the nip30303 event itself. Either way the
// row on disk is the outer event -- that is what the group sent
// and what the chat line is attributed to.
val submission = if (event.kind == SubmissionEvent.KIND) {
SubmissionEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig,
)
} else {
null
}
val payload = submission?.payload()
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = event.id,
@@ -208,296 +228,37 @@ data class ChatMessage(
content = event.content,
chatRoomId = groupEventResult.groupId,
kind = event.kind,
payloadEventId = payload?.id,
createdAt = Instant.fromEpochSeconds(event.createdAt)
)
)
when (event.kind) {
ChatEvent.KIND -> {
ChatMessage(
giftWrapPayloadId = null,
messageType = "message",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = event.content, // TODO: Figure out what to do here...
)
}
ArtifactEvent.KIND -> {
MantraArtifact.fromArtifactEvent(
artifactEvent = ArtifactEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig,
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraArtifact ->
database.mantraArtifactDao().upsert(
mantraArtifact.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "artifact",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraArtifact.name} to artifacts"
)
}
}
ArtifactVersionEvent.KIND -> {
MantraArtifactVersion.fromArtifactVersionEvent(
artifactVersionEvent = ArtifactVersionEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraArtifactVersion ->
database.mantraArtifactVersionDao().upsert(
mantraArtifactVersion.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "artifactVersion",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraArtifactVersion.versionLabel} to artifact versions" // TODO: Use artifact name...
)
}
}
ChapterEvent.KIND -> {
MantraChapter.fromChapterEvent(
chapterEvent = ChapterEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraChapter ->
database.mantraChapterDao().upsert(
mantraChapter.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "chapter",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraChapter.name} to chapters" // TODO: Use artifact name...
)
}
}
ChunkEvent.KIND -> {
MantraChunk.fromChunkEvent(
chunkEvent = ChunkEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraChunk ->
database.mantraChunkDao().upsert(
mantraChunk.copy(
marmotGroupEventId = groupEvent.id,
)
)
// TODO: Chunks might be too noisy to show in chat...
}
null
}
DialectEvent.KIND -> {
MantraDialect.fromDialectEvent(
dialectEvent = DialectEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraDialect ->
database.mantraDialectDao().upsert(
mantraDialect.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "dialect",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraDialect.name} to dialects" // TODO: Use artifact name...
)
}
}
TranslationArtifactVersionEvent.KIND -> {
MantraTranslationArtifactVersion.fromTranslationArtifactVersionEvent(
translationArtifactVersionEvent = TranslationArtifactVersionEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraTranslationArtifactVersion ->
database.mantraTranslationArtifactVersionDao().upsert(
mantraTranslationArtifactVersion.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "translationArtifactVersion",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraTranslationArtifactVersion.name} to translation artifact versions" // TODO: Use artifact name...
)
}
}
TranslationArtifactVersionContributorListEvent.KIND -> {
// TODO: Consume contributors...
null
}
TranslationChapterEvent.KIND -> {
MantraTranslationChapter.fromTranslationChapterEvent(
translationChapterEvent = TranslationChapterEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraTranslationChapter ->
database.mantraTranslationChapterDao().upsert(
mantraTranslationChapter.copy(
marmotGroupEventId = groupEvent.id,
)
)
// TODO: translation chapter might be too noisy for chat updates
}
null
}
TranslationChunkEvent.KIND -> {
MantraTranslationChunk.fromTranslationChunkEvent(
translationChunkEvent = TranslationChunkEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraTranslationChunk ->
database.mantraTranslationChunkDao().upsert(
mantraTranslationChunk.copy(
marmotGroupEventId = groupEvent.id,
)
)
}
null
}
TranslationContributorListEvent.KIND -> {
// TODO: Consume contributors...
null
}
TranslationEvent.KIND -> {
MantraTranslation.fromTranslationEvent(
translationEvent = TranslationEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupEventResult.groupId,
)?.let { mantraTranslation ->
database.mantraTranslationDao().upsert(
mantraTranslation.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "translation",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = "Added ${mantraTranslation.text} to translations" // TODO: Reference original text
)
}
}
else -> {
ChatMessage(
giftWrapPayloadId = null,
messageType = "unsupported",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = groupEvent.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(groupEvent.createdAt),
content = groupEventResult.innerEventJson, // TODO: Figure out what to do here...
)
}
// A submission whose payload will not parse, or which carries
// another submission, is kept but not applied: there is nothing
// here we can turn into a row.
if (submission != null && (payload == null || payload.kind == SubmissionEvent.KIND)) {
ChatMessage(
giftWrapPayloadId = null,
messageType = "unsupported",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupEventResult.groupId,
createdAt = Instant.fromEpochSeconds(event.createdAt),
content = event.content,
)
} else {
applyInnerEvent(
database = database,
activeKeyPair = activeKeyPair,
groupEvent = groupEvent,
groupId = groupEventResult.groupId,
event = payload ?: event,
marmotInnerEventId = event.id,
senderPublicKey = event.pubKey,
createdAt = Instant.fromEpochSeconds(event.createdAt),
)
}
}
is GroupEventResult.CommitPending -> {
@@ -581,5 +342,317 @@ data class ChatMessage(
}
}
}
/**
* Apply one nip30303 [event] the group delivered, and describe it for
* the transcript.
*
* [event] is what is being applied; the other parameters are how it
* arrived. They come apart for submissions: the payload is written by
* whoever wrote it -- possibly nobody in this group -- while
* [marmotInnerEventId], [senderPublicKey] and [createdAt] all belong to
* the envelope a member actually sent. Entity rows take their author
* from [event], so the chat line says who added it and the row says who
* wrote it.
*/
private suspend fun applyInnerEvent(
database: MantraDatabase,
activeKeyPair: KeyPair,
groupEvent: GroupEvent,
groupId: String,
event: Event,
marmotInnerEventId: HexKey,
senderPublicKey: HexKey,
createdAt: Instant,
): ChatMessage? {
return when (event.kind) {
ChatEvent.KIND -> {
ChatMessage(
giftWrapPayloadId = null,
messageType = "message",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = event.content, // TODO: Figure out what to do here...
)
}
ArtifactEvent.KIND -> {
MantraArtifact.fromArtifactEvent(
artifactEvent = ArtifactEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig,
),
chatRoomId = groupId,
)?.let { mantraArtifact ->
database.mantraArtifactDao().upsert(
mantraArtifact.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "artifact",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraArtifact.name} to artifacts"
)
}
}
ArtifactVersionEvent.KIND -> {
MantraArtifactVersion.fromArtifactVersionEvent(
artifactVersionEvent = ArtifactVersionEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraArtifactVersion ->
database.mantraArtifactVersionDao().upsert(
mantraArtifactVersion.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "artifactVersion",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraArtifactVersion.versionLabel} to artifact versions" // TODO: Use artifact name...
)
}
}
ChapterEvent.KIND -> {
MantraChapter.fromChapterEvent(
chapterEvent = ChapterEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraChapter ->
database.mantraChapterDao().upsert(
mantraChapter.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "chapter",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraChapter.name} to chapters" // TODO: Use artifact name...
)
}
}
ChunkEvent.KIND -> {
MantraChunk.fromChunkEvent(
chunkEvent = ChunkEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraChunk ->
database.mantraChunkDao().upsert(
mantraChunk.copy(
marmotGroupEventId = groupEvent.id,
)
)
// TODO: Chunks might be too noisy to show in chat...
}
null
}
DialectEvent.KIND -> {
MantraDialect.fromDialectEvent(
dialectEvent = DialectEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraDialect ->
database.mantraDialectDao().upsert(
mantraDialect.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "dialect",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraDialect.name} to dialects" // TODO: Use artifact name...
)
}
}
TranslationArtifactVersionEvent.KIND -> {
MantraTranslationArtifactVersion.fromTranslationArtifactVersionEvent(
translationArtifactVersionEvent = TranslationArtifactVersionEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraTranslationArtifactVersion ->
database.mantraTranslationArtifactVersionDao().upsert(
mantraTranslationArtifactVersion.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "translationArtifactVersion",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraTranslationArtifactVersion.name} to translation artifact versions" // TODO: Use artifact name...
)
}
}
TranslationArtifactVersionContributorListEvent.KIND -> {
// TODO: Consume contributors...
null
}
TranslationChapterEvent.KIND -> {
MantraTranslationChapter.fromTranslationChapterEvent(
translationChapterEvent = TranslationChapterEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraTranslationChapter ->
database.mantraTranslationChapterDao().upsert(
mantraTranslationChapter.copy(
marmotGroupEventId = groupEvent.id,
)
)
// TODO: translation chapter might be too noisy for chat updates
}
null
}
TranslationChunkEvent.KIND -> {
MantraTranslationChunk.fromTranslationChunkEvent(
translationChunkEvent = TranslationChunkEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraTranslationChunk ->
database.mantraTranslationChunkDao().upsert(
mantraTranslationChunk.copy(
marmotGroupEventId = groupEvent.id,
)
)
}
null
}
TranslationContributorListEvent.KIND -> {
// TODO: Consume contributors...
null
}
TranslationEvent.KIND -> {
MantraTranslation.fromTranslationEvent(
translationEvent = TranslationEvent(
id = event.id,
pubKey = event.pubKey,
createdAt = event.createdAt,
tags = event.tags,
content = event.content,
sig = event.sig
),
chatRoomId = groupId,
)?.let { mantraTranslation ->
database.mantraTranslationDao().upsert(
mantraTranslation.copy(
marmotGroupEventId = groupEvent.id,
)
)
ChatMessage(
giftWrapPayloadId = null,
messageType = "translation",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = "Added ${mantraTranslation.text} to translations" // TODO: Reference original text
)
}
}
else -> {
ChatMessage(
giftWrapPayloadId = null,
messageType = "unsupported",
marmotGroupEventId = groupEvent.id,
marmotInnerEventId = marmotInnerEventId,
senderPublicKey = senderPublicKey,
isUserMessage = activeKeyPair.pubKey.toHex() == groupEvent.pubKey,
chatRoomId = groupId,
createdAt = createdAt,
content = event.toJson(),
)
}
}
}
}
}

View File

@@ -64,6 +64,15 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
val content: String,
val quotedEventId: String? = null,
/**
* For a SubmissionEvent rumor, the id of the nip30303 event it carries.
*
* The submission's own id is derived from the envelope, so it is the only
* handle the queue has on what is actually being submitted. Null for every
* other kind, where the row *is* the event.
*/
val payloadEventId: HexKey? = null,
/**
* Associated MarmotGroupEvent
*/
@@ -117,6 +126,7 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
if (!tags.contentDeepEquals(other.tags)) return false
if (content != other.content) return false
if (quotedEventId != other.quotedEventId) return false
if (payloadEventId != other.payloadEventId) return false
if (marmotGroupEventId != other.marmotGroupEventId) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
@@ -136,6 +146,7 @@ data class MarmotInnerEvent( // TODO: Rename this to GiftWrapPayload...
result = 31 * result + tags.contentDeepHashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (quotedEventId?.hashCode() ?: 0)
result = 31 * result + (payloadEventId?.hashCode() ?: 0)
result = 31 * result + (marmotGroupEventId?.hashCode() ?: 0)
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()