Skip to content

Commit

Permalink
feat: Add a "connect" extension to colibri2.
Browse files Browse the repository at this point in the history
  • Loading branch information
bgrozev committed Oct 24, 2024
1 parent 3816e5a commit 7aec2bf
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ private ConferenceModifyIQ(Builder b)
rtcstatsEnabled = b.rtcstatsEnabled;
create = b.create;
expire = b.expire;
if (b.connects != null)
{
addExtension(b.connects);
}

if (b.meetingId == null)
{
Expand Down Expand Up @@ -170,6 +174,12 @@ public boolean getExpire()
return expire;
}

@Nullable
public Connects getConnects()
{
return getExtension(Connects.class);
}

@Contract("_ -> new")
public static @NotNull Builder builder(XMPPConnection connection)
{
Expand All @@ -196,6 +206,7 @@ public static final class Builder
private boolean expire = EXPIRE_DEFAULT;
private String conferenceName;
private String meetingId;
private Connects connects = null;

private Builder(IqData iqCommon)
{
Expand All @@ -218,6 +229,22 @@ public Builder setRtcstatsEnabled(boolean rtcstatsEnabled)
return this;
}

public Builder setEmptyConnects()
{
connects = new Connects();
return this;
}

public Builder addConnect(@NotNull Connect connect)
{
if (connects == null)
{
connects = new Connects();
}
connects.addConnect(connect);
return this;
}

public Builder setConferenceName(String name)
{
conferenceName = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ private static void doRegisterProviders()
/* Original colibri does something weird with these elements' namespaces, so register them here. */
ProviderManager.addExtensionProvider(ForceMute.ELEMENT, ForceMute.NAMESPACE, new ForceMute.Provider());
ProviderManager.addExtensionProvider(InitialLastN.ELEMENT, InitialLastN.NAMESPACE, new InitialLastNProvider());
ProviderManager.addExtensionProvider(Connect.ELEMENT, Connect.NAMESPACE, new ConnectProvider());
ProviderManager.addExtensionProvider(Connects.ELEMENT, Connects.NAMESPACE, new ConnectsProvider());
ProviderManager.addExtensionProvider(Capability.ELEMENT, Capability.NAMESPACE, new Capability.Provider());
ProviderManager.addExtensionProvider(Sctp.ELEMENT, Sctp.NAMESPACE, new Sctp.Provider());
ProviderManager.addExtensionProvider(Colibri2Error.ELEMENT,
Expand Down
100 changes: 100 additions & 0 deletions src/main/kotlin/org/jitsi/xmpp/extensions/colibri2/Connect.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.xmpp.extensions.colibri2

import org.jitsi.xmpp.extensions.AbstractPacketExtension
import org.jitsi.xmpp.extensions.DefaultPacketExtensionProvider
import org.jivesoftware.smack.packet.XmlEnvironment
import org.jivesoftware.smack.parsing.SmackParsingException
import org.jivesoftware.smack.xml.XmlPullParser
import org.jivesoftware.smack.xml.XmlPullParserException
import java.io.IOException
import java.net.URI

class Connect(
val url: URI,
val protocol: Protocols,
val type: Types,
audio: Boolean = false,
video: Boolean = false
) : AbstractPacketExtension(NAMESPACE, ELEMENT) {
init {
setAttribute(URL_ATTR_NAME, url)
setAttribute(PROTOCOL_ATTR_NAME, protocol.toString().lowercase())
setAttribute(TYPE_ATTR_NAME, type.toString().lowercase())
if (audio) {
setAttribute(AUDIO_ATTR_NAME, true)
}
if (video) {
setAttribute(VIDEO_ATTR_NAME, true)
}
}

val audio: Boolean
get() = getAttributeAsString(AUDIO_ATTR_NAME)?.toBoolean() ?: false
val video: Boolean
get() = getAttributeAsString(VIDEO_ATTR_NAME)?.toBoolean() ?: false

enum class Protocols(val value: String) {
MEDIAJSON("mediajson")
}

enum class Types(val value: String) {
RECORDER("recorder"),
TRANSCRIBER("transcriber")
}

companion object {
const val ELEMENT = "connect"
const val NAMESPACE = ConferenceModifyIQ.NAMESPACE
const val URL_ATTR_NAME = "url"
const val PROTOCOL_ATTR_NAME = "protocol"
const val TYPE_ATTR_NAME = "type"
const val AUDIO_ATTR_NAME = "audio"
const val VIDEO_ATTR_NAME = "video"
}
}

class ConnectProvider : DefaultPacketExtensionProvider<Connect>(Connect::class.java) {
@Throws(XmlPullParserException::class, IOException::class, SmackParsingException::class)
override fun parse(parser: XmlPullParser, depth: Int, xml: XmlEnvironment?): Connect {
val url = parser.getAttributeValue("", Connect.URL_ATTR_NAME)
?: throw SmackParsingException.RequiredAttributeMissingException("Missing 'url' attribute")
val uri = try {
URI(url)
} catch (e: Exception) {
throw SmackParsingException("Invalid 'url': ${e.message}")
}
val audio = parser.getAttributeValue("", Connect.AUDIO_ATTR_NAME)?.toBoolean() ?: false
val video = parser.getAttributeValue("", Connect.VIDEO_ATTR_NAME)?.toBoolean() ?: false
val protocolStr = parser.getAttributeValue("", Connect.PROTOCOL_ATTR_NAME)
?: throw SmackParsingException.RequiredAttributeMissingException("Missing 'protocol' attribute")
val protocol = try {
Connect.Protocols.valueOf(protocolStr.uppercase())
} catch (e: Exception) {
throw SmackParsingException("Invalid 'protocol': $protocolStr")
}
val typeStr = parser.getAttributeValue("", Connect.TYPE_ATTR_NAME)
?: throw SmackParsingException.RequiredAttributeMissingException("Missing 'type' attribute")
val type = try {
Connect.Types.valueOf(typeStr.uppercase())
} catch (e: Exception) {
throw SmackParsingException("Invalid 'type': $typeStr")
}

return Connect(url = uri, protocol = protocol, type = type, audio = audio, video = video)
}
}
32 changes: 32 additions & 0 deletions src/main/kotlin/org/jitsi/xmpp/extensions/colibri2/Connects.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.xmpp.extensions.colibri2

import org.jitsi.xmpp.extensions.AbstractPacketExtension
import org.jitsi.xmpp.extensions.DefaultPacketExtensionProvider

class Connects : AbstractPacketExtension(NAMESPACE, ELEMENT) {

fun getConnects(): List<Connect> = getChildExtensionsOfType(Connect::class.java)
fun addConnect(connect: Connect) = addChildExtension(connect)

companion object {
const val ELEMENT = "connects"
const val NAMESPACE = ConferenceModifyIQ.NAMESPACE
}
}

class ConnectsProvider : DefaultPacketExtensionProvider<Connects>(Connects::class.java)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import org.jitsi.xmpp.extensions.colibri2.Colibri2Endpoint
import org.jitsi.xmpp.extensions.colibri2.Colibri2Relay
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifiedIQ
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifyIQ
import org.jitsi.xmpp.extensions.colibri2.Connect
import org.jitsi.xmpp.extensions.colibri2.Connects
import org.jitsi.xmpp.extensions.colibri2.Endpoints
import org.jitsi.xmpp.extensions.colibri2.ForceMute
import org.jitsi.xmpp.extensions.colibri2.InitialLastN
Expand All @@ -37,6 +39,7 @@ import org.jivesoftware.smackx.muc.MUCRole
import org.json.simple.JSONArray
import org.json.simple.JSONObject
import java.lang.IllegalArgumentException
import java.net.URI

object Colibri2JSONDeserializer {
private fun deserializeMedia(media: JSONObject): Media {
Expand Down Expand Up @@ -356,6 +359,32 @@ object Colibri2JSONDeserializer {
setRtcstatsEnabled(it)
}
}

conferenceModify[Connects.ELEMENT]?.let {
if (it is JSONArray) {
var added = false
it.forEach { connect ->
if (connect is JSONObject) {
addConnect(
Connect(
URI(connect[Connect.URL_ATTR_NAME] as String),
protocol = Connect.Protocols.valueOf(
(connect[Connect.PROTOCOL_ATTR_NAME] as String).uppercase()
),
type = Connect.Types.valueOf(
(connect[Connect.TYPE_ATTR_NAME] as String).uppercase()
),
audio = connect[Connect.AUDIO_ATTR_NAME]?.toString()?.toBoolean() ?: false,
video = connect[Connect.VIDEO_ATTR_NAME]?.toString()?.toBoolean() ?: false
)
)
added = true
}
}
// An empty array is distinct from no value specified.
if (!added) setEmptyConnects()
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import org.jitsi.xmpp.extensions.colibri2.Colibri2Endpoint
import org.jitsi.xmpp.extensions.colibri2.Colibri2Relay
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifiedIQ
import org.jitsi.xmpp.extensions.colibri2.ConferenceModifyIQ
import org.jitsi.xmpp.extensions.colibri2.Connect
import org.jitsi.xmpp.extensions.colibri2.Connects
import org.jitsi.xmpp.extensions.colibri2.ForceMute
import org.jitsi.xmpp.extensions.colibri2.InitialLastN
import org.jitsi.xmpp.extensions.colibri2.Media
Expand Down Expand Up @@ -231,6 +233,18 @@ object Colibri2JSONSerializer {
}
}

private fun serializeConnect(connect: Connect) = JSONObject().apply {
put(Connect.URL_ATTR_NAME, connect.url.toString())
put(Connect.PROTOCOL_ATTR_NAME, connect.protocol.toString().lowercase())
put(Connect.TYPE_ATTR_NAME, connect.type.toString().lowercase())
if (connect.audio) put(Connect.AUDIO_ATTR_NAME, true)
if (connect.video) put(Connect.VIDEO_ATTR_NAME, true)
}

private fun serializeConnects(connects: Connects) = JSONArray().apply {
connects.getConnects().forEach { add(serializeConnect(it)) }
}

@JvmStatic
fun serializeConferenceModify(iq: ConferenceModifyIQ): JSONObject {
return serializeAbstractConferenceModificationIQ(iq).apply {
Expand All @@ -246,6 +260,10 @@ object Colibri2JSONSerializer {
put(ConferenceModifyIQ.RTCSTATS_ENABLED_ATTR_NAME, iq.isRtcstatsEnabled)
}

iq.connects?.let {
put(Connects.ELEMENT, serializeConnects(it))
}

put(ConferenceModifyIQ.MEETING_ID_ATTR_NAME, iq.meetingId)

iq.conferenceName?.let { put(ConferenceModifyIQ.NAME_ATTR_NAME, it) }
Expand Down
102 changes: 102 additions & 0 deletions src/test/kotlin/org/jitsi/xmpp/extensions/colibri2/ConnectTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.xmpp.extensions.colibri2

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import org.jivesoftware.smack.parsing.SmackParsingException
import org.jivesoftware.smack.util.PacketParserUtils
import java.net.URI

class ConnectTest : ShouldSpec() {
init {
IqProviderUtils.registerProviders()
val provider = ConnectProvider()
val url = "ws://example.com"

context("Parsing a valid extension") {
context("Without audio/video") {
val connect = provider.parse(
PacketParserUtils.getParserFor("<connect url='$url' protocol='mediajson' type='recorder'/>")
)
connect.url shouldBe URI(url)
connect.protocol shouldBe Connect.Protocols.MEDIAJSON
connect.type shouldBe Connect.Types.RECORDER
connect.audio shouldBe false
connect.video shouldBe false
}
context("With audio") {
val connect = provider.parse(
PacketParserUtils.getParserFor(
"<connect url='$url' protocol='mediajson' type='recorder' audio='true'/>"
)
)
connect.url shouldBe URI(url)
connect.protocol shouldBe Connect.Protocols.MEDIAJSON
connect.type shouldBe Connect.Types.RECORDER
connect.audio shouldBe true
connect.video shouldBe false
}
context("With video") {
val connect = provider.parse(
PacketParserUtils.getParserFor(
"<connect url='$url' protocol='mediajson' type='transcriber' audio='false' video='true'/>"
)
)
connect.url shouldBe URI(url)
connect.protocol shouldBe Connect.Protocols.MEDIAJSON
connect.type shouldBe Connect.Types.TRANSCRIBER
connect.audio shouldBe false
connect.video shouldBe true
}
}
context("Parsing with missing url") {
shouldThrow<SmackParsingException> {
provider.parse(
PacketParserUtils.getParserFor("<connect protocol='mediajson' type='recorder '></connect>")
)
}
}
context("Parsing with invalid url") {
shouldThrow<SmackParsingException> {
provider.parse(
PacketParserUtils.getParserFor("<connect url='in val id' protocol='mediajson' type='recorder'/>")
)
}
}
context("Parsing with missing protocol") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<connect url='$url' type='recorder'/>"))
}
}
context("Parsing with invalid protocol") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<connect url='$url' protocol='abc' type='recorder'/>"))
}
}
context("Parsing with missing type") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<connect url='$url' protocol='mediajson'/>"))
}
}
context("Parsing with invalid type") {
shouldThrow<SmackParsingException> {
provider.parse(PacketParserUtils.getParserFor("<connect url='$url' protocol='mediajson' type='inv'/>"))
}
}
}
}
Loading

0 comments on commit 7aec2bf

Please sign in to comment.