Skip to content

Commit 0ec5ae2

Browse files
damenchobgrozev
andauthored
feat: Handles mute desktop IQ. (#1235)
* feat: Handles mute desktop IQ. * ref: Adapt to jitsi-xmpp-changes. * ref: Use jackson for parsing AvModeration json message. * ref: Use org.jitsi.jicofo.MediaType in ChatRoom. * ref: Use org.jitsi.jicofo.MediaType in JitsiMeetConference. * Add a DESKTOP media type. * Register a desktop mute iq handler. * feat: Take into account avmoderation whitelists when setting colibri mute state. * fix: Handle DESKTOP when enableing av moderation. * chore: Update jitsi-xmpp-extensions. --------- Co-authored-by: Boris Grozev <[email protected]>
1 parent b114d6f commit 0ec5ae2

File tree

18 files changed

+454
-257
lines changed

18 files changed

+454
-257
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Jicofo, the Jitsi Conference Focus.
3+
*
4+
* Copyright @ 2025-Present 8x8, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.jitsi.jicofo
19+
20+
enum class MediaType {
21+
AUDIO,
22+
VIDEO,
23+
DESKTOP
24+
}

jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/RoomMetadata.kt renamed to jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/JsonMessage.kt

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* Jicofo, the Jitsi Conference Focus.
33
*
4-
* Copyright @ 2024-Present 8x8, Inc.
4+
* Copyright @ 2025-Present 8x8, Inc.
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -15,25 +15,60 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
package org.jitsi.jicofo.xmpp.muc
18+
package org.jitsi.jicofo.xmpp
1919

2020
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
21+
import com.fasterxml.jackson.annotation.JsonSubTypes
22+
import com.fasterxml.jackson.annotation.JsonTypeInfo
2123
import com.fasterxml.jackson.core.JsonParser
2224
import com.fasterxml.jackson.core.JsonProcessingException
2325
import com.fasterxml.jackson.databind.JsonMappingException
2426
import com.fasterxml.jackson.databind.MapperFeature
2527
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2628
import com.fasterxml.jackson.module.kotlin.readValue
29+
import org.jitsi.jicofo.MediaType
30+
31+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
32+
@JsonSubTypes(
33+
JsonSubTypes.Type(value = AvModerationMessage::class, name = AvModerationMessage.TYPE),
34+
JsonSubTypes.Type(value = RoomMetadata::class, name = RoomMetadata.TYPE)
35+
)
36+
@JsonIgnoreProperties(ignoreUnknown = true)
37+
sealed class JsonMessage(val type: String) {
38+
companion object {
39+
private val mapper = jacksonObjectMapper().apply {
40+
enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
41+
enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
42+
}
43+
44+
@JvmStatic
45+
@Throws(JsonProcessingException::class, JsonMappingException::class)
46+
fun parse(string: String): JsonMessage {
47+
return mapper.readValue(string)
48+
}
49+
}
50+
}
51+
52+
@JsonIgnoreProperties(ignoreUnknown = true)
53+
data class AvModerationMessage(
54+
val room: String?,
55+
val enabled: Boolean? = null,
56+
val mediaType: MediaType? = null,
57+
val actor: String? = null,
58+
val whitelists: Map<MediaType, List<String>>? = null
59+
) : JsonMessage(TYPE) {
60+
61+
companion object {
62+
const val TYPE = "av_moderation"
63+
}
64+
}
2765

2866
/**
2967
* The JSON structure included in the MUC config form from the room_metadata prosody module in jitsi-meet. Includes
3068
* only the fields that we need here in jicofo.
3169
*/
3270
@JsonIgnoreProperties(ignoreUnknown = true)
33-
data class RoomMetadata(
34-
val type: String,
35-
val metadata: Metadata?
36-
) {
71+
data class RoomMetadata(val metadata: Metadata?) : JsonMessage(TYPE) {
3772
@JsonIgnoreProperties(ignoreUnknown = true)
3873
data class Metadata(
3974
val visitors: Visitors?,
@@ -56,14 +91,6 @@ data class RoomMetadata(
5691
}
5792

5893
companion object {
59-
private val mapper = jacksonObjectMapper().apply {
60-
enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
61-
enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
62-
}
63-
64-
@Throws(JsonProcessingException::class, JsonMappingException::class)
65-
fun parse(string: String): RoomMetadata {
66-
return mapper.readValue(string)
67-
}
94+
const val TYPE = "room_metadata"
6895
}
6996
}

jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoom.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
*/
1818
package org.jitsi.jicofo.xmpp.muc
1919

20+
import org.jitsi.jicofo.MediaType
21+
import org.jitsi.jicofo.xmpp.RoomMetadata
2022
import org.jitsi.jicofo.xmpp.XmppProvider
21-
import org.jitsi.utils.MediaType
2223
import org.jitsi.utils.OrderedJsonObject
2324
import org.jivesoftware.smack.SmackException
2425
import org.jivesoftware.smack.XMPPException

jicofo-common/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomImpl.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ package org.jitsi.jicofo.xmpp.muc
1919

2020
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
2121
import org.jitsi.jicofo.JicofoConfig
22+
import org.jitsi.jicofo.MediaType
2223
import org.jitsi.jicofo.TaskPools.Companion.ioPool
2324
import org.jitsi.jicofo.util.PendingCount
25+
import org.jitsi.jicofo.xmpp.RoomMetadata
2426
import org.jitsi.jicofo.xmpp.XmppProvider
2527
import org.jitsi.jicofo.xmpp.muc.MemberRole.Companion.fromSmack
2628
import org.jitsi.jicofo.xmpp.sendIqAndGetResponse
2729
import org.jitsi.jicofo.xmpp.tryToSendStanza
28-
import org.jitsi.utils.MediaType
2930
import org.jitsi.utils.OrderedJsonObject
3031
import org.jitsi.utils.event.EventEmitter
3132
import org.jitsi.utils.event.SyncEventEmitter
@@ -247,7 +248,6 @@ class ChatRoomImpl(
247248
// Use toList to avoid concurrent modification. TODO: add a removeAll to EventEmitter.
248249
override fun removeAllListeners() = eventEmitter.eventHandlers.toList().forEach { eventEmitter.removeHandler(it) }
249250

250-
/** In practice we only use AUDIO and VIDEO, so polluting the map is not a problem. */
251251
private fun avModeration(mediaType: MediaType): AvModerationForMediaType =
252252
avModerationByMediaType.computeIfAbsent(mediaType) { AvModerationForMediaType(mediaType) }
253253
override fun isAvModerationEnabled(mediaType: MediaType) = avModeration(mediaType).enabled
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Jicofo, the Jitsi Conference Focus.
3+
*
4+
* Copyright @ 2024-Present 8x8, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.jitsi.jicofo.xmpp
19+
20+
import io.kotest.assertions.throwables.shouldThrow
21+
import io.kotest.core.spec.style.ShouldSpec
22+
import io.kotest.matchers.nulls.shouldNotBeNull
23+
import io.kotest.matchers.shouldBe
24+
import io.kotest.matchers.types.shouldBeInstanceOf
25+
import org.jitsi.jicofo.MediaType
26+
27+
class JsonMessageTest : ShouldSpec() {
28+
init {
29+
context("RoomMetadata") {
30+
context("Valid") {
31+
context("With visitors.live set") {
32+
val parsed = JsonMessage.parse(
33+
"""
34+
{
35+
"type": "room_metadata",
36+
"metadata": {
37+
"visitors": {
38+
"live": true,
39+
"anotherField": 123
40+
}
41+
},
42+
"anotherField": {}
43+
}
44+
}
45+
""".trimIndent()
46+
)
47+
parsed.shouldBeInstanceOf<RoomMetadata>()
48+
parsed.metadata!!.visitors!!.live shouldBe true
49+
}
50+
context("With no visitors included") {
51+
52+
val parsed = JsonMessage.parse(
53+
"""
54+
{
55+
"type": "room_metadata",
56+
"metadata": {
57+
"key": {
58+
"key2": "value2"
59+
},
60+
"anotherField": {}
61+
}
62+
}
63+
""".trimIndent()
64+
)
65+
parsed.shouldBeInstanceOf<RoomMetadata>()
66+
parsed.metadata!!.visitors shouldBe null
67+
}
68+
context("With visitors, mainMeetingParticipants, and startMuted") {
69+
JsonMessage.parse(
70+
"""
71+
{
72+
"metadata": {
73+
"transcriberType": "EGHT_WHISPER",
74+
"visitors": {
75+
"live": true
76+
},
77+
"moderators": [
78+
"user_id_1",
79+
"user_id_2"
80+
],
81+
"participants": [
82+
"user_id_3",
83+
"user_id_4"
84+
],
85+
"startMuted": {
86+
"audio": true
87+
}
88+
},
89+
"type": "room_metadata"
90+
}
91+
""".trimIndent()
92+
).apply {
93+
shouldBeInstanceOf<RoomMetadata>()
94+
metadata.apply {
95+
shouldNotBeNull()
96+
visitors.apply {
97+
shouldNotBeNull()
98+
live shouldBe true
99+
}
100+
moderators shouldBe listOf("user_id_1", "user_id_2")
101+
participants shouldBe listOf("user_id_3", "user_id_4")
102+
startMuted.apply {
103+
shouldNotBeNull()
104+
audio shouldBe true
105+
video shouldBe null
106+
}
107+
}
108+
type shouldBe "room_metadata"
109+
}
110+
}
111+
}
112+
context("Invalid") {
113+
context("Missing type") {
114+
shouldThrow<Exception> {
115+
JsonMessage.parse(
116+
"""
117+
{ "key": 123 }
118+
""".trimIndent()
119+
)
120+
}
121+
}
122+
context("Invalid JSON") {
123+
shouldThrow<Exception> {
124+
JsonMessage.parse("{")
125+
}
126+
}
127+
}
128+
}
129+
130+
context("AvModerationMessage") {
131+
context("Valid") {
132+
context("With minimal fields") {
133+
val parsed = JsonMessage.parse(
134+
"""
135+
{
136+
"type": "av_moderation",
137+
138+
}
139+
""".trimIndent()
140+
)
141+
parsed.shouldBeInstanceOf<AvModerationMessage>()
142+
parsed.room shouldBe "[email protected]"
143+
parsed.enabled shouldBe null
144+
parsed.mediaType shouldBe null
145+
parsed.actor shouldBe null
146+
parsed.whitelists shouldBe null
147+
}
148+
149+
context("With all fields") {
150+
val parsed = JsonMessage.parse(
151+
"""
152+
{
153+
"type": "av_moderation",
154+
"room": "[email protected]",
155+
"enabled": true,
156+
"mediaType": "AUDIO",
157+
"actor": "[email protected]",
158+
"whitelists": {
159+
160+
"VIDEO": ["[email protected]"]
161+
}
162+
}
163+
""".trimIndent()
164+
)
165+
parsed.shouldBeInstanceOf<AvModerationMessage>()
166+
parsed.room shouldBe "[email protected]"
167+
parsed.enabled shouldBe true
168+
parsed.mediaType shouldBe MediaType.AUDIO
169+
parsed.actor shouldBe "[email protected]"
170+
parsed.whitelists.shouldNotBeNull()
171+
parsed.whitelists!![MediaType.AUDIO] shouldBe listOf("[email protected]", "[email protected]")
172+
parsed.whitelists!![MediaType.VIDEO] shouldBe listOf("[email protected]")
173+
// parsed.whitelists!![MediaType.DESKTOP] shouldBe null
174+
}
175+
176+
context("With lowercase enum values") {
177+
val parsed = JsonMessage.parse(
178+
"""
179+
{
180+
"type": "av_moderation",
181+
"room": "[email protected]",
182+
"mediaType": "video",
183+
"whitelists": {
184+
"audio": ["[email protected]"]
185+
}
186+
}
187+
""".trimIndent()
188+
)
189+
parsed.shouldBeInstanceOf<AvModerationMessage>()
190+
parsed.mediaType shouldBe MediaType.VIDEO
191+
parsed.whitelists.shouldNotBeNull()
192+
parsed.whitelists!![MediaType.AUDIO] shouldBe listOf("[email protected]")
193+
// parsed.whitelists!![AvModerationMessage.MediaType.DESKTOP] shouldBe listOf("[email protected]")
194+
}
195+
196+
context("With mixed case enum values") {
197+
val parsed = JsonMessage.parse(
198+
"""
199+
{
200+
"type": "av_moderation",
201+
"room": "[email protected]",
202+
"mediaType": "vIdEo",
203+
"whitelists": {
204+
"AuDiO": ["[email protected]"],
205+
"ViDeO": ["[email protected]"]
206+
}
207+
}
208+
""".trimIndent()
209+
)
210+
parsed.shouldBeInstanceOf<AvModerationMessage>()
211+
parsed.mediaType shouldBe MediaType.VIDEO
212+
parsed.whitelists.shouldNotBeNull()
213+
parsed.whitelists!![MediaType.AUDIO] shouldBe listOf("[email protected]")
214+
parsed.whitelists!![MediaType.VIDEO] shouldBe listOf("[email protected]")
215+
}
216+
}
217+
218+
context("Invalid") {
219+
context("Invalid media type") {
220+
shouldThrow<Exception> {
221+
JsonMessage.parse(
222+
"""
223+
{
224+
"type": "av_moderation",
225+
"room": "[email protected]",
226+
"mediaType": "INVALID_TYPE"
227+
}
228+
""".trimIndent()
229+
)
230+
}
231+
}
232+
}
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)