diff --git a/CHANGES.md b/CHANGES.md index 50db24d623..56e36e3192 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,10 @@ Changes in Element 1.1.8 (2021-XX-XX) =================================================== Features ✨: - - + - Improvements 🙌: - - + - Support Jitsi authentication (#3379) Bugfix 🐛: - Space Invite by link not always displayed for public space (#3345) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 86252665a6..b5f90e87ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -233,6 +234,11 @@ interface Session : */ fun spaceService(): SpaceService + /** + * Returns the open id service associated with the session + */ + fun openIdService(): OpenIdService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt new file mode 100644 index 0000000000..65f6214f93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.openid + +interface OpenIdService { + + /** + * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix. + * 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/api/session/openid/OpenIdToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt new file mode 100644 index 0000000000..2c2ea65681 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OpenIdToken( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val accessToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 53e13c14ec..b100a336a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesServi import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -125,6 +126,7 @@ internal class DefaultSession @Inject constructor( private val thirdPartyService: Lazy, private val callSignalingService: Lazy, private val spaceService: Lazy, + private val openIdService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, @@ -289,6 +291,8 @@ internal class DefaultSession @Inject constructor( override fun spaceService(): SpaceService = spaceService.get() + override fun openIdService(): OpenIdService = openIdService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 63423b72c6..de74b34818 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService +import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService @@ -82,6 +83,7 @@ import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapab import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor @@ -373,6 +375,9 @@ internal abstract class SessionModule { @Binds abstract fun bindPermalinkService(service: DefaultPermalinkService): PermalinkService + @Binds + abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService + @Binds abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt index 1671859585..9d990d4d8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt @@ -16,9 +16,9 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -52,5 +52,5 @@ internal interface IdentityAuthAPI { * The request body is the same as the values returned by /openid/request_token in the Client-Server API. */ @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") - suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse + suspend fun register(@Body openIdToken: OpenIdToken): IdentityRegisterResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt index 8cc854bd94..1800d0eebe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt @@ -16,16 +16,16 @@ package org.matrix.android.sdk.internal.session.identity +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface IdentityRegisterTask : Task { data class Params( val identityAuthAPI: IdentityAuthAPI, - val openIdTokenResponse: RequestOpenIdTokenResponse + val openIdToken: OpenIdToken ) } @@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { return executeRequest(null) { - params.identityAuthAPI.register(params.openIdTokenResponse) + params.identityAuthAPI.register(params.openIdToken) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt new file mode 100644 index 0000000000..b90a2435f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.openid + +import org.matrix.android.sdk.api.session.openid.OpenIdService +import org.matrix.android.sdk.api.session.openid.OpenIdToken +import javax.inject.Inject + +internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService { + + override suspend fun getOpenIdToken(): OpenIdToken { + return getOpenIdTokenTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt index 8481a6ab93..a6ad025b8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt @@ -16,20 +16,21 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetOpenIdTokenTask : Task +internal interface GetOpenIdTokenTask : Task internal class DefaultGetOpenIdTokenTask @Inject constructor( @UserId private val userId: String, private val openIdAPI: OpenIdAPI, private val globalErrorReceiver: GlobalErrorReceiver) : GetOpenIdTokenTask { - override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + override suspend fun execute(params: Unit): OpenIdToken { return executeRequest(globalErrorReceiver) { openIdAPI.openIdToken(userId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt index ed090b845d..eb8c841d57 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.openid +import org.matrix.android.sdk.api.session.openid.OpenIdToken import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.http.Body @@ -34,5 +35,5 @@ internal interface OpenIdAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") suspend fun openIdToken(@Path("userId") userId: String, - @Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse + @Body body: JsonDict = emptyMap()): OpenIdToken } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt index 6652628026..bfc243c213 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt @@ -15,7 +15,7 @@ */ package org.matrix.android.sdk.internal.session.widgets -import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse +import org.matrix.android.sdk.api.session.openid.OpenIdToken import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -29,7 +29,7 @@ internal interface WidgetsAPI { * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961) */ @POST("register") - suspend fun register(@Body body: RequestOpenIdTokenResponse, + suspend fun register(@Body body: OpenIdToken, @Query("v") version: String?): RegisterWidgetResponse @GET("account") diff --git a/vector/build.gradle b/vector/build.gradle index aa962fd422..7ebc2a32fd 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -308,6 +308,7 @@ dependencies { def arch_version = '2.1.0' def lifecycle_version = '2.2.0' def rxbinding_version = '3.1.0' + def jjwt_version = '0.11.2' // Tests def kluent_version = '1.65' @@ -449,9 +450,9 @@ dependencies { // Jitsi implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') { - exclude group: 'com.google.firebase' - exclude group: 'com.google.android.gms' - exclude group: 'com.android.installreferrer' + exclude group: 'com.google.firebase' + exclude group: 'com.google.android.gms' + exclude group: 'com.android.installreferrer' } // QR-code @@ -465,6 +466,15 @@ dependencies { implementation 'im.dlg:android-dialer:1.2.5' + // JWT + api "io.jsonwebtoken:jjwt-api:$jjwt_version" + runtimeOnly "io.jsonwebtoken:jjwt-impl:$jjwt_version" + runtimeOnly("io.jsonwebtoken:jjwt-orgjson:$jjwt_version") { + exclude group: 'org.json', module: 'json' //provided by Android natively + } + implementation 'commons-codec:commons-codec:1.15' + + // TESTS testImplementation 'junit:junit:4.13.2' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" 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 new file mode 100644 index 0000000000..1bc6621771 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/network/OkHttp.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.network + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) + } + + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + continuation.resumeWithException(e) + } + }) + continuation.invokeOnCancellation { + try { + cancel() + } catch (ex: Throwable) { + // 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 new file mode 100644 index 0000000000..3ccc562057 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/Base32.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.utils + +import org.apache.commons.codec.binary.Base32 + +fun String.toBase32String(padding: Boolean = true): String { + val base32 = Base32().encodeAsString(toByteArray()) + return if (padding) { + base32 + } else { + base32.trimEnd('=') + } +} 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..70cda17ae6 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 @@ -30,7 +30,7 @@ fun String.isValidUrl(): Boolean { /** * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty */ -internal fun String.ensureProtocol(): String { +fun String.ensureProtocol(): String { return when { isEmpty() -> this !startsWith("http") -> "https://$this" @@ -38,7 +38,7 @@ internal fun String.ensureProtocol(): String { } } -internal fun String.ensureTrailingSlash(): String { +fun String.ensureTrailingSlash(): String { return when { isEmpty() -> this !endsWith("/") -> "$this/" diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt index d41f758f52..c8d570a73f 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewEvents.kt @@ -20,12 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents import org.jitsi.meet.sdk.JitsiMeetUserInfo sealed class JitsiCallViewEvents : VectorViewEvents { - data class StartConference( + data class JoinConference( val enableVideo: Boolean, val jitsiUrl: String, val subject: String, val confId: String, - val userInfo: JitsiMeetUserInfo + val userInfo: JitsiMeetUserInfo, + val token: String? ) : JitsiCallViewEvents() data class ConfirmSwitchingConference( @@ -33,5 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents { ) : JitsiCallViewEvents() object LeaveConference : JitsiCallViewEvents() + object FailJoiningConference: JitsiCallViewEvents() object Finish : JitsiCallViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 92dd2ebcd0..0fc85cb58c 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -27,24 +27,19 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.themes.ThemeProvider import io.reactivex.disposables.Disposable import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.rx.asObservable -import java.net.URL class JitsiCallViewModel @AssistedInject constructor( @Assisted initialState: JitsiCallViewState, private val session: Session, - private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory, - private val themeProvider: ThemeProvider + private val jitsiService: JitsiService ) : VectorViewModel(initialState) { @AssistedFactory @@ -55,7 +50,7 @@ class JitsiCallViewModel @AssistedInject constructor( private var currentWidgetObserver: Disposable? = null private val widgetService = session.widgetService() - private var confIsStarted = false + private var confIsJoined = false private var pendingArgs: VectorJitsiActivity.Args? = null init { @@ -63,7 +58,7 @@ class JitsiCallViewModel @AssistedInject constructor( } private fun observeWidget(roomId: String, widgetId: String) { - confIsStarted = false + confIsJoined = false currentWidgetObserver?.dispose() currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values()) .asObservable() @@ -74,10 +69,9 @@ class JitsiCallViewModel @AssistedInject constructor( setState { copy(widget = Success(jitsiWidget)) } - - if (!confIsStarted) { - confIsStarted = true - startConference(jitsiWidget) + if (!confIsJoined) { + confIsJoined = true + joinConference(jitsiWidget) } } else { setState { @@ -90,24 +84,15 @@ class JitsiCallViewModel @AssistedInject constructor( .disposeOnClear() } - private fun startConference(jitsiWidget: Widget) = withState { state -> - val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem() - val userInfo = JitsiMeetUserInfo().apply { - displayName = me?.getBestName() - avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) } + private fun joinConference(jitsiWidget: Widget) = withState { state -> + viewModelScope.launch { + try { + val joinConference = jitsiService.joinConference(state.roomId, jitsiWidget, state.enableVideo) + _viewEvents.post(joinConference) + } catch (throwable: Throwable) { + _viewEvents.post(JitsiCallViewEvents.FailJoiningConference) + } } - val roomName = session.getRoomSummary(state.roomId)?.displayName - - val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) - ?.let { url -> jitsiMeetPropertiesFactory.create(url) } - - _viewEvents.post(JitsiCallViewEvents.StartConference( - enableVideo = state.enableVideo, - jitsiUrl = "https://${ppt?.domain}", - subject = roomName ?: "", - confId = ppt?.confId ?: "", - userInfo = userInfo - )) } override fun handle(action: JitsiCallViewActions) { diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt new file mode 100644 index 0000000000..c9fb8fbccd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import im.vector.app.R +import im.vector.app.core.network.await +import im.vector.app.core.resources.StringProvider +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 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 { + // Build data for a jitsi widget + val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() + val preferredJitsiDomain = tryOrNull { + rawService.getElementWellknown(session.myUserId) + ?.jitsiServer + ?.preferredDomain + } + val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) + val jitsiAuth = getJitsiAuth(jitsiDomain) + val confId = createConferenceId(roomId, jitsiAuth) + + // 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") + if (jitsiAuth != null) { + append("&auth=$jitsiAuth") + } + } + val widgetEventContent = mapOf( + "url" to url, + "type" to WidgetType.Jitsi.legacy, + "data" to mapOf( + "conferenceId" to confId, + "domain" to jitsiDomain, + "isAudioOnly" to !withVideo, + JITSI_AUTH_KEY to jitsiAuth + ), + "creatorUserId" to session.myUserId, + "id" to widgetId, + "name" to "jitsi" + ) + + 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.openIdService().getOpenIdToken() + return jitsiJWTFactory.create( + openIdToken = openIdToken, + jitsiServerDomain = domain, + 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 + // For compatibility with Jitsi, use base32 without padding. + // More details here: + // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification + roomId.toBase32String(padding = false) + } else { + // 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) + } + roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) + } + } + + private suspend fun getJitsiAuth(jitsiDomain: String): String? { + 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 + MoshiProvider.providesMoshi().adapter(JitsiWellKnown::class.java).fromJson(json)?.auth + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt new file mode 100644 index 0000000000..b18831f050 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWellKnown.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class JitsiWellKnown( + @Json(name = "auth") val auth: String +) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 3f2d52e9e7..15346422a6 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -25,6 +25,7 @@ import android.content.res.Configuration import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -86,8 +87,9 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee jitsiViewModel.observeViewEvents { when (it) { - is JitsiCallViewEvents.StartConference -> configureJitsiView(it) + is JitsiCallViewEvents.JoinConference -> configureJitsiView(it) is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it) + JitsiCallViewEvents.FailJoiningConference -> handleFailJoining() JitsiCallViewEvents.Finish -> finish() JitsiCallViewEvents.LeaveConference -> handleLeaveConference() }.exhaustive @@ -138,12 +140,18 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee } } - private fun configureJitsiView(startConference: JitsiCallViewEvents.StartConference) { + private fun handleFailJoining() { + Toast.makeText(this, getString(R.string.error_jitsi_join_conf), Toast.LENGTH_LONG).show() + finish() + } + + private fun configureJitsiView(joinConference: JitsiCallViewEvents.JoinConference) { val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder() - .setVideoMuted(!startConference.enableVideo) - .setUserInfo(startConference.userInfo) + .setVideoMuted(!joinConference.enableVideo) + .setUserInfo(joinConference.userInfo) + .setToken(joinConference.token) .apply { - tryOrNull { URL(startConference.jitsiUrl) }?.let { + tryOrNull { URL(joinConference.jitsiUrl) }?.let { setServerURL(it) } } @@ -153,8 +161,8 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee .setFeatureFlag("add-people.enabled", false) .setFeatureFlag("video-share.enabled", false) .setFeatureFlag("call-integration.enabled", false) - .setRoom(startConference.confId) - .setSubject(startConference.subject) + .setRoom(joinConference.confId) + .setSubject(joinConference.subject) .build() jitsiMeetView?.join(jitsiMeetConferenceOptions) } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt new file mode 100644 index 0000000000..39b87c5d63 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.call.conference.jwt + +import im.vector.app.core.utils.ensureProtocol +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys +import org.matrix.android.sdk.api.session.openid.OpenIdToken +import javax.inject.Inject + +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(openIdToken: OpenIdToken, + jitsiServerDomain: 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 openIdToken.accessToken, + "room_id" to roomId, + "server_name" to openIdToken.matrixServerName + ), + "user" to mapOf( + "name" to userDisplayName, + "avatar" to userAvatarUrl + ) + ) + // 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(jitsiServerDomain.ensureProtocol()) + // room is not used at the moment, a * works here. + .claim("room", "*") + .claim("context", context) + .signWith(key) + .compact() + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 3a9969b43c..44392309e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser @@ -52,9 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsF import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory -import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorLocale import im.vector.app.features.settings.VectorPreferences import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy @@ -68,7 +67,6 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.crypto.MXCryptoError @@ -99,13 +97,11 @@ import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber -import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -115,7 +111,6 @@ class RoomDetailViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val rainbowGenerator: RainbowGenerator, private val session: Session, - private val rawService: RawService, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, private val roomSummariesHolder: RoomSummariesHolder, @@ -123,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val callManager: WebRtcCallManager, private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, + private val jitsiService: JitsiService, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { @@ -186,7 +182,7 @@ class RoomDetailViewModel @AssistedInject constructor( tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } } // Inform the SDK that the room is displayed - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { tryOrNull { session.onRoomDisplayed(initialState.roomId) } } callManager.addPstnSupportListener(this) @@ -267,67 +263,67 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) - is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.AcceptCall -> handleAcceptCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) - is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) - is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) - RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() - RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() - is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) - RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) - is RoomDetailAction.ShowRoomAvatarFullScreen -> { + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { _viewEvents.post( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } - is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) - RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() - RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) + RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() + RoomDetailAction.ResendAll -> handleResendAll() }.exhaustive } @@ -437,57 +433,8 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) viewModelScope.launch(Dispatchers.IO) { - // Build data for a jitsi widget - val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis() - - // Create a random enough jitsi conference id - // Note: the jitsi server automatically creates conference when the conference - // id does not exist yet - var widgetSessionId = UUID.randomUUID().toString() - - if (widgetSessionId.length > 8) { - widgetSessionId = widgetSessionId.substring(0, 7) - } - val roomId: String = room.roomId - val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale) - - val preferredJitsiDomain = tryOrNull { - rawService.getElementWellknown(session.myUserId) - ?.jitsiServer - ?.preferredDomain - } - val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain) - - // We use the default element wrapper for this widget - // https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md - // https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469 - val url = buildString { - append("https://app.element.io/jitsi.html") - appendParamToUrl("confId", confId) - append("#conferenceDomain=\$domain") - append("&conferenceId=\$conferenceId") - append("&isAudioOnly=\$isAudioOnly") - append("&displayName=\$matrix_display_name") - append("&avatarUrl=\$matrix_avatar_url") - append("&userId=\$matrix_user_id") - append("&roomId=\$matrix_room_id") - append("&theme=\$theme") - } - val widgetEventContent = mapOf( - "url" to url, - "type" to WidgetType.Jitsi.legacy, - "data" to mapOf( - "conferenceId" to confId, - "domain" to jitsiDomain, - "isAudioOnly" to !action.withVideo - ), - "creatorUserId" to session.myUserId, - "id" to widgetId, - "name" to "jitsi" - ) - try { - val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) + val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo) _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) @@ -670,13 +617,13 @@ class RoomDetailViewModel @AssistedInject constructor( } when (itemId) { R.id.timeline_setting -> true - R.id.invite -> state.canInvite + R.id.invite -> state.canInvite R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() - R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() - R.id.search -> true - R.id.dev_tools -> vectorPreferences.developerMode() + R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() + R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() + R.id.search -> true + R.id.dev_tools -> vectorPreferences.developerMode() else -> false } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 9473f01ccd..8649b14ca3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1675,6 +1675,7 @@ Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0) + Sorry, an error occurred while trying to join the conference Leave the current conference and switch to the other one? This widget wants to use the following resources: