Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a "connect" extension to colibri2. #108

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading