feat: include all group members in p-tags of inner ephemeral events
All signaling event types in group calls (answer, hangup, reject, renegotiate) now include p-tags for every group member, matching the pattern already used by CallOfferEvent. This allows signing the inner event once and gift-wrapping it separately for each recipient. - Add Set<HexKey> build() overloads to CallAnswerEvent, CallHangupEvent, CallRejectEvent, and CallRenegotiateEvent - Add createGroupCallAnswer, createGroupRenegotiate factory methods - Fix createGroupHangup to sign once instead of per-peer - Update createGroupReject to accept full member set - Update CallManager to use group methods when in group call - Document group call p-tag convention in NIP-AC spec - Add tests for all group build overloads https://claude.ai/code/session_013h2E7spwHDgSjunqumsYgp
This commit is contained in:
+46
-16
@@ -137,27 +137,45 @@ class CallManager(
|
|||||||
val current = _state.value
|
val current = _state.value
|
||||||
if (current !is CallState.IncomingCall) return
|
if (current !is CallState.IncomingCall) return
|
||||||
|
|
||||||
val result = factory.createCallAnswer(sdpAnswer, current.callerPubKey, current.callId, signer)
|
|
||||||
_state.value = CallState.Connecting(current.callId, current.peerPubKeys(), current.callType)
|
_state.value = CallState.Connecting(current.callId, current.peerPubKeys(), current.callType)
|
||||||
cancelTimeout()
|
cancelTimeout()
|
||||||
publishEvent(result.wrap)
|
|
||||||
|
|
||||||
// Notify other devices of this user that the call was answered here.
|
if (current.groupMembers.size > 2) {
|
||||||
val selfNotify = factory.createCallAnswer(sdpAnswer, signer.pubKey, current.callId, signer)
|
// Group call: include all members in p-tags, sign once, wrap for each.
|
||||||
publishEvent(selfNotify.wrap)
|
// Include self so other devices get notified too.
|
||||||
|
val allRecipients = current.groupMembers + signer.pubKey
|
||||||
|
val result = factory.createGroupCallAnswer(sdpAnswer, allRecipients, current.callId, signer)
|
||||||
|
result.wraps.forEach { publishEvent(it) }
|
||||||
|
} else {
|
||||||
|
val result = factory.createCallAnswer(sdpAnswer, current.callerPubKey, current.callId, signer)
|
||||||
|
publishEvent(result.wrap)
|
||||||
|
|
||||||
|
// Notify other devices of this user that the call was answered here.
|
||||||
|
val selfNotify = factory.createCallAnswer(sdpAnswer, signer.pubKey, current.callId, signer)
|
||||||
|
publishEvent(selfNotify.wrap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun rejectCall() {
|
suspend fun rejectCall() {
|
||||||
val current = _state.value
|
val current = _state.value
|
||||||
if (current !is CallState.IncomingCall) return
|
if (current !is CallState.IncomingCall) return
|
||||||
|
|
||||||
val result = factory.createReject(current.callerPubKey, current.callId, signer = signer)
|
|
||||||
transitionToEnded(current.callId, current.peerPubKeys(), EndReason.REJECTED)
|
transitionToEnded(current.callId, current.peerPubKeys(), EndReason.REJECTED)
|
||||||
publishEvent(result.wrap)
|
|
||||||
|
|
||||||
// Notify other devices of this user that the call was rejected here.
|
if (current.groupMembers.size > 2) {
|
||||||
val selfNotify = factory.createReject(signer.pubKey, current.callId, signer = signer)
|
// Group call: include all members in p-tags, sign once, wrap for each.
|
||||||
publishEvent(selfNotify.wrap)
|
// Include self so other devices get notified too.
|
||||||
|
val allRecipients = current.groupMembers + signer.pubKey
|
||||||
|
val result = factory.createGroupReject(allRecipients, current.callId, signer = signer)
|
||||||
|
result.wraps.forEach { publishEvent(it) }
|
||||||
|
} else {
|
||||||
|
val result = factory.createReject(current.callerPubKey, current.callId, signer = signer)
|
||||||
|
publishEvent(result.wrap)
|
||||||
|
|
||||||
|
// Notify other devices of this user that the call was rejected here.
|
||||||
|
val selfNotify = factory.createReject(signer.pubKey, current.callId, signer = signer)
|
||||||
|
publishEvent(selfNotify.wrap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCallAnswered(event: CallAnswerEvent) {
|
fun onCallAnswered(event: CallAnswerEvent) {
|
||||||
@@ -283,16 +301,28 @@ class CallManager(
|
|||||||
|
|
||||||
suspend fun sendRenegotiation(sdpOffer: String) {
|
suspend fun sendRenegotiation(sdpOffer: String) {
|
||||||
val callId = currentCallId() ?: return
|
val callId = currentCallId() ?: return
|
||||||
val peerPubKey = currentPeerPubKey() ?: return
|
val peerPubKeys = currentPeerPubKeys() ?: return
|
||||||
val result = factory.createRenegotiate(sdpOffer, peerPubKey, callId, signer)
|
|
||||||
publishEvent(result.wrap)
|
if (peerPubKeys.size > 1) {
|
||||||
|
val result = factory.createGroupRenegotiate(sdpOffer, peerPubKeys, callId, signer)
|
||||||
|
result.wraps.forEach { publishEvent(it) }
|
||||||
|
} else {
|
||||||
|
val result = factory.createRenegotiate(sdpOffer, peerPubKeys.first(), callId, signer)
|
||||||
|
publishEvent(result.wrap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendRenegotiationAnswer(sdpAnswer: String) {
|
suspend fun sendRenegotiationAnswer(sdpAnswer: String) {
|
||||||
val callId = currentCallId() ?: return
|
val callId = currentCallId() ?: return
|
||||||
val peerPubKey = currentPeerPubKey() ?: return
|
val peerPubKeys = currentPeerPubKeys() ?: return
|
||||||
val result = factory.createCallAnswer(sdpAnswer, peerPubKey, callId, signer)
|
|
||||||
publishEvent(result.wrap)
|
if (peerPubKeys.size > 1) {
|
||||||
|
val result = factory.createGroupCallAnswer(sdpAnswer, peerPubKeys, callId, signer)
|
||||||
|
result.wraps.forEach { publishEvent(it) }
|
||||||
|
} else {
|
||||||
|
val result = factory.createCallAnswer(sdpAnswer, peerPubKeys.first(), callId, signer)
|
||||||
|
publishEvent(result.wrap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPeerConnected() {
|
fun onPeerConnected() {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ All signaling events MUST include:
|
|||||||
|
|
||||||
| Tag | Description | Required |
|
| Tag | Description | Required |
|
||||||
|---------------|-------------------------------------------------------|----------|
|
|---------------|-------------------------------------------------------|----------|
|
||||||
| `p` | Hex pubkey of the recipient | YES |
|
| `p` | Hex pubkey of the recipient (group calls: one per member) | YES |
|
||||||
| `call-id` | UUID identifying the call session | 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 |
|
| `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 |
|
| `alt` | Human-readable description ([NIP-31](https://github.com/nostr-protocol/nips/blob/master/31.md)) | YES |
|
||||||
@@ -251,6 +251,49 @@ Either party may send a `CallHangup` (kind 25053) at any time. The recipient SHO
|
|||||||
|
|
||||||
The callee may send a `CallReject` (kind 25054) instead of a `CallAnswer`. The caller SHOULD stop ringing and display a "call rejected" state.
|
The callee may send a `CallReject` (kind 25054) instead of a `CallAnswer`. The caller SHOULD stop ringing and display a "call rejected" state.
|
||||||
|
|
||||||
|
## Group Calls
|
||||||
|
|
||||||
|
Group calls (calls with more than two participants) use the same event kinds but differ in how `p` tags and gift wraps are structured.
|
||||||
|
|
||||||
|
### P-Tag Convention
|
||||||
|
|
||||||
|
In a group call, all signaling events (except ICE candidates, kind 25052) MUST include a `p` tag for **every** group member. This allows each recipient to know the full group composition from any signaling event.
|
||||||
|
|
||||||
|
ICE candidates (kind 25052) remain addressed to a single peer because WebRTC connections are peer-to-peer — each ICE candidate is relevant only to the specific connection it belongs to.
|
||||||
|
|
||||||
|
### Sign Once, Wrap Per Recipient
|
||||||
|
|
||||||
|
Because all group members are listed in the inner event's `p` tags, the event is **signed once** and then gift-wrapped individually for each recipient:
|
||||||
|
|
||||||
|
1. **Build** the signaling event with `p` tags for all group members
|
||||||
|
2. **Sign** the event once with the sender's key
|
||||||
|
3. **Gift-wrap** the same signed event separately for each member (each wrap encrypted to that member's pubkey)
|
||||||
|
4. **Publish** each gift wrap to the corresponding member's relay list
|
||||||
|
|
||||||
|
This is more efficient than signing a separate event per recipient and ensures cryptographic consistency — every member receives the exact same signed inner event.
|
||||||
|
|
||||||
|
### Group Call Offer
|
||||||
|
|
||||||
|
The Call Offer (kind 25050) initiating a group call contains multiple `p` tags:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 25050,
|
||||||
|
"pubkey": "<caller-hex-pubkey>",
|
||||||
|
"tags": [
|
||||||
|
["p", "<callee-1-hex-pubkey>"],
|
||||||
|
["p", "<callee-2-hex-pubkey>"],
|
||||||
|
["p", "<callee-3-hex-pubkey>"],
|
||||||
|
["call-id", "550e8400-e29b-41d4-a716-446655440000"],
|
||||||
|
["call-type", "video"],
|
||||||
|
["expiration", "1234567910"],
|
||||||
|
["alt", "WebRTC call offer"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Recipients detect a group call by the presence of multiple `p` tags. The full group is the union of all `p`-tagged pubkeys plus the event's `pubkey` (the caller).
|
||||||
|
|
||||||
## Spam Prevention
|
## Spam Prevention
|
||||||
|
|
||||||
Clients SHOULD implement call filtering:
|
Clients SHOULD implement call filtering:
|
||||||
|
|||||||
+52
-14
@@ -148,8 +148,29 @@ class WebRtcCallFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a hangup to every peer in a group call. Each peer receives its
|
* Creates a call answer for a group call. The signed inner event contains
|
||||||
* own gift-wrapped hangup event.
|
* `p` tags for **every** member so each recipient knows the full group.
|
||||||
|
* A separate [GiftWrapEvent] is produced for each member.
|
||||||
|
*/
|
||||||
|
suspend fun createGroupCallAnswer(
|
||||||
|
sdpAnswer: String,
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): GroupResult {
|
||||||
|
val template = CallAnswerEvent.build(sdpAnswer, memberPubKeys, callId)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wraps =
|
||||||
|
memberPubKeys.map { pubKey ->
|
||||||
|
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
||||||
|
}
|
||||||
|
return GroupResult(signed, wraps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
suspend fun createGroupHangup(
|
suspend fun createGroupHangup(
|
||||||
peerPubKeys: Set<HexKey>,
|
peerPubKeys: Set<HexKey>,
|
||||||
@@ -157,33 +178,50 @@ class WebRtcCallFactory {
|
|||||||
reason: String = "",
|
reason: String = "",
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
): GroupResult {
|
): GroupResult {
|
||||||
// Each peer gets its own signed hangup with the correct `p` tag
|
val template = CallHangupEvent.build(peerPubKeys, callId, reason)
|
||||||
// targeting that specific recipient.
|
val signed = signer.sign(template)
|
||||||
var firstSigned: Event? = null
|
|
||||||
val wraps =
|
val wraps =
|
||||||
peerPubKeys.map { pubKey ->
|
peerPubKeys.map { pubKey ->
|
||||||
val template = CallHangupEvent.build(pubKey, callId, reason)
|
|
||||||
val signed = signer.sign(template)
|
|
||||||
if (firstSigned == null) firstSigned = signed
|
|
||||||
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
||||||
}
|
}
|
||||||
return GroupResult(firstSigned!!, wraps)
|
return GroupResult(signed, wraps)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rejects a group call offer. Sends the rejection to the caller and
|
* Rejects a group call offer. The signed inner event contains `p` tags
|
||||||
* notifies self (for multi-device support).
|
* for **every** member so each recipient knows the full group.
|
||||||
|
* A separate [GiftWrapEvent] is produced for each member.
|
||||||
*/
|
*/
|
||||||
suspend fun createGroupReject(
|
suspend fun createGroupReject(
|
||||||
callerPubKey: HexKey,
|
memberPubKeys: Set<HexKey>,
|
||||||
callId: String,
|
callId: String,
|
||||||
reason: String = "",
|
reason: String = "",
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
): GroupResult {
|
): GroupResult {
|
||||||
val template = CallRejectEvent.build(callerPubKey, callId, reason)
|
val template = CallRejectEvent.build(memberPubKeys, callId, reason)
|
||||||
val signed = signer.sign(template)
|
val signed = signer.sign(template)
|
||||||
val wraps =
|
val wraps =
|
||||||
listOf(callerPubKey, signer.pubKey).distinct().map { pubKey ->
|
memberPubKeys.map { pubKey ->
|
||||||
|
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
||||||
|
}
|
||||||
|
return GroupResult(signed, wraps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a renegotiation offer 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.
|
||||||
|
*/
|
||||||
|
suspend fun createGroupRenegotiate(
|
||||||
|
sdpOffer: String,
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): GroupResult {
|
||||||
|
val template = CallRenegotiateEvent.build(sdpOffer, memberPubKeys, callId)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wraps =
|
||||||
|
memberPubKeys.map { pubKey ->
|
||||||
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
GiftWrapEvent.create(event = signed, recipientPubKey = pubKey, expirationDelta = WRAP_EXPIRATION_SECONDS)
|
||||||
}
|
}
|
||||||
return GroupResult(signed, wraps)
|
return GroupResult(signed, wraps)
|
||||||
|
|||||||
+15
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
||||||
@@ -63,5 +64,19 @@ class CallAnswerEvent(
|
|||||||
expiration(createdAt + EXPIRATION_SECONDS)
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
initializer()
|
initializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
sdpAnswer: String,
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallAnswerEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, sdpAnswer, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTagIds(memberPubKeys)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
||||||
@@ -63,5 +64,19 @@ class CallHangupEvent(
|
|||||||
expiration(createdAt + EXPIRATION_SECONDS)
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
initializer()
|
initializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallHangupEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, reason, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTagIds(memberPubKeys)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
||||||
@@ -63,5 +64,19 @@ class CallRejectEvent(
|
|||||||
expiration(createdAt + EXPIRATION_SECONDS)
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
initializer()
|
initializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallRejectEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, reason, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTagIds(memberPubKeys)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
|||||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||||
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.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallIdTag
|
||||||
@@ -63,5 +64,19 @@ class CallRenegotiateEvent(
|
|||||||
expiration(createdAt + EXPIRATION_SECONDS)
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
initializer()
|
initializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
sdpOffer: String,
|
||||||
|
memberPubKeys: Set<HexKey>,
|
||||||
|
callId: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallRenegotiateEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, sdpOffer, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTagIds(memberPubKeys)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+66
@@ -249,6 +249,72 @@ class CallEventsTest {
|
|||||||
assertEquals((2000L + CallOfferEvent.EXPIRATION_SECONDS).toString(), expirationTag?.get(1))
|
assertEquals((2000L + CallOfferEvent.EXPIRATION_SECONDS).toString(), expirationTag?.get(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupCallAnswerBuildIncludesAllPTags() {
|
||||||
|
val members = setOf("alice", "bob", "carol")
|
||||||
|
val template =
|
||||||
|
CallAnswerEvent.build(
|
||||||
|
sdpAnswer = "answer-sdp",
|
||||||
|
memberPubKeys = members,
|
||||||
|
callId = "group-call-1",
|
||||||
|
)
|
||||||
|
val pTagValues =
|
||||||
|
template.tags
|
||||||
|
.filter { it[0] == "p" }
|
||||||
|
.map { it[1] }
|
||||||
|
.toSet()
|
||||||
|
assertEquals(members, pTagValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupCallHangupBuildIncludesAllPTags() {
|
||||||
|
val members = setOf("alice", "bob", "carol")
|
||||||
|
val template =
|
||||||
|
CallHangupEvent.build(
|
||||||
|
memberPubKeys = members,
|
||||||
|
callId = "group-call-1",
|
||||||
|
)
|
||||||
|
val pTagValues =
|
||||||
|
template.tags
|
||||||
|
.filter { it[0] == "p" }
|
||||||
|
.map { it[1] }
|
||||||
|
.toSet()
|
||||||
|
assertEquals(members, pTagValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupCallRejectBuildIncludesAllPTags() {
|
||||||
|
val members = setOf("alice", "bob", "carol")
|
||||||
|
val template =
|
||||||
|
CallRejectEvent.build(
|
||||||
|
memberPubKeys = members,
|
||||||
|
callId = "group-call-1",
|
||||||
|
)
|
||||||
|
val pTagValues =
|
||||||
|
template.tags
|
||||||
|
.filter { it[0] == "p" }
|
||||||
|
.map { it[1] }
|
||||||
|
.toSet()
|
||||||
|
assertEquals(members, pTagValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun groupCallRenegotiateBuildIncludesAllPTags() {
|
||||||
|
val members = setOf("alice", "bob", "carol")
|
||||||
|
val template =
|
||||||
|
CallRenegotiateEvent.build(
|
||||||
|
sdpOffer = "new-sdp-offer",
|
||||||
|
memberPubKeys = members,
|
||||||
|
callId = "group-call-1",
|
||||||
|
)
|
||||||
|
val pTagValues =
|
||||||
|
template.tags
|
||||||
|
.filter { it[0] == "p" }
|
||||||
|
.map { it[1] }
|
||||||
|
.toSet()
|
||||||
|
assertEquals(members, pTagValues)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun singleCalleeOfferIsNotGroupCall() {
|
fun singleCalleeOfferIsNotGroupCall() {
|
||||||
val template =
|
val template =
|
||||||
|
|||||||
Reference in New Issue
Block a user