Jitsi auth: fix some mistakes and gives the jwt to Jitsi

This commit is contained in:
ganfra 2021-05-20 18:53:56 +02:00
parent e4b65053d4
commit 894af10934
13 changed files with 183 additions and 178 deletions

View file

@ -43,5 +43,4 @@ interface ThirdPartyService {
* The generated token is only valid for exchanging for user information from the federation API for OpenID. * The generated token is only valid for exchanging for user information from the federation API for OpenID.
*/ */
suspend fun getOpenIdToken(): OpenIdToken suspend fun getOpenIdToken(): OpenIdToken
} }

View file

@ -57,6 +57,4 @@ internal interface ThirdPartyAPI {
suspend fun requestOpenIdToken(@Path("userId") userId: String, suspend fun requestOpenIdToken(@Path("userId") userId: String,
// We should post an empty body // We should post an empty body
@Body body: JsonDict = HashMap()): OpenIdToken @Body body: JsonDict = HashMap()): OpenIdToken
} }

View file

@ -25,4 +25,3 @@ fun String.toBase32String(padding: Boolean = true): String {
base32.replace("=", "") base32.replace("=", "")
} }
} }

View file

@ -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 { internal fun String.ensureTrailingSlash(): String {
return when { return when {
isEmpty() -> this isEmpty() -> this

View file

@ -20,12 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents
import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.jitsi.meet.sdk.JitsiMeetUserInfo
sealed class JitsiCallViewEvents : VectorViewEvents { sealed class JitsiCallViewEvents : VectorViewEvents {
data class StartConference( data class JoinConference(
val enableVideo: Boolean, val enableVideo: Boolean,
val jitsiUrl: String, val jitsiUrl: String,
val subject: String, val subject: String,
val confId: String, val confId: String,
val userInfo: JitsiMeetUserInfo val userInfo: JitsiMeetUserInfo,
val token: String?
) : JitsiCallViewEvents() ) : JitsiCallViewEvents()
data class ConfirmSwitchingConference( data class ConfirmSwitchingConference(
@ -33,5 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents {
) : JitsiCallViewEvents() ) : JitsiCallViewEvents()
object LeaveConference : JitsiCallViewEvents() object LeaveConference : JitsiCallViewEvents()
object FailJoiningConference: JitsiCallViewEvents()
object Finish : JitsiCallViewEvents() object Finish : JitsiCallViewEvents()
} }

View file

@ -27,24 +27,19 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.themes.ThemeProvider
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session 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.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType 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 org.matrix.android.sdk.rx.asObservable
import java.net.URL
class JitsiCallViewModel @AssistedInject constructor( class JitsiCallViewModel @AssistedInject constructor(
@Assisted initialState: JitsiCallViewState, @Assisted initialState: JitsiCallViewState,
private val session: Session, private val session: Session,
private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory, private val jitsiService: JitsiService
private val themeProvider: ThemeProvider
) : VectorViewModel<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(initialState) { ) : VectorViewModel<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -55,7 +50,7 @@ class JitsiCallViewModel @AssistedInject constructor(
private var currentWidgetObserver: Disposable? = null private var currentWidgetObserver: Disposable? = null
private val widgetService = session.widgetService() private val widgetService = session.widgetService()
private var confIsStarted = false private var confIsJoined = false
private var pendingArgs: VectorJitsiActivity.Args? = null private var pendingArgs: VectorJitsiActivity.Args? = null
init { init {
@ -63,7 +58,7 @@ class JitsiCallViewModel @AssistedInject constructor(
} }
private fun observeWidget(roomId: String, widgetId: String) { private fun observeWidget(roomId: String, widgetId: String) {
confIsStarted = false confIsJoined = false
currentWidgetObserver?.dispose() currentWidgetObserver?.dispose()
currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values()) currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values())
.asObservable() .asObservable()
@ -74,10 +69,9 @@ class JitsiCallViewModel @AssistedInject constructor(
setState { setState {
copy(widget = Success(jitsiWidget)) copy(widget = Success(jitsiWidget))
} }
if (!confIsJoined) {
if (!confIsStarted) { confIsJoined = true
confIsStarted = true joinConference(jitsiWidget)
startConference(jitsiWidget)
} }
} else { } else {
setState { setState {
@ -90,24 +84,15 @@ class JitsiCallViewModel @AssistedInject constructor(
.disposeOnClear() .disposeOnClear()
} }
private fun startConference(jitsiWidget: Widget) = withState { state -> private fun joinConference(jitsiWidget: Widget) = withState { state ->
val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem() viewModelScope.launch {
val userInfo = JitsiMeetUserInfo().apply { try {
displayName = me?.getBestName() val joinConference = jitsiService.joinConference(state.roomId, jitsiWidget, state.enableVideo)
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } _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) { override fun handle(action: JitsiCallViewActions) {

View file

@ -19,27 +19,38 @@ package im.vector.app.features.call.conference
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.network.await import im.vector.app.core.network.await
import im.vector.app.core.resources.StringProvider 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.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.raw.wellknown.getElementWellknown
import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.themes.ThemeProvider
import okhttp3.Request import okhttp3.Request
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session 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.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType 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.appendParamToUrl
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import java.net.URL
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
class JitsiService @Inject constructor( class JitsiService @Inject constructor(
private val session: Session, private val session: Session,
private val rawService: RawService, 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 { companion object {
const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt" 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 { suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget {
@ -49,8 +60,9 @@ class JitsiService @Inject constructor(
rawService.getElementWellknown(session.myUserId) rawService.getElementWellknown(session.myUserId)
?.jitsiServer ?.jitsiServer
?.preferredDomain ?.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 jitsiAuth = getJitsiAuth(jitsiDomain)
val confId = createConferenceId(roomId, jitsiAuth) val confId = createConferenceId(roomId, jitsiAuth)
@ -79,7 +91,7 @@ class JitsiService @Inject constructor(
"conferenceId" to confId, "conferenceId" to confId,
"domain" to jitsiDomain, "domain" to jitsiDomain,
"isAudioOnly" to !withVideo, "isAudioOnly" to !withVideo,
"authenticationType" to jitsiAuth JITSI_AUTH_KEY to jitsiAuth
), ),
"creatorUserId" to session.myUserId, "creatorUserId" to session.myUserId,
"id" to widgetId, "id" to widgetId,
@ -89,6 +101,49 @@ class JitsiService @Inject constructor(
return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) 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 { private fun createConferenceId(roomId: String, jitsiAuth: String?): String {
return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) { return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) {
// Create conference ID from room ID // 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 // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
roomId.toBase32String(padding = false) roomId.toBase32String(padding = false)
} else { } else {
// Create a random conference ID
// Create a random enough jitsi conference id // Create a random enough jitsi conference id
// Note: the jitsi server automatically creates conference when the conference // Note: the jitsi server automatically creates conference when the conference
// id does not exist yet // id does not exist yet
@ -110,7 +164,7 @@ class JitsiService @Inject constructor(
} }
private suspend fun getJitsiAuth(jitsiDomain: String): String? { 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 { return tryOrNull {
val response = session.getOkHttpClient().newCall(request).await() val response = session.getOkHttpClient().newCall(request).await()
val json = response.body?.string() ?: return null val json = response.body?.string() ?: return null

View file

@ -37,7 +37,7 @@ class JitsiWidgetPropertiesFactory @Inject constructor(
.orEmpty() .orEmpty()
return JitsiWidgetProperties( 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"], confId = configs["conferenceId"],
displayName = configs["displayName"], displayName = configs["displayName"],
avatarUrl = configs["avatarUrl"] avatarUrl = configs["avatarUrl"]

View file

@ -25,6 +25,7 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
@ -86,8 +87,9 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
jitsiViewModel.observeViewEvents { jitsiViewModel.observeViewEvents {
when (it) { when (it) {
is JitsiCallViewEvents.StartConference -> configureJitsiView(it) is JitsiCallViewEvents.JoinConference -> configureJitsiView(it)
is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it) is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it)
JitsiCallViewEvents.FailJoiningConference -> handleFailJoining()
JitsiCallViewEvents.Finish -> finish() JitsiCallViewEvents.Finish -> finish()
JitsiCallViewEvents.LeaveConference -> handleLeaveConference() JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
}.exhaustive }.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() val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder()
.setVideoMuted(!startConference.enableVideo) .setVideoMuted(!joinConference.enableVideo)
.setUserInfo(startConference.userInfo) .setUserInfo(joinConference.userInfo)
.setToken(joinConference.token)
.apply { .apply {
tryOrNull { URL(startConference.jitsiUrl) }?.let { tryOrNull { URL(joinConference.jitsiUrl) }?.let {
setServerURL(it) setServerURL(it)
} }
} }
@ -153,8 +161,8 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
.setFeatureFlag("add-people.enabled", false) .setFeatureFlag("add-people.enabled", false)
.setFeatureFlag("video-share.enabled", false) .setFeatureFlag("video-share.enabled", false)
.setFeatureFlag("call-integration.enabled", false) .setFeatureFlag("call-integration.enabled", false)
.setRoom(startConference.confId) .setRoom(joinConference.confId)
.setSubject(startConference.subject) .setSubject(joinConference.subject)
.build() .build()
jitsiMeetView?.join(jitsiMeetConferenceOptions) jitsiMeetView?.join(jitsiMeetConferenceOptions)
} }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.call.conference.jwt package im.vector.app.features.call.conference.jwt
import im.vector.app.core.utils.ensureProtocol
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
@ -27,28 +28,32 @@ class JitsiJWTFactory @Inject constructor() {
* Create a JWT token for jitsi openidtoken-jwt authentication * Create a JWT token for jitsi openidtoken-jwt authentication
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification * See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
*/ */
fun create(jitsiServerDomain: String, fun create(homeServerName: String,
jitsiServerDomain: String,
openIdAccessToken: String, openIdAccessToken: String,
roomId: String, roomId: String,
userAvatarUrl: String, userAvatarUrl: String,
userDisplayName: String): 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. // 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 key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
val context = mapOf( val context = mapOf(
"matrix" to mapOf(
"token" to openIdAccessToken,
"room_id" to roomId,
"server_name" to homeServerName
),
"user" to mapOf( "user" to mapOf(
"name" to userDisplayName, "name" to userDisplayName,
"avatar" to userAvatarUrl "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() return Jwts.builder()
.setIssuer(jitsiServerDomain) .setIssuer(jitsiServerDomain)
.setSubject(jitsiServerDomain) .setSubject(jitsiServerDomain)
.setAudience("https://$jitsiServerDomain") .setAudience(jitsiServerDomain.ensureProtocol())
// room is not used at the moment, a * works here. // room is not used at the moment, a * works here.
.claim("room", "*") .claim("room", "*")
.claim("context", context) .claim("context", context)

View file

@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider 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.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.CommandParser 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.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory 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.session.coroutineScope
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy 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.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue 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.Session
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.space.CreateSpaceParams 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.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.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import timber.log.Timber import timber.log.Timber
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -115,7 +111,6 @@ class RoomDetailViewModel @AssistedInject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val rainbowGenerator: RainbowGenerator, private val rainbowGenerator: RainbowGenerator,
private val session: Session, private val session: Session,
private val rawService: RawService,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler, private val stickerPickerActionHandler: StickerPickerActionHandler,
private val roomSummariesHolder: RoomSummariesHolder, private val roomSummariesHolder: RoomSummariesHolder,
@ -123,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val chatEffectManager: ChatEffectManager, private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
timelineSettingsFactory: TimelineSettingsFactory timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener {
@ -437,57 +433,8 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
viewModelScope.launch(Dispatchers.IO) { 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 { try {
val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo)
_viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo))
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget)))

View file

@ -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_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="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> <string name="room_widget_resource_permission_title">This widget wants to use the following resources:</string>