From 84e6a67a5155fb089fbff5f7aa3f52e4711cdb1f Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 27 Aug 2021 09:47:49 +0200 Subject: [PATCH] Jitsi widget: refact a bit so we use data instead of url when possible --- .../session/widgets/DefaultWidgetService.kt | 2 +- .../features/call/conference/JitsiService.kt | 32 ++--- ...WidgetProperties.kt => JitsiWidgetData.kt} | 18 ++- .../call/conference/JitsiWidgetDataFactory.kt | 61 +++++++++ .../JitsiWidgetPropertiesFactory.kt | 45 ------- .../conference/JitsiWidgetDataFactoryTest.kt | 122 ++++++++++++++++++ 6 files changed, 211 insertions(+), 69 deletions(-) rename vector/src/main/java/im/vector/app/features/call/conference/{JitsiWidgetProperties.kt => JitsiWidgetData.kt} (55%) create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt delete mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt create mode 100644 vector/src/test/java/im/vector/app/features/call/conference/JitsiWidgetDataFactoryTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt index 5912dc7b53..dfe4b6b810 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -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?, diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt index 7b01824c6c..d6b591d8a4 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt similarity index 55% rename from vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt rename to vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt index ed63f723c8..323de826a1 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetData.kt @@ -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 ) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt new file mode 100644 index 0000000000..bceb38d544 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetDataFactory.kt @@ -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() ?: 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"] + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt deleted file mode 100644 index cf4896a3e1..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt +++ /dev/null @@ -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"] - ) - } -} diff --git a/vector/src/test/java/im/vector/app/features/call/conference/JitsiWidgetDataFactoryTest.kt b/vector/src/test/java/im/vector/app/features/call/conference/JitsiWidgetDataFactoryTest.kt new file mode 100644 index 0000000000..f18fe32b20 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/call/conference/JitsiWidgetDataFactoryTest.kt @@ -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() + return Widget( + widgetContent = widgetContentModel!!, + event = event, + widgetId = WIDGET_ID, + senderInfo = SenderInfo(USER_ID, null, false, null), + isAddedByMe = true, + type = WidgetType.Jitsi + ) + } +}