From 6116f45888e6e0a47905c25deb8976e06fa533b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 15:55:44 +0000 Subject: [PATCH] feat: add EphemeralGiftWrapEvent (kind 21059) Adds an ephemeral variant of GiftWrap for transient encrypted messages that relays don't need to persist. EphemeralGiftWrapEvent extends GiftWrapEvent so all existing processing paths (decryption, routing, notifications, call signaling) automatically handle it via type hierarchy. Relay subscription filters are updated to request both kinds. https://claude.ai/code/session_0157X96G6HLTzYxkdX9pyTSJ --- .../nip59GiftWraps/FilterGiftWrapsToPubkey.kt | 3 +- .../loggedIn/relays/RelayInformationScreen.kt | 4 +- .../relays/dm/DMRelayListViewModel.kt | 3 +- .../loggedIn/relays/eventsync/EventSync.kt | 6 +- .../desktop/subscriptions/FilterBuilders.kt | 4 +- .../desktop/subscriptions/FilterDMs.kt | 3 +- .../wraps/EphemeralGiftWrapEvent.kt | 87 +++++++++++++++++++ .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 7 +- .../quartz/utils/EventFactory.kt | 2 + 9 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/EphemeralGiftWrapEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt index 081f2e486..f8e1e3f26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt @@ -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()), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index bb6b5e0f0..ca560d561 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index 53a6a5ed0..22b191758 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -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)), ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt index 3858d7397..e7c962442 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSync.kt @@ -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 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt index ecdefb508..9f31007ef 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt @@ -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, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt index e1b9ced84..bb9377f95 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt @@ -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()), ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/EphemeralGiftWrapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/EphemeralGiftWrapEvent.kt new file mode 100644 index 000000000..2a968b941 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/EphemeralGiftWrapEvent.kt @@ -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>, + 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), + ) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index 9f7fcab56..093d1b974 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -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>, 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, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index b792d6f45..48ad5ff4a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -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)