From 799ebd692f599e2f88774c56fd61e2b2a2872247 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 22:11:19 +0000 Subject: [PATCH] feat: switch NIP-AC from GiftWrap with expiration to EphemeralGiftWrap Use EphemeralGiftWrapEvent (kind 21059) instead of GiftWrapEvent (kind 1059) for WebRTC call signaling. The ephemeral kind signals to relays that these events are transient and should not be persisted, eliminating the need for expiration tags on both inner signaling events and outer wraps. Changes: - Remove expiration tags from all 6 call event types (25050-25055) - Switch WebRtcCallFactory to produce EphemeralGiftWrapEvent wraps - Update CallManager and CallController publishEvent types - Update Account.publishCallSignaling signature - Use CallManager.MAX_EVENT_AGE_SECONDS for staleness checks - Update NIP-AC spec to document EphemeralGiftWrap usage - Remove expiration-related tests https://claude.ai/code/session_014kyBgZx7cNyeUXYWV25M4j --- .../vitorpamplona/amethyst/model/Account.kt | 3 +- .../amethyst/service/call/CallController.kt | 4 +- .../EventNotificationConsumer.kt | 3 +- .../amethyst/commons/call/CallManager.kt | 4 +- .../quartz/nipACWebRtcCalls/NIP-AC.md | 52 ++++++++----------- .../nipACWebRtcCalls/WebRtcCallFactory.kt | 44 +++++++--------- .../events/CallAnswerEvent.kt | 11 +--- .../events/CallHangupEvent.kt | 11 +--- .../events/CallIceCandidateEvent.kt | 7 +-- .../nipACWebRtcCalls/events/CallOfferEvent.kt | 11 +--- .../events/CallRejectEvent.kt | 11 +--- .../events/CallRenegotiateEvent.kt | 11 +--- .../quartz/nipACWebRtcCalls/CallEventsTest.kt | 33 ------------ 13 files changed, 62 insertions(+), 143 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 457f669da..79f1a2e9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -191,6 +191,7 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -1705,7 +1706,7 @@ class Account( suspend fun createStatus(newStatus: String) = sendMyPublicAndPrivateOutbox(UserStatusAction.create(newStatus, signer)) - suspend fun publishCallSignaling(wrap: GiftWrapEvent) { + suspend fun publishCallSignaling(wrap: EphemeralGiftWrapEvent) { val relayList = computeRelayListToBroadcast(wrap) client.publish(wrap, relayList) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt index 8d8eca8e0..c9a195956 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.WebRtcCallFactory import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent @@ -77,7 +77,7 @@ class CallController( private val context: Context, val callManager: CallManager, private val scope: CoroutineScope, - private val publishWrap: suspend (GiftWrapEvent) -> Unit, + private val publishWrap: suspend (EphemeralGiftWrapEvent) -> Unit, private val signerProvider: suspend () -> com.vitorpamplona.quartz.nip01Core.signers.NostrSigner, ) { // ---- Per-peer session state ---- diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index df24bc1b7..86677a0f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -31,6 +31,7 @@ import coil3.request.allowHardware import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.call.CallManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -636,7 +637,7 @@ class EventNotificationConsumer( ) { if (!account.isFollowing(event.pubKey)) return - if (TimeUtils.now() - event.createdAt > CallOfferEvent.EXPIRATION_SECONDS) return + if (TimeUtils.now() - event.createdAt > CallManager.MAX_EVENT_AGE_SECONDS) return val callerUser = LocalCache.getUserIfExists(event.pubKey) val callerName = callerUser?.toBestDisplayName() ?: event.pubKey.take(8) + "..." diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt index 055f664e9..f5493923b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/call/CallManager.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.call import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.WebRtcCallFactory import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent @@ -46,7 +46,7 @@ class CallManager( private val signer: NostrSigner, private val scope: CoroutineScope, private val isFollowing: (HexKey) -> Boolean, - private val publishEvent: (GiftWrapEvent) -> Unit, + private val publishEvent: (EphemeralGiftWrapEvent) -> Unit, ) { private val factory = WebRtcCallFactory() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md index 58a30a365..7a26de46f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/NIP-AC.md @@ -15,15 +15,17 @@ NAT traversal — no custom server infrastructure is required. The protocol works as follows: 1. **Caller** creates a signed call offer event containing an SDP offer -2. The event is **gift-wrapped** ([NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md)) and published to relays +2. The event is wrapped in an **Ephemeral Gift Wrap** (kind `21059`) and published to relays 3. **Callee** unwraps the event, verifies the signature, and decides whether to accept 4. If accepted, callee sends back a gift-wrapped call answer event containing an SDP answer 5. Both parties exchange **ICE candidates** as gift-wrapped events for NAT traversal 6. A **direct WebRTC peer connection** is established for audio/video -All signaling events MUST be gift-wrapped using [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md) for metadata privacy. -Events are signed by the sender's key and wrapped directly (without the seal layer) — the gift wrap's -random ephemeral key already hides the sender from relay operators. +All signaling events MUST be delivered using **Ephemeral Gift Wraps** (kind `21059`), an ephemeral +variant of [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md) Gift Wraps. Events are +signed by the sender's key and wrapped directly (without the seal layer) — the ephemeral gift wrap's +random ephemeral key already hides the sender from relay operators. Relays SHOULD treat kind `21059` +events as ephemeral and not persist them to long-term storage. ## Event Kinds @@ -44,7 +46,6 @@ All signaling events MUST include: |---------------|-------------------------------------------------------|----------| | `p` | Hex pubkey of the recipient (group calls: one per member) | YES | | `call-id` | UUID identifying the call session | YES | -| `expiration` | Unix timestamp ([NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md)), SHOULD be `created_at + 20` seconds | YES | | `alt` | Human-readable description ([NIP-31](https://github.com/nostr-protocol/nips/blob/master/31.md)) | YES | Additional tags for **Call Offer** (kind 25050): @@ -69,7 +70,6 @@ The `content` field contains the SDP offer string. ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], ["call-type", "video"], - ["expiration", "1234567910"], ["alt", "WebRTC call offer"] ], "id": "", @@ -90,7 +90,6 @@ The `content` field contains the SDP answer string. "tags": [ ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], - ["expiration", "1234567915"], ["alt", "WebRTC call answer"] ], "id": "", @@ -111,7 +110,6 @@ The `content` field contains the ICE candidate as a JSON string with the fields "tags": [ ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], - ["expiration", "1234567916"], ["alt", "WebRTC ICE candidate"] ], "id": "", @@ -132,7 +130,6 @@ The `content` field MAY contain a human-readable reason or be empty. "tags": [ ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], - ["expiration", "1234568020"], ["alt", "WebRTC call hangup"] ], "id": "", @@ -153,7 +150,6 @@ The `content` field MAY contain a reason or be empty. "tags": [ ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], - ["expiration", "1234567913"], ["alt", "WebRTC call rejection"] ], "id": "", @@ -174,7 +170,6 @@ Used for mid-call changes such as toggling video on/off. The `content` field con "tags": [ ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], - ["expiration", "1234568120"], ["alt", "WebRTC call renegotiation"] ], "id": "", @@ -184,21 +179,22 @@ Used for mid-call changes such as toggling video on/off. The `content` field con ## Encryption and Delivery -All signaling events MUST be delivered using [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md) Gift Wraps: +All signaling events MUST be delivered using **Ephemeral Gift Wraps** (kind `21059`): 1. **Sign** the signaling event with the sender's key -2. **Gift-wrap** the signed event directly using `GiftWrapEvent` (kind 1059) with NIP-44 encryption -3. **Set expiration on the gift wrap** — the outer gift wrap MUST include an `expiration` tag matching the inner event's expiration (adjusted for the randomized `created_at`). This allows relays to garbage-collect ephemeral signaling data. -4. **Publish** the gift wrap to the recipient's relay list +2. **Wrap** the signed event directly using `EphemeralGiftWrapEvent` (kind `21059`) with NIP-44 encryption +3. **Publish** the ephemeral gift wrap to the recipient's relay list -The seal layer (`SealedRumorEvent`) is NOT used. The gift wrap already provides: +The seal layer (`SealedRumorEvent`) is NOT used. The ephemeral gift wrap already provides: - **NIP-44 encryption** — content is unreadable to relay operators - **Random ephemeral pubkey** — the relay cannot identify the sender - **`p` tag** — reveals only the recipient (necessary for delivery) -- **`expiration` tag** — allows relays to delete the wrap after the signaling window +- **Ephemeral kind (`21059`)** — signals to relays that this event is transient and SHOULD NOT be persisted to long-term storage -Recipients unwrap the gift, verify the inner event's signature against the sender's pubkey, and then process the signaling message. +No `expiration` tag is needed on the inner signaling events or the outer wrap. The ephemeral kind itself communicates the transient nature of the data. Clients MUST still perform staleness checks (see Spam Prevention) to discard old events. + +Recipients unwrap the ephemeral gift, verify the inner event's signature against the sender's pubkey, and then process the signaling message. ## Protocol Flow @@ -207,16 +203,16 @@ Recipients unwrap the gift, verify the inner event's signature against the sende ``` Caller Relay Callee | | | - |-- GiftWrap(CallOffer) ------->| | - | |-- GiftWrap(CallOffer) ------->| + |-- EphemeralGiftWrap(CallOffer) ------->| | + | |-- EphemeralGiftWrap(CallOffer) ------->| | | | | | [Callee unwraps, verifies signature] | | [Checks: is caller followed?] | | [YES → ring / NO → ignore] | | | - |<-- GiftWrap(CallAnswer) ------|<-- GiftWrap(CallAnswer) ------| + |<-- EphemeralGiftWrap(CallAnswer) ------|<-- EphemeralGiftWrap(CallAnswer) ------| | | | - |<-> GiftWrap(IceCandidate) <-->|<-> GiftWrap(IceCandidate) <-->| + |<-> EphemeralGiftWrap(IceCandidate) <-->|<-> EphemeralGiftWrap(IceCandidate) <-->| | | | |============= WebRTC P2P Connection Established ===============| | (relay no longer involved) | @@ -233,12 +229,12 @@ Either party may send a `CallRenegotiate` (kind 25055) during an active call to ``` Party A Relay Party B | | | - |-- GiftWrap(Renegotiate) ----->| | - | |-- GiftWrap(Renegotiate) ----->| + |-- EphemeralGiftWrap(Renegotiate) ----->| | + | |-- EphemeralGiftWrap(Renegotiate) ----->| | | | | | [Party B creates SDP answer] | | | | - |<-- GiftWrap(CallAnswer) ------|<-- GiftWrap(CallAnswer) ------| + |<-- EphemeralGiftWrap(CallAnswer) ------|<-- EphemeralGiftWrap(CallAnswer) ------| | | | |========= Updated WebRTC P2P Connection ========================| ``` @@ -298,7 +294,6 @@ The Call Offer (kind 25050) initiating a group call contains multiple `p` tags: ["p", ""], ["call-id", "550e8400-e29b-41d4-a716-446655440000"], ["call-type", "video"], - ["expiration", "1234567910"], ["alt", "WebRTC call offer"] ] } @@ -315,7 +310,7 @@ To invite a new peer into an active group call, send a Call Offer (kind 25050) w Clients SHOULD implement call filtering: - **Follow-gated ringing**: Only display incoming call notifications for users in the recipient's follow list. Calls from non-followed users SHOULD be silently ignored. -- **Staleness check**: Clients MUST discard signaling events older than the expiration window (20 seconds). This prevents old cached events from triggering phantom calls on app restart or relay reconnect. +- **Staleness check**: Clients MUST discard signaling events older than 20 seconds (based on `created_at`). This prevents old cached events from triggering phantom calls on app restart or relay reconnect. - **Deduplication**: Clients MUST track processed event IDs to prevent the same signaling event (delivered by multiple relays) from being processed twice. - **Rate limiting**: Clients SHOULD ignore duplicate call offers from the same pubkey within a short window. @@ -334,7 +329,7 @@ This NIP does not mandate specific STUN or TURN servers. Clients SHOULD: ### Event Lifecycle - The `call-id` tag MUST be a UUID that is unique per call session. All signaling events for the same call share the same `call-id`. -- Both inner events and outer gift wraps SHOULD have short expiration times (~20 seconds) since signaling data is ephemeral and has no long-term value. This allows relays to garbage-collect call signaling events. +- Signaling data is ephemeral and has no long-term value. Using kind `21059` (Ephemeral Gift Wrap) signals to relays that these events are transient and SHOULD NOT be persisted. - Clients SHOULD implement a ringing timeout (e.g., 60 seconds). If no answer is received, the call transitions to a "timed out" state. - After a call ends, the call state SHOULD auto-reset to idle after a brief display period (e.g., 2 seconds) to ensure the client is ready for subsequent calls. @@ -380,7 +375,6 @@ These self-notification events use the same `call-id` as the original call and f - [NIP-01: Basic Protocol](https://github.com/nostr-protocol/nips/blob/master/01.md) — Event structure - [NIP-31: Alt Tag](https://github.com/nostr-protocol/nips/blob/master/31.md) — Human-readable event descriptions -- [NIP-40: Expiration](https://github.com/nostr-protocol/nips/blob/master/40.md) — Event expiration timestamps - [NIP-44: Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md) — XChaCha20-Poly1305 encryption - [NIP-59: Gift Wraps](https://github.com/nostr-protocol/nips/blob/master/59.md) — Encrypted event delivery - [WebRTC Specification](https://www.w3.org/TR/webrtc/) — Peer-to-peer real-time communication diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt index d6d7f7e34..955cc4f7b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/WebRtcCallFactory.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nipACWebRtcCalls import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallHangupEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent @@ -36,19 +36,15 @@ class WebRtcCallFactory { /** Result for a single-recipient signaling message (P2P). */ data class Result( val msg: Event, - val wrap: GiftWrapEvent, + val wrap: EphemeralGiftWrapEvent, ) /** Result for a signaling message gift-wrapped to multiple recipients (group calls). */ data class GroupResult( val msg: Event, - val wraps: List, + val wraps: List, ) - companion object { - const val WRAP_EXPIRATION_SECONDS = 20L - } - // ---- P2P (single recipient) methods ---- suspend fun createCallOffer( @@ -60,7 +56,7 @@ class WebRtcCallFactory { ): Result { val template = CallOfferEvent.build(sdpOffer, calleePubKey, callId, callType) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = calleePubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = calleePubKey) return Result(signed, wrap) } @@ -80,7 +76,7 @@ class WebRtcCallFactory { ): Result { val template = CallOfferEvent.build(sdpOffer, memberPubKeys, callId, callType) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = calleePubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = calleePubKey) return Result(signed, wrap) } @@ -92,7 +88,7 @@ class WebRtcCallFactory { ): Result { val template = CallAnswerEvent.build(sdpAnswer, callerPubKey, callId) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey) return Result(signed, wrap) } @@ -111,7 +107,7 @@ class WebRtcCallFactory { ): Result { val template = CallAnswerEvent.build(sdpAnswer, memberPubKeys, callId) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey) return Result(signed, wrap) } @@ -123,7 +119,7 @@ class WebRtcCallFactory { ): Result { val template = CallIceCandidateEvent.build(candidateJson, peerPubKey, callId) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey) return Result(signed, wrap) } @@ -135,7 +131,7 @@ class WebRtcCallFactory { ): Result { val template = CallHangupEvent.build(peerPubKey, callId, reason) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey) return Result(signed, wrap) } @@ -147,7 +143,7 @@ class WebRtcCallFactory { ): Result { val template = CallRejectEvent.build(callerPubKey, callId, reason) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey) return Result(signed, wrap) } @@ -159,7 +155,7 @@ class WebRtcCallFactory { ): Result { val template = CallRenegotiateEvent.build(sdpOffer, peerPubKey, callId) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey) return Result(signed, wrap) } @@ -178,7 +174,7 @@ class WebRtcCallFactory { ): Result { val template = CallRenegotiateEvent.build(sdpOffer, memberPubKeys, callId) val signed = signer.sign(template) - val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + val wrap = EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey) return Result(signed, wrap) } @@ -187,7 +183,7 @@ class WebRtcCallFactory { /** * Creates a call offer for a group call. The signed inner event contains * `p` tags for **every** callee so each recipient knows the full group. - * A separate [GiftWrapEvent] is produced for each callee. + * A separate [EphemeralGiftWrapEvent] is produced for each callee. */ suspend fun createGroupCallOffer( sdpOffer: String, @@ -200,7 +196,7 @@ class WebRtcCallFactory { val signed = signer.sign(template) val wraps = calleePubKeys.map { pubKey -> - GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = pubKey) } return GroupResult(signed, wraps) } @@ -208,7 +204,7 @@ class WebRtcCallFactory { /** * Creates a call answer for a group call. The signed inner event contains * `p` tags for **every** member so each recipient knows the full group. - * A separate [GiftWrapEvent] is produced for each member. + * A separate [EphemeralGiftWrapEvent] is produced for each member. */ suspend fun createGroupCallAnswer( sdpAnswer: String, @@ -220,7 +216,7 @@ class WebRtcCallFactory { val signed = signer.sign(template) val wraps = memberPubKeys.map { pubKey -> - GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = pubKey) } return GroupResult(signed, wraps) } @@ -228,7 +224,7 @@ class WebRtcCallFactory { /** * Sends a hangup to every peer in a group call. The signed inner event * contains `p` tags for **every** member so each recipient knows the - * full group. A separate [GiftWrapEvent] is produced for each member. + * full group. A separate [EphemeralGiftWrapEvent] is produced for each member. */ suspend fun createGroupHangup( peerPubKeys: Set, @@ -240,7 +236,7 @@ class WebRtcCallFactory { val signed = signer.sign(template) val wraps = peerPubKeys.map { pubKey -> - GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = pubKey) } return GroupResult(signed, wraps) } @@ -248,7 +244,7 @@ class WebRtcCallFactory { /** * Rejects a group call offer. The signed inner event contains `p` tags * for **every** member so each recipient knows the full group. - * A separate [GiftWrapEvent] is produced for each member. + * A separate [EphemeralGiftWrapEvent] is produced for each member. */ suspend fun createGroupReject( memberPubKeys: Set, @@ -260,7 +256,7 @@ class WebRtcCallFactory { val signed = signer.sign(template) val wraps = memberPubKeys.map { pubKey -> - GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS) + EphemeralGiftWrapEvent.create(event = signed, recipientPubKey = pubKey) } return GroupResult(signed, wraps) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt index 3a5e892d0..f560dc5c6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallAnswerEvent.kt @@ -29,10 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallAnswerEvent( @@ -56,19 +54,16 @@ class CallAnswerEvent( companion object { const val KIND = 25051 const val ALT_DESCRIPTION = "WebRTC call answer" - const val EXPIRATION_SECONDS = 20L fun build( sdpAnswer: String, callerPubKey: HexKey, callId: String, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpAnswer, createdAt) { + ) = eventTemplate(KIND, sdpAnswer) { alt(ALT_DESCRIPTION) pTag(callerPubKey) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } @@ -76,13 +71,11 @@ class CallAnswerEvent( sdpAnswer: String, memberPubKeys: Set, callId: String, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpAnswer, createdAt) { + ) = eventTemplate(KIND, sdpAnswer) { alt(ALT_DESCRIPTION) pTagIds(memberPubKeys) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt index cd953d18b..e1e39a72e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallHangupEvent.kt @@ -29,10 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallHangupEvent( @@ -56,19 +54,16 @@ class CallHangupEvent( companion object { const val KIND = 25053 const val ALT_DESCRIPTION = "WebRTC call hangup" - const val EXPIRATION_SECONDS = 20L fun build( peerPubKey: HexKey, callId: String, reason: String = "", - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, reason, createdAt) { + ) = eventTemplate(KIND, reason) { alt(ALT_DESCRIPTION) pTag(peerPubKey) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } @@ -76,13 +71,11 @@ class CallHangupEvent( memberPubKeys: Set, callId: String, reason: String = "", - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, reason, createdAt) { + ) = eventTemplate(KIND, reason) { alt(ALT_DESCRIPTION) pTagIds(memberPubKeys) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallIceCandidateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallIceCandidateEvent.kt index 1ef209b36..106b4cb9e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallIceCandidateEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallIceCandidateEvent.kt @@ -27,10 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallIceCandidateEvent( @@ -59,7 +57,6 @@ class CallIceCandidateEvent( companion object { const val KIND = 25052 const val ALT_DESCRIPTION = "WebRTC ICE candidate" - const val EXPIRATION_SECONDS = 20L private val CANDIDATE_REGEX = """"candidate"\s*:\s*"([^"]*)"""".toRegex() private val SDP_MID_REGEX = """"sdpMid"\s*:\s*"([^"]*)"""".toRegex() @@ -79,13 +76,11 @@ class CallIceCandidateEvent( candidateJson: String, peerPubKey: HexKey, callId: String, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, candidateJson, createdAt) { + ) = eventTemplate(KIND, candidateJson) { alt(ALT_DESCRIPTION) pTag(peerPubKey) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallOfferEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallOfferEvent.kt index 60ff8dc42..0ca69678f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallOfferEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallOfferEvent.kt @@ -29,13 +29,11 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallTypeTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callType -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallOfferEvent( @@ -67,21 +65,18 @@ class CallOfferEvent( companion object { const val KIND = 25050 const val ALT_DESCRIPTION = "WebRTC call offer" - const val EXPIRATION_SECONDS = 20L fun build( sdpOffer: String, calleePubKey: HexKey, callId: String, type: CallType, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpOffer, createdAt) { + ) = eventTemplate(KIND, sdpOffer) { alt(ALT_DESCRIPTION) pTag(calleePubKey) callId(callId) callType(type) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } @@ -90,14 +85,12 @@ class CallOfferEvent( calleePubKeys: Set, callId: String, type: CallType, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpOffer, createdAt) { + ) = eventTemplate(KIND, sdpOffer) { alt(ALT_DESCRIPTION) pTagIds(calleePubKeys) callId(callId) callType(type) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt index aad23a1ce..858a45270 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRejectEvent.kt @@ -29,10 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallRejectEvent( @@ -56,19 +54,16 @@ class CallRejectEvent( companion object { const val KIND = 25054 const val ALT_DESCRIPTION = "WebRTC call rejection" - const val EXPIRATION_SECONDS = 20L fun build( callerPubKey: HexKey, callId: String, reason: String = "", - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, reason, createdAt) { + ) = eventTemplate(KIND, reason) { alt(ALT_DESCRIPTION) pTag(callerPubKey) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } @@ -76,13 +71,11 @@ class CallRejectEvent( memberPubKeys: Set, callId: String, reason: String = "", - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, reason, createdAt) { + ) = eventTemplate(KIND, reason) { alt(ALT_DESCRIPTION) pTagIds(memberPubKeys) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt index ba41f41be..852bc0300 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/events/CallRenegotiateEvent.kt @@ -29,10 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.callId -import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class CallRenegotiateEvent( @@ -56,19 +54,16 @@ class CallRenegotiateEvent( companion object { const val KIND = 25055 const val ALT_DESCRIPTION = "WebRTC call renegotiation" - const val EXPIRATION_SECONDS = 20L fun build( sdpOffer: String, peerPubKey: HexKey, callId: String, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpOffer, createdAt) { + ) = eventTemplate(KIND, sdpOffer) { alt(ALT_DESCRIPTION) pTag(peerPubKey) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } @@ -76,13 +71,11 @@ class CallRenegotiateEvent( sdpOffer: String, memberPubKeys: Set, callId: String, - createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = eventTemplate(KIND, sdpOffer, createdAt) { + ) = eventTemplate(KIND, sdpOffer) { alt(ALT_DESCRIPTION) pTagIds(memberPubKeys) callId(callId) - expiration(createdAt + EXPIRATION_SECONDS) initializer() } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/CallEventsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/CallEventsTest.kt index 7f885612d..38186ab90 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/CallEventsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipACWebRtcCalls/CallEventsTest.kt @@ -103,25 +103,6 @@ class CallEventsTest { assertEquals("recipient-hex-key", pTag?.get(1)) } - @Test - fun callOfferBuildIncludesExpirationTag() { - val template = - CallOfferEvent.build( - sdpOffer = "sdp", - calleePubKey = "abc", - callId = "id", - type = CallType.VOICE, - createdAt = 1000L, - ) - val expirationTag = template.tags.firstOrNull { it[0] == "expiration" } - assertEquals((1000L + CallOfferEvent.EXPIRATION_SECONDS).toString(), expirationTag?.get(1)) - } - - @Test - fun callOfferExpirationIs20Seconds() { - assertEquals(20L, CallOfferEvent.EXPIRATION_SECONDS) - } - @Test fun callAnswerBuildContainsSdp() { val template = @@ -235,20 +216,6 @@ class CallEventsTest { assertEquals("video", callTypeTag?.get(1)) } - @Test - fun groupCallOfferBuildIncludesExpirationTag() { - val template = - CallOfferEvent.build( - sdpOffer = "sdp", - calleePubKeys = setOf("alice", "bob"), - callId = "id", - type = CallType.VOICE, - createdAt = 2000L, - ) - val expirationTag = template.tags.firstOrNull { it[0] == "expiration" } - assertEquals((2000L + CallOfferEvent.EXPIRATION_SECONDS).toString(), expirationTag?.get(1)) - } - @Test fun groupCallAnswerBuildIncludesAllPTags() { val members = setOf("alice", "bob", "carol")