From 894af109343bf54d6c2f2946a3074fa78b893eab Mon Sep 17 00:00:00 2001
From: ganfra <francoisg@element.io>
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<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(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<ActivityJitsiBinding>(), 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<ActivityJitsiBinding>(), 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<ActivityJitsiBinding>(), 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<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(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 @@
 
 
     <string name="error_jitsi_not_supported_on_old_device">Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0)</string>
+    <string name="error_jitsi_join_conf">Sorry, an error occurred while trying to join the conference</string>
     <string name="jitsi_leave_conf_to_join_another_one_content">Leave the current conference and switch to the other one?</string>
 
     <string name="room_widget_resource_permission_title">This widget wants to use the following resources:</string>