Merge pull request #2494 from vitorpamplona/claude/clubhouse-event-listing-Kta46

Add MoQ-transport client and audio rooms support
This commit is contained in:
Vitor Pamplona
2026-04-22 09:50:02 -04:00
committed by GitHub
59 changed files with 6802 additions and 1 deletions
@@ -31,8 +31,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.MutedTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -53,6 +55,8 @@ class MeetingRoomPresenceEvent(
fun handRaised() = tags.firstNotNullOfOrNull(HandRaisedTag::parse)
fun muted() = tags.firstNotNullOfOrNull(MutedTag::parse)
companion object Companion {
const val KIND = 10312
const val ALT = "Room Presence tag"
@@ -78,5 +82,27 @@ class MeetingRoomPresenceEvent(
}
initializer()
}
/**
* Convenience builder when the parent is a kind 30312 [MeetingSpaceEvent]
* (Clubhouse-style audio room). Mirrors the [MeetingRoomEvent] overload so
* a participant can publish presence + hand-raise + mute against an audio
* room without adapting to the meeting-room (kind 30313) variant.
*/
fun build(
root: MeetingSpaceEvent,
handRaised: Boolean? = null,
muted: Boolean? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<MeetingRoomPresenceEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
alt(root.room() ?: ALT)
roomMeeting(MeetingSpaceTag(root.address(), root.relays().firstOrNull()))
handRaised?.let { handRaised(it) }
muted?.let { muted(it) }
initializer()
}
}
}
@@ -26,9 +26,12 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.MutedTag
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: MeetingSpaceTag) = addUnique(rep.toTagArray())
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: EventHintBundle<MeetingRoomEvent>) = addUnique(rep.toATag().toATagArray())
fun TagArrayBuilder<MeetingRoomPresenceEvent>.handRaised(raised: Boolean) = addUnique(HandRaisedTag.assemble(raised))
fun TagArrayBuilder<MeetingRoomPresenceEvent>.muted(muted: Boolean) = addUnique(MutedTag.assemble(muted))
@@ -0,0 +1,45 @@
/*
* 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.nip53LiveActivities.presence.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/**
* Mute flag for kind 10312 presence events used by Clubhouse-style audio
* rooms (e.g. nostrnests/nests). Carried alongside the optional `hand` tag so
* other clients can render the speaker as muted without waiting for the audio
* transport to drop frames.
*/
class MutedTag {
companion object Companion {
const val TAG_NAME = "muted"
fun parse(tag: Array<String>): Boolean? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1] == "1"
}
fun assemble(muted: Boolean) = arrayOf(TAG_NAME, if (muted) "1" else "0")
}
}