diff --git a/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoom.kt b/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoom.kt index 5427896806..79f1fe87c0 100644 --- a/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoom.kt +++ b/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoom.kt @@ -136,6 +136,9 @@ interface ChatRoom { /** Updates the list of members that are allowed to unmute audio or video. */ fun setAvModerationWhitelist(mediaType: MediaType, whitelist: List) + /** Update the value in the room_metadata structure */ + fun setRoomMetadata(roomMetadata: RoomMetadata) + /** whether the current A/V moderation setting allow the member [jid] to unmute (for a specific [mediaType]). */ fun isMemberAllowedToUnmute(jid: Jid, mediaType: MediaType): Boolean diff --git a/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomImpl.kt b/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomImpl.kt index b7e55e38db..b30f70a2aa 100644 --- a/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomImpl.kt +++ b/jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomImpl.kt @@ -265,6 +265,12 @@ class ChatRoomImpl( val config = muc.configurationForm parseConfigForm(config) + // We only read the initial metadata from the config form. Setting room metadata after a config form reload may + // race with updates coming via [RoomMetadataHandler]. + config.getRoomMetadata()?.let { + setRoomMetadata(it) + } + // Make the room non-anonymous, so that others can recognize focus JID val answer = config.fillableForm answer.setAnswer(MucConfigFields.WHOIS, "anyone") @@ -283,23 +289,22 @@ class ChatRoomImpl( ) } + override fun setRoomMetadata(roomMetadata: RoomMetadata) { + transcriptionRequested = roomMetadata.metadata?.recording?.isTranscribingEnabled == true + } + /** Read the fields we care about from [configForm] and update local state. */ private fun parseConfigForm(configForm: Form) { lobbyEnabled = configForm.getField(MucConfigFormManager.MUC_ROOMCONFIG_MEMBERSONLY)?.firstValue?.toBoolean() ?: false visitorsEnabled = configForm.getField(MucConfigFields.VISITORS_ENABLED)?.firstValue?.toBoolean() participantsSoftLimit = configForm.getField(MucConfigFields.PARTICIPANTS_SOFT_LIMIT)?.firstValue?.toInt() - // Default to false unless specified. - val roomMetadata = configForm.getRoomMetadata() - if (roomMetadata != null) { - transcriptionRequested = roomMetadata.recording?.isTranscribingEnabled == true - } } - private fun Form.getRoomMetadata(): RoomMetadata.Metadata? { + private fun Form.getRoomMetadata(): RoomMetadata? { getField("muc#roominfo_jitsimetadata")?.firstValue?.let { try { - return RoomMetadata.parse(it).metadata + return RoomMetadata.parse(it) } catch (e: Exception) { logger.warn("Invalid room metadata content", e) return null diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/RoomMetadataHandler.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/RoomMetadataHandler.kt new file mode 100644 index 0000000000..ada3ac7cf9 --- /dev/null +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/RoomMetadataHandler.kt @@ -0,0 +1,107 @@ +/* + * Jicofo, the Jitsi Conference Focus. + * + * Copyright @ 2024-Present 8x8, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jitsi.jicofo.xmpp + +import org.jitsi.jicofo.ConferenceStore +import org.jitsi.jicofo.TaskPools +import org.jitsi.jicofo.xmpp.muc.RoomMetadata +import org.jitsi.utils.OrderedJsonObject +import org.jitsi.utils.logging2.createLogger +import org.jitsi.utils.queue.PacketQueue +import org.jitsi.xmpp.extensions.jitsimeet.JsonMessageExtension +import org.jivesoftware.smack.StanzaListener +import org.jivesoftware.smack.filter.MessageTypeFilter +import org.jivesoftware.smack.packet.Stanza +import org.jxmpp.jid.DomainBareJid +import org.jxmpp.jid.impl.JidCreate + +class RoomMetadataHandler( + private val xmppProvider: XmppProvider, + private val conferenceStore: ConferenceStore +) : XmppProvider.Listener, StanzaListener { + private var componentAddress: DomainBareJid? = null + private val logger = createLogger() + + /** Queue to process requests in order in the IO pool */ + private val queue = PacketQueue( + Integer.MAX_VALUE, + false, + "room_metadata queue", + { + doProcess(it) + return@PacketQueue true + }, + TaskPools.ioPool + ) + + init { + xmppProvider.xmppConnection.addSyncStanzaListener(this, MessageTypeFilter.NORMAL) + xmppProvider.addListener(this) + registrationChanged(xmppProvider.registered) + componentsChanged(xmppProvider.components) + } + + val debugState: OrderedJsonObject + get() = OrderedJsonObject().apply { + this["address"] = componentAddress.toString() + } + + private fun doProcess(jsonMessage: JsonMessageExtension) { + try { + val conferenceJid = JidCreate.entityBareFrom(jsonMessage.getAttribute("room")?.toString()) + val roomMetadata = RoomMetadata.parse(jsonMessage.json) + + val conference = conferenceStore.getConference(conferenceJid) + ?: throw IllegalStateException("Conference $conferenceJid does not exist.") + val chatRoom = conference.chatRoom + ?: throw IllegalStateException("Conference has no associated chatRoom.") + + chatRoom.setRoomMetadata(roomMetadata) + } catch (e: Exception) { + logger.warn("Failed to process room_metadata request: $jsonMessage", e) + } + } + + override fun processStanza(stanza: Stanza) { + if (stanza.from != componentAddress) { + return + } + + val jsonMessage = stanza.getExtension(JsonMessageExtension::class.java) ?: return Unit.also { + logger.warn("Skip processing stanza without JsonMessageExtension.") + } + + queue.add(jsonMessage) + } + + override fun componentsChanged(components: Set) { + val address = components.find { it.type == "room_metadata" }?.address + + componentAddress = if (address == null) { + logger.info("No room_metadata component discovered.") + null + } else { + logger.info("Using room_metadata component at $address.") + JidCreate.domainBareFrom(address) + } + } + + fun shutdown() { + xmppProvider.xmppConnection.removeSyncStanzaListener(this) + } +} diff --git a/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/XmppServices.kt b/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/XmppServices.kt index 8a4e68d9ad..607de2c033 100644 --- a/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/XmppServices.kt +++ b/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/XmppServices.kt @@ -95,6 +95,7 @@ class XmppServices( val avModerationHandler = AvModerationHandler(clientConnection, conferenceStore) val configurationChangeHandler = ConfigurationChangeHandler(clientConnection, conferenceStore) + val roomMetadataHandler = RoomMetadataHandler(clientConnection, conferenceStore) private val audioMuteHandler = AudioMuteIqHandler(setOf(clientConnection.xmppConnection), conferenceStore) private val videoMuteHandler = VideoMuteIqHandler(setOf(clientConnection.xmppConnection), conferenceStore) val jingleHandler = JingleIqRequestHandler(