Merge pull request #2074 from vitorpamplona/claude/add-ephemeral-giftwrap-event-PiIXY

Add support for NIP-59 Ephemeral Gift Wrap events (kind 21059)
This commit is contained in:
Vitor Pamplona
2026-04-02 12:00:24 -04:00
committed by GitHub
9 changed files with 109 additions and 10 deletions
@@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -39,7 +40,7 @@ fun filterGiftWrapsToPubkey(
relay = relay,
filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND),
kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND),
tags = mapOf("p" to listOf(pubkey)),
since = since?.minus(TimeUtils.twoDays()),
),
@@ -238,6 +238,7 @@ import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
@@ -615,6 +616,7 @@ fun kindDisplayName(kind: Int): Int =
GenericRepostEvent.KIND -> R.string.kind_reposts_16
GeohashListEvent.KIND -> R.string.kind_geohash_follows
GiftWrapEvent.KIND -> R.string.kind_gift_wraps
EphemeralGiftWrapEvent.KIND -> R.string.kind_gift_wraps
GitIssueEvent.KIND -> R.string.kind_git_issue
GitPatchEvent.KIND -> R.string.kind_git_patch
GitRepositoryEvent.KIND -> R.string.kind_git_repo
@@ -693,7 +695,7 @@ fun kindDisplayName(kind: Int): Int =
val posts = setOf(0, 1, 6, 7, 16, 30023)
val settings = setOf(3, 10002, 10000, 10001, 10003, 10004, 30000)
val dms = setOf(4, 1059, 10050)
val dms = setOf(4, GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, 10050)
val zaps = setOf(9734, 9735, 9041, 17375, 23194, 23195)
val reports = setOf(ReportEvent.KIND, MuteListEvent.KIND, DeletionEvent.KIND, RequestToVanishEvent.KIND)
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
@Stable
@@ -43,7 +44,7 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() {
label = R.string.dms,
filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND),
kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(account.pubKey)),
),
),
@@ -38,6 +38,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -55,7 +57,7 @@ import kotlin.coroutines.cancellation.CancellationException
* Syncs the user's events across all known relays:
* 1. Downloads all events authored by the user and sends them to their outbox relays.
* 2. Downloads all events that p-tag the user (non-DM) and sends them to their inbox relays.
* 3. Downloads kind-4 and kind-1059 events that p-tag the user and sends them to DM relays.
* 3. Downloads kind-4, kind-1059 and kind-21059 events that p-tag the user and sends them to DM relays.
*
* Up to [MAX_CONCURRENT_RELAYS] relays are queried in parallel. As soon as one relay is fully
* exhausted (all pages retrieved) the next relay from the list starts immediately, keeping the
@@ -512,7 +514,7 @@ class EventSync(
onEvent = { event, sourceRelay ->
val isMyEvent = event.pubKey == myPubKey
val mentionsMe = event.tags.isTaggedUser(myPubKey)
val isDmKind = event.kind == 4 || event.kind == 1059
val isDmKind = event.kind == 4 || event.kind == GiftWrapEvent.KIND || event.kind == EphemeralGiftWrapEvent.KIND
val live = liveActivity.value
@@ -21,6 +21,8 @@
package com.vitorpamplona.amethyst.desktop.subscriptions
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
/**
* Type-safe builders for common Nostr filter patterns.
@@ -540,7 +542,7 @@ object FilterBuilders {
since: Long? = null,
): Filter =
Filter(
kinds = listOf(1059), // GiftWrapEvent.KIND
kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND),
tags = mapOf("p" to listOf(pubKeyHex)),
since = since,
)
@@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -126,7 +127,7 @@ object FilterDMs {
since: Long? = null,
): Filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND),
kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND),
tags = mapOf("p" to listOf(userPubKeyHex)),
since = since?.minus(TimeUtils.twoDays()),
)
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip59Giftwrap.wraps
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class EphemeralGiftWrapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : GiftWrapEvent(id, pubKey, createdAt, tags, content, sig, KIND) {
override fun copyNoContent(): EphemeralGiftWrapEvent {
val copy =
EphemeralGiftWrapEvent(
id,
pubKey,
createdAt,
tags,
"",
sig,
)
copy.innerEventId = innerEventId
return copy
}
companion object {
const val KIND = 21059
const val ALT = "Ephemeral encrypted event"
fun create(
event: Event,
recipientPubKey: HexKey,
expirationDelta: Long? = null,
createdAt: Long = TimeUtils.randomWithTwoDays(),
): EphemeralGiftWrapEvent {
val signer = NostrSignerSync(KeyPair())
val tags =
expirationDelta?.let {
arrayOf(
PTag.assemble(recipientPubKey, null),
ExpirationTag.assemble(createdAt + it + TimeUtils.twoDays()),
)
} ?: arrayOf(
PTag.assemble(recipientPubKey, null),
)
return signer.sign(
createdAt = createdAt,
kind = KIND,
tags = tags,
content = signer.nip44Encrypt(event.toJson(), recipientPubKey),
)
}
}
}
@@ -36,19 +36,20 @@ import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
class GiftWrapEvent(
open class GiftWrapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
kind: Int = KIND,
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
var innerEventId: HexKey? = null
fun copyNoContent(): GiftWrapEvent {
open fun copyNoContent(): GiftWrapEvent {
val copy =
GiftWrapEvent(
id,
@@ -159,6 +159,7 @@ import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
@@ -384,6 +385,7 @@ class EventFactory {
GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig)
GeohashListEvent.KIND -> GeohashListEvent(id, pubKey, createdAt, tags, content, sig)
GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
EphemeralGiftWrapEvent.KIND -> EphemeralGiftWrapEvent(id, pubKey, createdAt, tags, content, sig)
GitAuthorListEvent.KIND -> GitAuthorListEvent(id, pubKey, createdAt, tags, content, sig)
GitRepositoryListEvent.KIND -> GitRepositoryListEvent(id, pubKey, createdAt, tags, content, sig)
GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig)