mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
Jitsi auth: fix some mistakes and gives the jwt to Jitsi
This commit is contained in:
parent
e4b65053d4
commit
894af10934
13 changed files with 183 additions and 178 deletions
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,4 +25,3 @@ fun String.toBase32String(padding: Boolean = true): String {
|
||||||
base32.replace("=", "")
|
base32.replace("=", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue