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
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
+2
-1
@@ -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) + "..."
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
|
||||
@@ -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", "<callee-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["call-type", "video"],
|
||||
["expiration", "1234567910"],
|
||||
["alt", "WebRTC call offer"]
|
||||
],
|
||||
"id": "<event-id>",
|
||||
@@ -90,7 +90,6 @@ The `content` field contains the SDP answer string.
|
||||
"tags": [
|
||||
["p", "<caller-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["expiration", "1234567915"],
|
||||
["alt", "WebRTC call answer"]
|
||||
],
|
||||
"id": "<event-id>",
|
||||
@@ -111,7 +110,6 @@ The `content` field contains the ICE candidate as a JSON string with the fields
|
||||
"tags": [
|
||||
["p", "<peer-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["expiration", "1234567916"],
|
||||
["alt", "WebRTC ICE candidate"]
|
||||
],
|
||||
"id": "<event-id>",
|
||||
@@ -132,7 +130,6 @@ The `content` field MAY contain a human-readable reason or be empty.
|
||||
"tags": [
|
||||
["p", "<peer-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["expiration", "1234568020"],
|
||||
["alt", "WebRTC call hangup"]
|
||||
],
|
||||
"id": "<event-id>",
|
||||
@@ -153,7 +150,6 @@ The `content` field MAY contain a reason or be empty.
|
||||
"tags": [
|
||||
["p", "<caller-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["expiration", "1234567913"],
|
||||
["alt", "WebRTC call rejection"]
|
||||
],
|
||||
"id": "<event-id>",
|
||||
@@ -174,7 +170,6 @@ Used for mid-call changes such as toggling video on/off. The `content` field con
|
||||
"tags": [
|
||||
["p", "<peer-hex-pubkey>"],
|
||||
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||
["expiration", "1234568120"],
|
||||
["alt", "WebRTC call renegotiation"]
|
||||
],
|
||||
"id": "<event-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", "<callee-3-hex-pubkey>"],
|
||||
["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
|
||||
|
||||
+20
-24
@@ -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<GiftWrapEvent>,
|
||||
val wraps: List<EphemeralGiftWrapEvent>,
|
||||
)
|
||||
|
||||
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<HexKey>,
|
||||
@@ -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<HexKey>,
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<CallAnswerEvent>.() -> 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<HexKey>,
|
||||
callId: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<CallAnswerEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, sdpAnswer, createdAt) {
|
||||
) = eventTemplate(KIND, sdpAnswer) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTagIds(memberPubKeys)
|
||||
callId(callId)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<CallHangupEvent>.() -> 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<HexKey>,
|
||||
callId: String,
|
||||
reason: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<CallHangupEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, reason, createdAt) {
|
||||
) = eventTemplate(KIND, reason) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTagIds(memberPubKeys)
|
||||
callId(callId)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -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<CallIceCandidateEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, candidateJson, createdAt) {
|
||||
) = eventTemplate(KIND, candidateJson) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTag(peerPubKey)
|
||||
callId(callId)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<CallOfferEvent>.() -> 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<HexKey>,
|
||||
callId: String,
|
||||
type: CallType,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<CallOfferEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, sdpOffer, createdAt) {
|
||||
) = eventTemplate(KIND, sdpOffer) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTagIds(calleePubKeys)
|
||||
callId(callId)
|
||||
callType(type)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<CallRejectEvent>.() -> 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<HexKey>,
|
||||
callId: String,
|
||||
reason: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<CallRejectEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, reason, createdAt) {
|
||||
) = eventTemplate(KIND, reason) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTagIds(memberPubKeys)
|
||||
callId(callId)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -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<CallRenegotiateEvent>.() -> 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<HexKey>,
|
||||
callId: String,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<CallRenegotiateEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, sdpOffer, createdAt) {
|
||||
) = eventTemplate(KIND, sdpOffer) {
|
||||
alt(ALT_DESCRIPTION)
|
||||
pTagIds(memberPubKeys)
|
||||
callId(callId)
|
||||
expiration(createdAt + EXPIRATION_SECONDS)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
|
||||
-33
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user