Bump Rust SDK version and adapt our code (#3068)

* Use the new SDK version
* Adapt the authentication service to the new Rust SDK APIs
* Remove `Timeline.enterSpecialMode(...)` as it's no longer necessary
This commit is contained in:
Jorge Martin Espinosa
2024-06-27 11:44:14 +02:00
committed by GitHub
parent f163852c4b
commit cdbb46fa22
14 changed files with 85 additions and 179 deletions

View File

@@ -19,6 +19,18 @@ package io.element.android.appconfig
object AuthenticationConfig {
const val MATRIX_ORG_URL = "https://matrix.org"
/**
* Default homeserver url to sign in with, unless the user selects a different one.
*/
const val DEFAULT_HOMESERVER_URL = MATRIX_ORG_URL
/**
* URL with some docs that explain what's sliding sync and how to add it to your home server.
*/
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
/**
* Force a sliding sync proxy url, if not null, the proxy url in the .well-known file will be ignored.
*/
val SLIDING_SYNC_PROXY_URL: String? = null
}

1
changelog.d/3068.misc Normal file
View File

@@ -0,0 +1 @@
Updated Rust SDK to `v0.2.28`. Fixed incompatibilities.

View File

@@ -621,16 +621,8 @@ class MessageComposerPresenter @Inject constructor(
) = launch {
messageComposerContext.composerMode = composerMode
when (composerMode) {
is MessageComposerMode.Reply -> {
timelineController.invokeOnCurrentTimeline {
enterSpecialMode(composerMode.eventId)
}
}
is MessageComposerMode.Edit -> {
setText(composerMode.content, markdownTextEditorState, richTextEditorState)
timelineController.invokeOnCurrentTimeline {
enterSpecialMode(composerMode.eventId)
}
}
else -> Unit
}

View File

@@ -161,7 +161,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.27"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.28"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View File

@@ -17,10 +17,7 @@
package io.element.android.libraries.matrix.api.auth
sealed class AuthenticationException(message: String) : Exception(message) {
class ClientMissing(message: String) : AuthenticationException(message)
class InvalidServerName(message: String) : AuthenticationException(message)
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
class SessionMissing(message: String) : AuthenticationException(message)
class Generic(message: String) : AuthenticationException(message)
data class OidcError(val type: String, override val message: String) : AuthenticationException(message)
}

View File

@@ -56,8 +56,6 @@ interface Timeline : AutoCloseable {
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
suspend fun replyMessage(
eventId: EventId,
body: String,

View File

@@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
@@ -46,11 +47,9 @@ class RustMatrixClientFactory @Inject constructor(
private val utdTracker: UtdTracker,
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder(sessionData.sessionPath)
.homeserverUrl(sessionData.homeserverUrl)
val client = getBaseClientBuilder(sessionData.sessionPath, sessionData.passphrase)
.serverNameOrHomeserverUrl(sessionData.homeserverUrl)
.username(sessionData.userId)
.passphrase(sessionData.passphrase)
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
.use { it.build() }
client.restoreSession(sessionData.toSession())
@@ -71,21 +70,20 @@ class RustMatrixClientFactory @Inject constructor(
)
}
internal fun getBaseClientBuilder(sessionPath: String): ClientBuilder {
internal fun getBaseClientBuilder(sessionPath: String, passphrase: String?): ClientBuilder {
return ClientBuilder()
.sessionPath(sessionPath)
.passphrase(passphrase)
.slidingSyncProxy(AuthenticationConfig.SLIDING_SYNC_PROXY_URL)
.userAgent(userAgentProvider.provide())
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true)
.autoEnableCrossSigning(true)
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
.let {
// Sadly ClientBuilder.proxy() does not accept null :/
// Tracked by https://github.com/matrix-org/matrix-rust-sdk/issues/3159
val proxy = proxyProvider.provides()
if (proxy != null) {
it.proxy(proxy)
} else {
it
}
.run {
// Workaround for non-nullable proxy parameter in the SDK, since each call to the ClientBuilder returns a new reference we need to keep
proxyProvider.provides()?.let { proxy(it) } ?: this
}
}
}

View File

@@ -17,22 +17,14 @@
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException as RustAuthenticationException
fun Throwable.mapAuthenticationException(): AuthenticationException {
val message = this.message ?: "Unknown error"
return when (this) {
is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(message)
is RustAuthenticationException.Generic -> AuthenticationException.Generic(message)
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(message)
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(message)
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(message)
is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message)
is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message)
is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message)
is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message)
is RustAuthenticationException.OidcCancelled -> AuthenticationException.OidcError("OidcCancelled", message)
is RustAuthenticationException.OidcCallbackUrlInvalid -> AuthenticationException.OidcError("OidcCallbackUrlInvalid", message)
else -> AuthenticationException.Generic(message)
}
}

