feat: add WebRTC voice/video call infrastructure
Implements P2P calling over Nostr relays using WebRTC for media transport and NIP-59 Gift Wraps for encrypted signaling. No custom server required — only public STUN servers for NAT traversal. Protocol layer (quartz/nip100WebRtcCalls): - 6 new event kinds (25050-25055): offer, answer, ICE candidate, hangup, reject, renegotiate - WebRtcCallFactory for creating and gift-wrapping signaling events - CallIdTag and CallTypeTag for event metadata - Events registered in EventFactory Call state machine (commons/call): - CallState sealed interface with full lifecycle states - CallManager orchestrating signaling and state transitions - Follow-gate spam prevention: only followed users can ring, non-follows are silently ignored Android WebRTC integration (amethyst/service/call): - WebRtcCallSession wrapping Google WebRTC PeerConnection - CallForegroundService for keeping calls alive in background - IceServerConfig with default public STUN servers - User-configurable TURN server support Android UI (amethyst/ui/call): - CallScreen with offering, connecting, connected, and ended states - IncomingCallUI with accept/reject buttons - ConnectedCallUI with mute, video toggle, speaker, and timer - Call button added to 1-on-1 DM chat header - ActiveCall route added to navigation https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls
|
||||||
|
|
||||||
|
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.nip100WebRtcCalls.events.CallAnswerEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallHangupEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallIceCandidateEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallOfferEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallRejectEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallRenegotiateEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.CallType
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
|
||||||
|
class WebRtcCallFactory {
|
||||||
|
data class Result(
|
||||||
|
val msg: Event,
|
||||||
|
val wrap: GiftWrapEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun createCallOffer(
|
||||||
|
sdpOffer: String,
|
||||||
|
calleePubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
callType: CallType,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallOfferEvent.build(sdpOffer, calleePubKey, callId, callType)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = calleePubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createCallAnswer(
|
||||||
|
sdpAnswer: String,
|
||||||
|
callerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallAnswerEvent.build(sdpAnswer, callerPubKey, callId)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createIceCandidate(
|
||||||
|
candidateJson: String,
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallIceCandidateEvent.build(candidateJson, peerPubKey, callId)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createHangup(
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallHangupEvent.build(peerPubKey, callId, reason)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createReject(
|
||||||
|
callerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallRejectEvent.build(callerPubKey, callId, reason)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = callerPubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRenegotiate(
|
||||||
|
sdpOffer: String,
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
): Result {
|
||||||
|
val template = CallRenegotiateEvent.build(sdpOffer, peerPubKey, callId)
|
||||||
|
val signed = signer.sign(template)
|
||||||
|
val wrap = GiftWrapEvent.create(event = signed, recipientPubKey = peerPubKey)
|
||||||
|
return Result(signed, wrap)
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallAnswerEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun sdpAnswer() = content
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25051
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC call answer"
|
||||||
|
const val EXPIRATION_SECONDS = 300L
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
sdpAnswer: String,
|
||||||
|
callerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallAnswerEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, sdpAnswer, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(callerPubKey)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallHangupEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun reason() = content.ifEmpty { null }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25053
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC call hangup"
|
||||||
|
const val EXPIRATION_SECONDS = 300L
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallHangupEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, reason, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(peerPubKey)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallIceCandidateEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun candidateJson() = content
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25052
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC ICE candidate"
|
||||||
|
const val EXPIRATION_SECONDS = 300L
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
candidateJson: String,
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallIceCandidateEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, candidateJson, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(peerPubKey)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.CallType
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.CallTypeTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callType
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallOfferEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun callType() = tags.firstNotNullOfOrNull(CallTypeTag::parse)
|
||||||
|
|
||||||
|
fun sdpOffer() = content
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25050
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC call offer"
|
||||||
|
const val EXPIRATION_SECONDS = 300L // 5 minutes
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
sdpOffer: String,
|
||||||
|
calleePubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
type: CallType,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallOfferEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, sdpOffer, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(calleePubKey)
|
||||||
|
callId(callId)
|
||||||
|
callType(type)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallRejectEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun reason() = content.ifEmpty { null }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25054
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC call rejection"
|
||||||
|
const val EXPIRATION_SECONDS = 300L
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
callerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
reason: String = "",
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallRejectEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, reason, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(callerPubKey)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
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.nip100WebRtcCalls.tags.CallIdTag
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.tags.callId
|
||||||
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
|
import com.vitorpamplona.quartz.nip40Expiration.expiration
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class CallRenegotiateEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: Array<Array<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey,
|
||||||
|
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||||
|
fun callId() = tags.firstNotNullOfOrNull(CallIdTag::parse)
|
||||||
|
|
||||||
|
fun sdpOffer() = content
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KIND = 25055
|
||||||
|
const val ALT_DESCRIPTION = "WebRTC call renegotiation"
|
||||||
|
const val EXPIRATION_SECONDS = 300L
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
sdpOffer: String,
|
||||||
|
peerPubKey: HexKey,
|
||||||
|
callId: String,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<CallRenegotiateEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, sdpOffer, createdAt) {
|
||||||
|
alt(ALT_DESCRIPTION)
|
||||||
|
pTag(peerPubKey)
|
||||||
|
callId(callId)
|
||||||
|
expiration(createdAt + EXPIRATION_SECONDS)
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
class CallIdTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "call-id"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): String? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return tag[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(callId: String) = arrayOf(TAG_NAME, callId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
enum class CallType(
|
||||||
|
val value: String,
|
||||||
|
) {
|
||||||
|
VOICE("voice"),
|
||||||
|
VIDEO("video"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): CallType? =
|
||||||
|
when (value) {
|
||||||
|
"voice" -> VOICE
|
||||||
|
"video" -> VIDEO
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallTypeTag {
|
||||||
|
companion object {
|
||||||
|
const val TAG_NAME = "call-type"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): CallType? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
return CallType.fromString(tag[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(callType: CallType) = arrayOf(TAG_NAME, callType.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip100WebRtcCalls.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||||
|
|
||||||
|
fun <T : Event> TagArrayBuilder<T>.callId(callId: String) = addUnique(CallIdTag.assemble(callId))
|
||||||
|
|
||||||
|
fun <T : Event> TagArrayBuilder<T>.callType(callType: CallType) = addUnique(CallTypeTag.assemble(callType))
|
||||||
@@ -50,6 +50,12 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
|||||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent
|
||||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallAnswerEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallHangupEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallIceCandidateEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallOfferEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallRejectEvent
|
||||||
|
import com.vitorpamplona.quartz.nip100WebRtcCalls.events.CallRenegotiateEvent
|
||||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||||
import com.vitorpamplona.quartz.nip15Marketplace.auction.AuctionEvent
|
import com.vitorpamplona.quartz.nip15Marketplace.auction.AuctionEvent
|
||||||
import com.vitorpamplona.quartz.nip15Marketplace.bid.BidEvent
|
import com.vitorpamplona.quartz.nip15Marketplace.bid.BidEvent
|
||||||
|
|||||||
Reference in New Issue
Block a user