Jitsi widget: refact a bit so we use data instead of url when possible

This commit is contained in:
ganfra 2021-08-27 09:47:49 +02:00
parent dae035aa76
commit 84e6a67a51
6 changed files with 211 additions and 69 deletions

View File

@ -52,7 +52,7 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage
return widgetManager.getWidgetComputedUrl(widget, isLightTheme)
}
override fun getRoomWidgetsLive(
override fun getRoomWidgetsLive(
roomId: String,
widgetId: QueryStringValue,
widgetTypes: Set<String>?,

View File

@ -44,12 +44,16 @@ class JitsiService @Inject constructor(
private val rawService: RawService,
private val stringProvider: StringProvider,
private val themeProvider: ThemeProvider,
private val jitsiWidgetPropertiesFactory: JitsiWidgetPropertiesFactory,
private val jitsiJWTFactory: JitsiJWTFactory) {
companion object {
const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt"
private const val JITSI_AUTH_KEY = "auth"
}
private val jitsiWidgetDataFactory by lazy {
JitsiWidgetDataFactory(stringProvider.getString(R.string.preferred_jitsi_domain)) { widget ->
session.widgetService().getWidgetComputedUrl(widget, themeProvider.isLightTheme())
}
}
suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget {
@ -85,17 +89,11 @@ class JitsiService @Inject constructor(
val widgetEventContent = mapOf(
"url" to url,
"type" to WidgetType.Jitsi.legacy,
"data" to mapOf(
"conferenceId" to confId,
"domain" to jitsiDomain,
"isAudioOnly" to !withVideo,
JITSI_AUTH_KEY to jitsiAuth
),
"data" to JitsiWidgetData(jitsiDomain, confId, !withVideo, jitsiAuth),
"creatorUserId" to session.myUserId,
"id" to widgetId,
"name" to "jitsi"
)
return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent)
}
@ -108,26 +106,24 @@ class JitsiService @Inject constructor(
this.avatar = userAvatar?.let { URL(it) }
}
val roomName = session.getRoomSummary(roomId)?.displayName
val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException()
val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) {
getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "")
val widgetData = jitsiWidgetDataFactory.create(jitsiWidget)
val token = if (widgetData.isOpenIdJWTAuthenticationRequired()) {
getOpenIdJWTToken(roomId, widgetData.domain, userDisplayName ?: session.myUserId, userAvatar ?: "")
} else {
null
}
return JitsiCallViewEvents.JoinConference(
enableVideo = enableVideo,
jitsiUrl = properties.domain.ensureProtocol(),
jitsiUrl = widgetData.domain.ensureProtocol(),
subject = roomName ?: "",
confId = properties.confId ?: "",
confId = widgetData.confId,
userInfo = userInfo,
token = token
)
}
private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean {
return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH
private fun JitsiWidgetData.isOpenIdJWTAuthenticationRequired(): Boolean {
return auth == JITSI_OPEN_ID_TOKEN_JWT_AUTH
}
private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String {

View File

@ -16,9 +16,17 @@
package im.vector.app.features.call.conference
data class JitsiWidgetProperties(
val domain: String,
val confId: String?,
val displayName: String?,
val avatarUrl: String?
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This is jitsi widget data
* https://github.com/matrix-org/matrix-doc/blob/b910b8966524febe7ffe78f723127a5037defe64/api/widgets/definitions/jitsi_data.yaml
*/
@JsonClass(generateAdapter = true)
data class JitsiWidgetData(
@Json(name = "domain") val domain: String,
@Json(name = "conferenceId") val confId: String,
@Json(name = "isAudioOnly") val isAudioOnly: Boolean = false,
@Json(name = "auth") val auth: String? = null
)

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 im.vector.app.features.call.conference
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.widgets.model.Widget
import java.net.URL
import java.net.URLDecoder
class JitsiWidgetDataFactory(private val fallbackJitsiDomain: String, private val urlComputer: (Widget) -> String?) {
/**
* Extract JitsiWidgetData from a widget.
* For Widget V2, it will extract data from content.data
* For Widget V1, it will extract data from url.
*/
fun create(widget: Widget): JitsiWidgetData {
return widget.widgetContent.data.toModel<JitsiWidgetData>() ?: widget.createFromUrl()
}
/**
* This creates a JitsiWidgetData from the url.
* It's a fallback for Widget V1.
* It first get the computed url and then tries to extract JitsiWidgetData from it.
*/
private fun Widget.createFromUrl(): JitsiWidgetData {
return urlComputer(this)?.let { url -> createFromUrl(url) } ?: throw IllegalStateException()
}
private fun createFromUrl(url: String): JitsiWidgetData {
val configString = tryOrNull { URL(url) }?.query
val configs = configString?.split("&")
?.map { it.split("=") }
?.filter { it.size == 2 }
?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") }
?.toMap()
.orEmpty()
return JitsiWidgetData(
domain = configs["conferenceDomain"] ?: fallbackJitsiDomain,
confId = configs["conferenceId"] ?: configs["confId"] ?: throw IllegalStateException(),
isAudioOnly = configs["isAudioOnly"].toBoolean(),
auth = configs["auth"]
)
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 im.vector.app.features.call.conference
import android.net.Uri
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.extensions.tryOrNull
import java.net.URLDecoder
import javax.inject.Inject
class JitsiWidgetPropertiesFactory @Inject constructor(
private val stringProvider: StringProvider
) {
fun create(url: String): JitsiWidgetProperties {
val configString = tryOrNull { Uri.parse(url) }?.encodedQuery
val configs = configString?.split("&")
?.map { it.split("=") }
?.filter { it.size == 2 }
?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") }
?.toMap()
.orEmpty()
return JitsiWidgetProperties(
domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain),
confId = configs["conferenceId"] ?: configs["confId"],
displayName = configs["displayName"],
avatarUrl = configs["avatarUrl"]
)
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 im.vector.app.features.call.conference
import org.amshove.kluent.internal.assertFails
import org.junit.Assert.assertEquals
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.appendParamToUrl
private const val DOMAIN = "DOMAIN"
private const val CONF_ID = "CONF_ID"
private const val USER_ID = "USER_ID"
private const val WIDGET_ID = "WIDGET_ID"
class JitsiWidgetDataFactoryTest {
private val jitsiWidgetDataFactory = JitsiWidgetDataFactory(DOMAIN) { widget ->
// we don't need to compute here.
widget.widgetContent.url
}
@Test
fun jitsiWidget_V2_success() {
val widget = createWidgetV2()
val widgetData = jitsiWidgetDataFactory.create(widget)
assertEquals(widgetData.confId, CONF_ID)
assertEquals(widgetData.domain, DOMAIN)
}
@Test
fun jitsiWidget_V1_success() {
val widget = createWidgetV1(true)
val widgetData = jitsiWidgetDataFactory.create(widget)
assertEquals(widgetData.confId, CONF_ID)
assertEquals(widgetData.domain, DOMAIN)
}
@Test
fun jitsiWidget_V1_failure() {
val widget = createWidgetV1(false)
assertFails {
jitsiWidgetDataFactory.create(widget)
}
}
private fun createWidgetV1(successful: Boolean): Widget {
val url = buildString {
append("https://app.element.io/jitsi.html")
if (successful) {
appendParamToUrl("confId", CONF_ID)
}
append("#conferenceDomain=\$domain")
append("&conferenceId=\$conferenceId")
append("&isAudioOnly=\$isAudioOnly")
append("&displayName=\$matrix_display_name")
append("&avatarUrl=\$matrix_avatar_url")
append("&userId=\$matrix_user_id")
append("&roomId=\$matrix_room_id")
append("&theme=\$theme")
}
val widgetEventContent = mapOf(
"url" to url,
"type" to WidgetType.Jitsi.preferred,
"data" to mapOf(
"widgetSessionId" to WIDGET_ID
),
"creatorUserId" to USER_ID,
"id" to WIDGET_ID,
"name" to "jitsi"
)
return createWidgetWithContent(widgetEventContent)
}
private fun createWidgetV2(): Widget {
val widgetEventContent = mapOf(
// We don't care of url here because we have data field
"url" to "url",
"type" to WidgetType.Jitsi.preferred,
"data" to JitsiWidgetData(DOMAIN, CONF_ID, false).toContent(),
"creatorUserId" to USER_ID,
"id" to WIDGET_ID,
"name" to "jitsi"
)
return createWidgetWithContent(widgetEventContent)
}
private fun createWidgetWithContent(widgetContent: Content): Widget {
val event = Event(type = EventType.STATE_ROOM_WIDGET, eventId = "eventId", content = widgetContent)
val widgetContentModel = widgetContent.toModel<WidgetContent>()
return Widget(
widgetContent = widgetContentModel!!,
event = event,
widgetId = WIDGET_ID,
senderInfo = SenderInfo(USER_ID, null, false, null),
isAddedByMe = true,
type = WidgetType.Jitsi
)
}
}