View File

@@ -32,12 +32,9 @@ import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionStore
@@ -46,17 +43,17 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.HumanQrLoginException
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
import org.matrix.rustcomponents.sdk.QrLoginProgress
import org.matrix.rustcomponents.sdk.QrLoginProgressListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import uniffi.matrix_sdk.OidcAuthorizationData
import java.io.File
import java.util.UUID
import javax.inject.Inject
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
@@ -64,28 +61,15 @@ class RustMatrixAuthenticationService @Inject constructor(
baseDirectory: File,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
userAgentProvider: UserAgentProvider,
private val rustMatrixClientFactory: RustMatrixClientFactory,
private val passphraseGenerator: PassphraseGenerator,
userCertificatesProvider: UserCertificatesProvider,
proxyProvider: ProxyProvider,
private val oidcConfigurationProvider: OidcConfigurationProvider,
) : MatrixAuthenticationService {
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
private val pendingPassphrase = getDatabasePassphrase()
private val sessionPath = File(baseDirectory, UUID.randomUUID().toString()).absolutePath
private val authService: RustAuthenticationService = RustAuthenticationService(
sessionPath = sessionPath,
passphrase = pendingPassphrase,
proxy = proxyProvider.provides(),
userAgent = userAgentProvider.provide(),
additionalRootCertificates = userCertificatesProvider.provides(),
oidcConfiguration = oidcConfigurationProvider.get(),
customSlidingSyncProxy = null,
sessionDelegate = null,
crossProcessRefreshLockId = null,
)
private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
override fun loggedInStateFlow(): Flow<LoggedInState> {
@@ -132,11 +116,14 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
withContext(coroutineDispatchers.io) {
runCatching {
authService.configureHomeserver(homeserver)
val homeServerDetails = authService.homeserverDetails()?.map()
if (homeServerDetails != null) {
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
}
val client = getBaseClientBuilder()
.serverNameOrHomeserverUrl(homeserver)
.build()
currentClient = client
val homeServerDetails = client.homeserverLoginDetails().map()
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
}.onFailure {
clear()
}.mapFailure { failure ->
failure.mapAuthenticationException()
}
@@ -145,15 +132,16 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun login(username: String, password: String): Result<SessionId> =
withContext(coroutineDispatchers.io) {
runCatching {
val client = authService.login(username, password, "Element X Android", null)
val sessionData = client.use {
it.session().toSessionData(
val client = currentClient ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
}
clear()
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}.mapFailure { failure ->
@@ -161,14 +149,15 @@ class RustMatrixAuthenticationService @Inject constructor(
}
}
private var pendingOidcAuthenticationData: OidcAuthenticationData? = null
private var pendingOidcAuthorizationData: OidcAuthorizationData? = null
override suspend fun getOidcUrl(): Result<OidcDetails> {
return withContext(coroutineDispatchers.io) {
runCatching {
val oidcAuthenticationData = authService.urlForOidcLogin()
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oidcAuthenticationData = client.urlForOidcLogin(oidcConfigurationProvider.get())
val url = oidcAuthenticationData.loginUrl()
pendingOidcAuthenticationData = oidcAuthenticationData
pendingOidcAuthorizationData = oidcAuthenticationData
OidcDetails(url)
}.mapFailure { failure ->
failure.mapAuthenticationException()
@@ -179,8 +168,8 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun cancelOidcLogin(): Result<Unit> {
return withContext(coroutineDispatchers.io) {
runCatching {
pendingOidcAuthenticationData?.close()
pendingOidcAuthenticationData = null
pendingOidcAuthorizationData?.close()
pendingOidcAuthorizationData = null
}.mapFailure { failure ->
failure.mapAuthenticationException()
}
@@ -193,18 +182,18 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithOidc(callbackUrl: String): Result<SessionId> {
return withContext(coroutineDispatchers.io) {
runCatching {
val urlForOidcLogin = pendingOidcAuthenticationData ?: error("You need to call `getOidcUrl()` first")
val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.use {
it.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
}
pendingOidcAuthenticationData?.close()
pendingOidcAuthenticationData = null
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first")
client.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = sessionPath,
)
clear()
pendingOidcAuthorizationData?.close()
pendingOidcAuthorizationData = null
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
}.mapFailure { failure ->
@@ -216,8 +205,7 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(sessionPath)
.passphrase(pendingPassphrase)
val client = rustMatrixClientFactory.getBaseClientBuilder(sessionPath, pendingPassphrase)
.buildWithQrCode(
qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData,
oidcConfiguration = oidcConfigurationProvider.get(),
@@ -252,4 +240,13 @@ class RustMatrixAuthenticationService @Inject constructor(
Timber.e(throwable, "Failed to login with QR code")
}
}
private fun getBaseClientBuilder() = rustMatrixClientFactory
.getBaseClientBuilder(sessionPath, pendingPassphrase)
.requiresSlidingSync()
private fun clear() {
currentClient?.close()
currentClient = null
}
}

View File

@@ -26,12 +26,13 @@ internal fun Session.toSessionData(
loginType: LoginType,
passphrase: String?,
sessionPath: String,
homeserverUrl: String? = null,
) = SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
homeserverUrl = homeserverUrl ?: this.homeserverUrl,
oidcData = oidcData,
slidingSyncProxy = slidingSyncProxy,
loginTimestamp = Date(),

View File

@@ -68,7 +68,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.EventTimelineItem
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
@@ -252,7 +251,6 @@ class RustTimeline(
override fun close() {
inner.close()
specialModeEventTimelineItem?.destroy()
}
private suspend fun fetchMembers() = withContext(dispatcher) {
@@ -329,14 +327,10 @@ class RustTimeline(
runCatching<Unit> {
when {
originalEventId != null -> {
val editedEvent = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(originalEventId.value)
editedEvent.use {
inner.edit(
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
editItem = it,
)
}
specialModeEventTimelineItem = null
inner.edit(
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
eventId = originalEventId.value,
)
}
transactionId != null -> {
error("Editing local echo is not supported yet.")
@@ -348,18 +342,6 @@ class RustTimeline(
}
}
private var specialModeEventTimelineItem: EventTimelineItem? = null
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = withContext(dispatcher) {
runCatching {
specialModeEventTimelineItem?.destroy()
specialModeEventTimelineItem = null
specialModeEventTimelineItem = eventId?.let { inner.getEventTimelineItemByEventId(it.value) }
}.onFailure {
Timber.e(it, "Unable to retrieve event for special mode. Are you using the correct timeline?")
}
}
override suspend fun replyMessage(
eventId: EventId,
body: String,
@@ -369,19 +351,7 @@ class RustTimeline(
): Result<Unit> = withContext(dispatcher) {
runCatching {
val msg = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map())
if (fromNotification) {
// When replying from a notification, do not interfere with `specialModeEventTimelineItem`
val inReplyTo = inner.getEventTimelineItemByEventId(eventId.value)
inReplyTo.use { eventTimelineItem ->
inner.sendReply(msg, eventTimelineItem)
}
} else {
val inReplyTo = specialModeEventTimelineItem ?: inner.getEventTimelineItemByEventId(eventId.value)
inReplyTo.use { eventTimelineItem ->
inner.sendReply(msg, eventTimelineItem)
}
specialModeEventTimelineItem = null
}
inner.sendReply(msg, eventId.value)
}
}

View File

@@ -20,7 +20,7 @@ import com.google.common.truth.ThrowableSubject
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.junit.Test
import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException
class AuthenticationExceptionMappingTest {
@Test
@@ -39,64 +39,21 @@ class AuthenticationExceptionMappingTest {
@Test
fun `mapping specific exceptions map to their kotlin counterparts`() {
assertThat(RustAuthenticationException.ClientMissing("Client missing").mapAuthenticationException())
.isException<AuthenticationException.ClientMissing>("Client missing")
assertThat(ClientBuildException.Generic("Unknown error").mapAuthenticationException())
.isException<AuthenticationException.Generic>("Unknown error")
assertThat(RustAuthenticationException.Generic("Generic").mapAuthenticationException()).isException<AuthenticationException.Generic>("Generic")
assertThat(RustAuthenticationException.InvalidServerName("Invalid server name").mapAuthenticationException())
assertThat(ClientBuildException.InvalidServerName("Invalid server name").mapAuthenticationException())
.isException<AuthenticationException.InvalidServerName>("Invalid server name")
assertThat(RustAuthenticationException.SessionMissing("Session missing").mapAuthenticationException())
.isException<AuthenticationException.SessionMissing>("Session missing")
assertThat(ClientBuildException.Sdk("SDK issue").mapAuthenticationException())
.isException<AuthenticationException.Generic>("SDK issue")
assertThat(RustAuthenticationException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
assertThat(ClientBuildException.SlidingSyncNotAvailable("Sliding sync not available").mapAuthenticationException())
.isException<AuthenticationException.SlidingSyncNotAvailable>("Sliding sync not available")
}
@Test
fun `mapping Oidc related exceptions creates an 'OidcError' with different types`() {
assertIsOidcError(
throwable = RustAuthenticationException.OidcException("Oidc exception"),
type = "OidcException",
message = "Oidc exception"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcMetadataInvalid("Oidc metadata invalid"),
type = "OidcMetadataInvalid",
message = "Oidc metadata invalid"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcMetadataMissing("Oidc metadata missing"),
type = "OidcMetadataMissing",
message = "Oidc metadata missing"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcNotSupported("Oidc not supported"),
type = "OidcNotSupported",
message = "Oidc not supported"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcCancelled("Oidc cancelled"),
type = "OidcCancelled",
message = "Oidc cancelled"
)
assertIsOidcError(
throwable = RustAuthenticationException.OidcCallbackUrlInvalid("Oidc callback url invalid"),
type = "OidcCallbackUrlInvalid",
message = "Oidc callback url invalid"
)
}
private inline fun <reified T> ThrowableSubject.isException(message: String) {
isInstanceOf(T::class.java)
hasMessageThat().isEqualTo(message)
}
private fun assertIsOidcError(throwable: Throwable, type: String, message: String) {
val authenticationException = throwable.mapAuthenticationException()
assertThat(authenticationException).isInstanceOf(AuthenticationException.OidcError::class.java)
assertThat((authenticationException as? AuthenticationException.OidcError)?.type).isEqualTo(type)
assertThat(authenticationException.message).isEqualTo(message)
}
}

View File

@@ -104,12 +104,6 @@ class FakeTimeline(
mentions
)
var enterSpecialModeLambda: (eventId: EventId?) -> Result<Unit> = {
Result.success(Unit)
}
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = enterSpecialModeLambda(eventId)
var replyMessageLambda: (
eventId: EventId,
body: String,

View File

@@ -51,7 +51,6 @@ class MainActivity : ComponentActivity() {
baseDirectory = baseDirectory,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
userAgentProvider = userAgentProvider,
rustMatrixClientFactory = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = applicationContext.cacheDir,
@@ -65,8 +64,6 @@ class MainActivity : ComponentActivity() {
utdTracker = UtdTracker(NoopAnalyticsService()),
),
passphraseGenerator = NullPassphraseGenerator(),
userCertificatesProvider = userCertificatesProvider,
proxyProvider = proxyProvider,
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
)
}