From 894af109343bf54d6c2f2946a3074fa78b893eab Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 20 May 2021 18:53:56 +0200 Subject: [PATCH] Jitsi auth: fix some mistakes and gives the jwt to Jitsi --- .../session/thirdparty/ThirdPartyService.kt | 1 - .../session/thirdparty/ThirdPartyAPI.kt | 2 - .../java/im/vector/app/core/network/OkHttp.kt | 2 +- .../java/im/vector/app/core/utils/Base32.kt | 1 - .../java/im/vector/app/core/utils/UrlUtils.kt | 7 + .../call/conference/JitsiCallViewEvents.kt | 6 +- .../call/conference/JitsiCallViewModel.kt | 43 ++-- .../features/call/conference/JitsiService.kt | 64 +++++- .../JitsiWidgetPropertiesFactory.kt | 2 +- .../call/conference/VectorJitsiActivity.kt | 22 ++- .../call/conference/jwt/JitsiJWTFactory.kt | 27 +-- .../home/room/detail/RoomDetailViewModel.kt | 183 +++++++----------- vector/src/main/res/values/strings.xml | 1 + 13 files changed, 183 insertions(+), 178 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt index 708ff39c3a..28ac3832f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -43,5 +43,4 @@ interface ThirdPartyService { * The generated token is only valid for exchanging for user information from the federation API for OpenID. */ suspend fun getOpenIdToken(): OpenIdToken - } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt index 3e810a1a13..3c3f57a504 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -57,6 +57,4 @@ internal interface ThirdPartyAPI { suspend fun requestOpenIdToken(@Path("userId") userId: String, // We should post an empty body @Body body: JsonDict = HashMap()): OpenIdToken - - } diff --git a/vector/src/main/java/im/vector/app/core/network/OkHttp.kt b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt index 338ebab0b4..1bc6621771 100644 --- a/vector/src/main/java/im/vector/app/core/network/OkHttp.kt +++ b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt @@ -40,7 +40,7 @@ suspend fun Call.await(): Response { try { cancel() } catch (ex: Throwable) { - //Ignore cancel exception + // Ignore cancel exception } } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Base32.kt b/vector/src/main/java/im/vector/app/core/utils/Base32.kt index 4a42a252a1..9f220e08eb 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Base32.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Base32.kt @@ -25,4 +25,3 @@ fun String.toBase32String(padding: Boolean = true): String { base32.replace("=", "") } } - diff --git a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt index 095e01fa56..d292612e54 100644 --- a/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/UrlUtils.kt @@ -38,6 +38,13 @@ internal fun String.ensureProtocol(): String { } } +/** + * Ensure string do not starts with "http" or "https" protocol. + */ +internal fun String.ensureNoProtocol(): String { + return removePrefix("https://").removePrefix("http://") +} + internal fun String.ensureTrailingSlash(): String { return when { isEmpty() -> this diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt index d41f758f52..c8d570a73f 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt @@ -20,12 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents import org.jitsi.meet.sdk.JitsiMeetUserInfo sealed class JitsiCallViewEvents : VectorViewEvents { - data class StartConference( + data class JoinConference( val enableVideo: Boolean, val jitsiUrl: String, val subject: String, val confId: String, - val userInfo: JitsiMeetUserInfo + val userInfo: JitsiMeetUserInfo, + val token: String? ) : JitsiCallViewEvents() data class ConfirmSwitchingConference( @@ -33,5 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents { ) : JitsiCallViewEvents() object LeaveConference : JitsiCallViewEvents() + object FailJoiningConference: JitsiCallViewEvents() object Finish : JitsiCallViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 92dd2ebcd0..0fc85cb58c 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -27,24 +27,19 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.themes.ThemeProvider import io.reactivex.disposables.Disposable import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.rx.asObservable -import java.net.URL class JitsiCallViewModel @AssistedInject constructor( @Assisted initialState: JitsiCallViewState, private val session: Session, - private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory, - private val themeProvider: ThemeProvider + private val jitsiService: JitsiService ) : VectorViewModel(initialState) { @AssistedFactory @@ -55,7 +50,7 @@ class JitsiCallViewModel @AssistedInject constructor( private var currentWidgetObserver: Disposable? = null private val widgetService = session.widgetService() - private var confIsStarted = false + private var confIsJoined = false private var pendingArgs: VectorJitsiActivity.Args? = null init { @@ -63,7 +58,7 @@ class JitsiCallViewModel @AssistedInject constructor( } private fun observeWidget(roomId: String, widgetId: String) { - confIsStarted = false + confIsJoined = false currentWidgetObserver?.dispose() currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values()) .asObservable() @@ -74,10 +69,9 @@ class JitsiCallViewModel @AssistedInject constructor( setState { copy(widget = Success(jitsiWidget)) } - - if (!confIsStarted) { - confIsStarted = true - startConference(jitsiWidget) + if (!confIsJoined) { + confIsJoined = true + joinConference(jitsiWidget) } } else { setState { @@ -90,24 +84,15 @@ class JitsiCallViewModel @AssistedInject constructor( .disposeOnClear() } - private fun startConference(jitsiWidget: Widget) = withState { state -> - val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem() - val userInfo = JitsiMeetUserInfo().apply { - displayName = me?.getBestName() - avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } + private fun joinConference(jitsiWidget: Widget) = withState { state -> + viewModelScope.launch { + try { + val joinConference = jitsiService.joinConference(state.roomId, jitsiWidget, state.enableVideo) + _viewEvents.post(joinConference) + } catch (throwable: Throwable) { + _viewEvents.post(JitsiCallViewEvents.FailJoiningConference) + } } - val roomName = session.getRoomSummary(state.roomId)?.displayName - - val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) - ?.let { url -> jitsiMeetPropertiesFactory.create(url) } - - _viewEvents.post(JitsiCallViewEvents.StartConference( - enableVideo = state.enableVideo, - jitsiUrl = "https://${ppt?.domain}", - subject = roomName ?: "", - confId = ppt?.confId ?: "", - userInfo = userInfo - )) } override fun handle(action: JitsiCallViewActions) { 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 c3632d282a..d49ffc1306 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 @@ -19,27 +19,38 @@ package im.vector.app.features.call.conference import im.vector.app.R import im.vector.app.core.network.await import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureNoProtocol +import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.toBase32String +import im.vector.app.features.call.conference.jwt.JitsiJWTFactory import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.themes.ThemeProvider import okhttp3.Request +import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.appendParamToUrl +import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.di.MoshiProvider +import java.net.URL import java.util.UUID import javax.inject.Inject class JitsiService @Inject constructor( private val session: Session, private val rawService: RawService, - private val stringProvider: StringProvider) { + 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" } suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget { @@ -49,8 +60,9 @@ class JitsiService @Inject constructor( rawService.getElementWellknown(session.myUserId) ?.jitsiServer ?.preferredDomain + ?.ensureNoProtocol() } - val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) + val jitsiDomain = (preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)) val jitsiAuth = getJitsiAuth(jitsiDomain) val confId = createConferenceId(roomId, jitsiAuth) @@ -79,7 +91,7 @@ class JitsiService @Inject constructor( "conferenceId" to confId, "domain" to jitsiDomain, "isAudioOnly" to !withVideo, - "authenticationType" to jitsiAuth + JITSI_AUTH_KEY to jitsiAuth ), "creatorUserId" to session.myUserId, "id" to widgetId, @@ -89,6 +101,49 @@ class JitsiService @Inject constructor( return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) } + suspend fun joinConference(roomId: String, jitsiWidget: Widget, enableVideo: Boolean): JitsiCallViewEvents.JoinConference { + val me = session.getRoomMember(session.myUserId, roomId)?.toMatrixItem() + val userDisplayName = me?.getBestName() + val userAvatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) } + val userInfo = JitsiMeetUserInfo().apply { + this.displayName = userDisplayName + 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 ?: "") + } else { + null + } + return JitsiCallViewEvents.JoinConference( + enableVideo = enableVideo, + jitsiUrl = properties.domain.ensureProtocol(), + subject = roomName ?: "", + confId = properties.confId ?: "", + userInfo = userInfo, + token = token + ) + } + + private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean { + return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH + } + + private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String { + val openIdToken = session.thirdPartyService().getOpenIdToken() + return jitsiJWTFactory.create( + homeServerName = session.sessionParams.homeServerUrl.ensureNoProtocol(), + jitsiServerDomain = domain, + openIdAccessToken = openIdToken.accessToken, + roomId = roomId, + userAvatarUrl = userAvatar, + userDisplayName = userDisplayName + ) + } + private fun createConferenceId(roomId: String, jitsiAuth: String?): String { return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) { // Create conference ID from room ID @@ -97,7 +152,6 @@ class JitsiService @Inject constructor( // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification roomId.toBase32String(padding = false) } else { - // Create a random conference ID // Create a random enough jitsi conference id // Note: the jitsi server automatically creates conference when the conference // id does not exist yet @@ -110,7 +164,7 @@ class JitsiService @Inject constructor( } private suspend fun getJitsiAuth(jitsiDomain: String): String? { - val request = Request.Builder().url("https://$jitsiDomain/.well-known/element/jitsi").build() + val request = Request.Builder().url("$jitsiDomain/.well-known/element/jitsi".ensureProtocol()).build() return tryOrNull { val response = session.getOkHttpClient().newCall(request).await() val json = response.body?.string() ?: return null 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 index 8014e01fb2..8ba8ec0c75 100644 --- 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 @@ -37,7 +37,7 @@ class JitsiWidgetPropertiesFactory @Inject constructor( .orEmpty() return JitsiWidgetProperties( - domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain), + domain = (configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain)), confId = configs["conferenceId"], displayName = configs["displayName"], avatarUrl = configs["avatarUrl"] diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 3f2d52e9e7..15346422a6 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -25,6 +25,7 @@ import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -86,8 +87,9 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee jitsiViewModel.observeViewEvents { when (it) { - is JitsiCallViewEvents.StartConference -> configureJitsiView(it) + is JitsiCallViewEvents.JoinConference -> configureJitsiView(it) is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it) + JitsiCallViewEvents.FailJoiningConference -> handleFailJoining() JitsiCallViewEvents.Finish -> finish() JitsiCallViewEvents.LeaveConference -> handleLeaveConference() }.exhaustive @@ -138,12 +140,18 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee } } - private fun configureJitsiView(startConference: JitsiCallViewEvents.StartConference) { + private fun handleFailJoining() { + Toast.makeText(this, getString(R.string.error_jitsi_join_conf), Toast.LENGTH_LONG).show() + finish() + } + + private fun configureJitsiView(joinConference: JitsiCallViewEvents.JoinConference) { val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder() - .setVideoMuted(!startConference.enableVideo) - .setUserInfo(startConference.userInfo) + .setVideoMuted(!joinConference.enableVideo) + .setUserInfo(joinConference.userInfo) + .setToken(joinConference.token) .apply { - tryOrNull { URL(startConference.jitsiUrl) }?.let { + tryOrNull { URL(joinConference.jitsiUrl) }?.let { setServerURL(it) } } @@ -153,8 +161,8 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee .setFeatureFlag("add-people.enabled", false) .setFeatureFlag("video-share.enabled", false) .setFeatureFlag("call-integration.enabled", false) - .setRoom(startConference.confId) - .setSubject(startConference.subject) + .setRoom(joinConference.confId) + .setSubject(joinConference.subject) .build() jitsiMeetView?.join(jitsiMeetConferenceOptions) } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt index 7e9458841a..68475232c7 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt @@ -16,6 +16,7 @@ package im.vector.app.features.call.conference.jwt +import im.vector.app.core.utils.ensureProtocol import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys @@ -27,28 +28,32 @@ class JitsiJWTFactory @Inject constructor() { * Create a JWT token for jitsi openidtoken-jwt authentication * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification */ - fun create(jitsiServerDomain: String, - openIdAccessToken: String, - roomId: String, - userAvatarUrl: String, - userDisplayName: String): String { - + fun create(homeServerName: String, + jitsiServerDomain: String, + openIdAccessToken: String, + roomId: String, + userAvatarUrl: String, + userDisplayName: String): String { // The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack. val key = Keys.secretKeyFor(SignatureAlgorithm.HS256) val context = mapOf( + "matrix" to mapOf( + "token" to openIdAccessToken, + "room_id" to roomId, + "server_name" to homeServerName + ), "user" to mapOf( "name" to userDisplayName, "avatar" to userAvatarUrl - ), - "matrix" to mapOf( - "token" to openIdAccessToken, - "room_id" to roomId ) ) + // As per Jitsi token auth, `iss` needs to be set to something agreed between + // JWT generating side and Prosody config. Since we have no configuration for + // the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense. return Jwts.builder() .setIssuer(jitsiServerDomain) .setSubject(jitsiServerDomain) - .setAudience("https://$jitsiServerDomain") + .setAudience(jitsiServerDomain.ensureProtocol()) // room is not used at the moment, a * works here. .claim("room", "*") .claim("context", context) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 3a9969b43c..44392309e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser @@ -52,9 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsF import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory -import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy @@ -68,7 +67,6 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -99,13 +97,11 @@ import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber -import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -115,7 +111,6 @@ class RoomDetailViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val rainbowGenerator: RainbowGenerator, private val session: Session, - private val rawService: RawService, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummariesHolder: RoomSummariesHolder, @@ -123,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val callManager: WebRtcCallManager, private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, + private val jitsiService: JitsiService, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { @@ -186,7 +182,7 @@ class RoomDetailViewModel @AssistedInject constructor( tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } // Inform the SDK that the room is displayed - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { tryOrNull { session.onRoomDisplayed(initialState.roomId) } } callManager.addPstnSupportListener(this) @@ -267,67 +263,67 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) - is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.AcceptCall -> handleAcceptCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) - is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) - is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) - RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() - RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() - is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) - RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) - is RoomDetailAction.ShowRoomAvatarFullScreen -> { + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { _viewEvents.post( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } - is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) - RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() - RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) + RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() + RoomDetailAction.ResendAll -> handleResendAll() }.exhaustive } @@ -437,57 +433,8 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) viewModelScope.launch(Dispatchers.IO) { - // Build data for a jitsi widget - val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() - - // Create a random enough jitsi conference id - // Note: the jitsi server automatically creates conference when the conference - // id does not exist yet - var widgetSessionId = UUID.randomUUID().toString() - - if (widgetSessionId.length > 8) { - widgetSessionId = widgetSessionId.substring(0, 7) - } - val roomId: String = room.roomId - val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) - - val preferredJitsiDomain = tryOrNull { - rawService.getElementWellknown(session.myUserId) - ?.jitsiServer - ?.preferredDomain - } - val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) - - // We use the default element wrapper for this widget - // https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md - // https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469 - val url = buildString { - append("https://app.element.io/jitsi.html") - appendParamToUrl("confId", confId) - 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.legacy, - "data" to mapOf( - "conferenceId" to confId, - "domain" to jitsiDomain, - "isAudioOnly" to !action.withVideo - ), - "creatorUserId" to session.myUserId, - "id" to widgetId, - "name" to "jitsi" - ) - try { - val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) + val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -670,13 +617,13 @@ class RoomDetailViewModel @AssistedInject constructor( } when (itemId) { R.id.timeline_setting -> true - R.id.invite -> state.canInvite + R.id.invite -> state.canInvite R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() - R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() - R.id.search -> true - R.id.dev_tools -> vectorPreferences.developerMode() + R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() + R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() + R.id.search -> true + R.id.dev_tools -> vectorPreferences.developerMode() else -> false } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index aa85a52ec3..1fcd406364 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1675,6 +1675,7 @@ Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0) + Sorry, an error occurred while trying to join the conference Leave the current conference and switch to the other one? This widget wants to use the following resources: