Merge pull request #6127 from vector-im/feature/fre/start_dm_on_first_msg_impl

Create the DM when sending an event
This commit is contained in:
Florian Renaud 2022-08-26 09:15:34 +02:00 committed by GitHub
commit e43bc88a4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1387 additions and 136 deletions

1
changelog.d/5525.wip Normal file
View File

@ -0,0 +1 @@
Create DM room only on first message - Create the DM and navigate to the new room after sending an event

View File

@ -104,6 +104,7 @@ ext.libs = [
'moshi' : "com.squareup.moshi:moshi:$moshi",
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
'moshiKotlin' : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
'moshiAdapters' : "com.squareup.moshi:moshi-adapters:$moshi",
'retrofit' : "com.squareup.retrofit2:retrofit:$retrofit",
'retrofitMoshi' : "com.squareup.retrofit2:converter-moshi:$retrofit"
],

View File

@ -163,6 +163,7 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor'
implementation libs.squareup.moshi
implementation libs.squareup.moshiAdapters
kapt libs.squareup.moshiKotlin
api "com.atlassian.commonmark:commonmark:0.13.0"

View File

@ -70,6 +70,9 @@ object EventType {
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
// This type is for local purposes, it should never be processed by the server
const val LOCAL_STATE_ROOM_THIRD_PARTY_INVITE = "local.room.third_party_invite"
// Call Events
const val CALL_INVITE = "m.call.invite"
const val CALL_CANDIDATES = "m.call.candidates"

View File

@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.identity
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier
sealed class ThreePid(open val value: String) {
@JsonClass(generateAdapter = true)
data class Email(val email: String) : ThreePid(email)
@JsonClass(generateAdapter = true)
data class Msisdn(val msisdn: String) : ThreePid(msisdn)
}

View File

@ -17,13 +17,16 @@
package org.matrix.android.sdk.api.session.room.model.create
import android.net.Uri
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.internal.di.MoshiProvider
@JsonClass(generateAdapter = true)
open class CreateRoomParams {
/**
* A public visibility indicates that the room will be shown in the published room list.
@ -61,12 +64,12 @@ open class CreateRoomParams {
* A list of user IDs to invite to the room.
* This will tell the server to invite everyone in the list to the newly created room.
*/
val invitedUserIds = mutableListOf<String>()
var invitedUserIds = mutableListOf<String>()
/**
* A list of objects representing third party IDs to invite into the room.
*/
val invite3pids = mutableListOf<ThreePid>()
var invite3pids = mutableListOf<ThreePid>()
/**
* Initial Guest Access.
@ -99,14 +102,14 @@ open class CreateRoomParams {
* The server will clobber the following keys: creator.
* Future versions of the specification may allow the server to clobber other keys.
*/
val creationContent = mutableMapOf<String, Any>()
var creationContent = mutableMapOf<String, Any>()
/**
* A list of state events to set in the new room. This allows the user to override the default state events
* set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
* Takes precedence over events set by preset, but gets overridden by name and topic keys.
*/
val initialStates = mutableListOf<CreateRoomStateEvent>()
var initialStates = mutableListOf<CreateRoomStateEvent>()
/**
* Set to true to disable federation of this room.
@ -151,7 +154,7 @@ open class CreateRoomParams {
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM.
*/
var algorithm: String? = null
private set
internal set
var historyVisibility: RoomHistoryVisibility? = null
@ -161,10 +164,18 @@ open class CreateRoomParams {
var roomVersion: String? = null
var featurePreset: RoomFeaturePreset? = null
@Transient var featurePreset: RoomFeaturePreset? = null
companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
internal const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
internal const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
fun fromJson(json: String?): CreateRoomParams? {
return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).fromJson(it) }
}
}
}
internal fun CreateRoomParams.toJSONString(): String {
return MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).toJson(this)
}

View File

@ -16,8 +16,10 @@
package org.matrix.android.sdk.api.session.room.model.create
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content
@JsonClass(generateAdapter = true)
data class CreateRoomStateEvent(
/**
* Required. The type of event to send.

View File

@ -16,10 +16,12 @@
package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task
@ -37,12 +39,17 @@ internal class DefaultSendEventTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: EncryptEventTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver
) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String {
try {
if (params.event.isLocalRoomEvent) {
return createRoomAndSendEvent(params)
}
// Make sure to load all members in the room before sending the event.
params.event.roomId
?.takeIf { params.encrypt }
@ -78,6 +85,12 @@ internal class DefaultSendEventTask @Inject constructor(
}
}
private suspend fun createRoomAndSendEvent(params: SendEventTask.Params): String {
val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.event.roomId.orEmpty()))
Timber.d("State event: convert local room (${params.event.roomId}) to existing room ($roomId) before sending the event.")
return execute(params.copy(event = params.event.copy(roomId = roomId)))
}
@Throws
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
if (params.encrypt && !params.event.isEncrypted()) {
@ -91,4 +104,7 @@ internal class DefaultSendEventTask @Inject constructor(
}
return params.event
}
private val Event.isLocalRoomEvent
get() = RoomLocalEcho.isLocalEchoId(roomId.orEmpty())
}

View File

@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 35L,
schemaVersion = 36L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -105,5 +106,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
if (oldVersion < 34) MigrateSessionTo034(realm).perform()
if (oldVersion < 35) MigrateSessionTo035(realm).perform()
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 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.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo036(realm: DynamicRealm) : RealmMigrator(realm, 36) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.create("LocalRoomSummaryEntity")
.addField(LocalRoomSummaryEntityFields.ROOM_ID, String::class.java)
.addPrimaryKey(LocalRoomSummaryEntityFields.ROOM_ID)
.setRequired(LocalRoomSummaryEntityFields.ROOM_ID, true)
.addField(LocalRoomSummaryEntityFields.CREATE_ROOM_PARAMS_STR, String::class.java)
.addRealmObjectField(LocalRoomSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2022 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.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.toJSONString
internal open class LocalRoomSummaryEntity(
@PrimaryKey var roomId: String = "",
var roomSummaryEntity: RoomSummaryEntity? = null,
private var createRoomParamsStr: String? = null
) : RealmObject() {
var createRoomParams: CreateRoomParams?
get() {
return CreateRoomParams.fromJson(createRoomParamsStr)
}
set(value) {
createRoomParamsStr = value?.toJSONString()
}
companion object
}

View File

@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
ReadReceiptEntity::class,
RoomEntity::class,
RoomSummaryEntity::class,
LocalRoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
PendingThreePidEntity::class,

View File

@ -0,0 +1,31 @@
/*
* Copyright 2022 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.database.query
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> {
val query = realm.where<LocalRoomSummaryEntity>()
if (roomId != null) {
query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
}
return query
}

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent
@ -60,6 +62,12 @@ internal object MoshiProvider {
.registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE)
)
.add(SerializeNulls.JSON_ADAPTER_FACTORY)
.add(
PolymorphicJsonAdapterFactory.of(ThreePid::class.java, "type")
.withSubtype(ThreePid.Email::class.java, "email")
.withSubtype(ThreePid.Msisdn::class.java, "msisdn")
.withDefaultValue(null)
)
.build()
fun providesMoshi(): Moshi {

View File

@ -43,9 +43,13 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAli
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask
import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomStateEventsTask
import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomStateEventsTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask
import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
@ -213,6 +217,12 @@ internal abstract class RoomModule {
@Binds
abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
@Binds
abstract fun bindCreateLocalRoomStateEventsTask(task: DefaultCreateLocalRoomStateEventsTask): CreateLocalRoomStateEventsTask
@Binds
abstract fun bindCreateRoomFromLocalRoomTask(task: DefaultCreateRoomFromLocalRoomTask): CreateRoomFromLocalRoomTask
@Binds
abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask

View File

@ -0,0 +1,299 @@
/*
* Copyright 2022 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.room.create
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.banOrDefault
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
import org.matrix.android.sdk.api.session.room.model.kickOrDefault
import org.matrix.android.sdk.api.session.room.model.redactOrDefault
import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomStateEventsTask.Params
import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
/**
* Generate a list of local state events from the given [CreateRoomBody].
* The states events are generated according to the given configuration and following the matrix specification.
* This list reflects as much as possible a list of state events related to a real room configured and got from the server.
*
* Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
*/
internal interface CreateLocalRoomStateEventsTask : Task<Params, List<Event>> {
data class Params(val createRoomBody: CreateRoomBody)
}
internal class DefaultCreateLocalRoomStateEventsTask @Inject constructor(
@UserId private val myUserId: String,
private val userService: UserService,
private val clock: Clock,
) : CreateLocalRoomStateEventsTask {
private lateinit var createRoomBody: CreateRoomBody
override suspend fun execute(params: Params): List<Event> {
createRoomBody = params.createRoomBody
// Build the list of the state events following the priorities from the matrix specification
// Changing the order of the events might break the correct display of the room on the client side
return buildList {
createRoomCreateEvent()
createRoomMemberEvents(listOf(myUserId))
createRoomPowerLevelsEvent()
createRoomAliasEvent()
createRoomPresetEvents()
createRoomInitialStateEvents()
createRoomNameAndTopicStateEvents()
createRoomMemberEvents(createRoomBody.invitedUserIds.orEmpty())
createRoomThreePidEvents()
createRoomDefaultEvents()
}
}
/**
* Generate the create state event related to this room.
*/
private fun MutableList<Event>.createRoomCreateEvent() {
val roomCreateEvent = createLocalStateEvent(
type = EventType.STATE_ROOM_CREATE,
content = RoomCreateContent(
creator = myUserId,
roomVersion = createRoomBody.roomVersion,
type = (createRoomBody.creationContent as? Map<*, *>)?.get(CreateRoomParams.CREATION_CONTENT_KEY_ROOM_TYPE) as? String
).toContent(),
)
add(roomCreateEvent)
}
/**
* Generate the create state event related to the power levels using the given overridden values or the default values according to the specification.
* Ref: https://spec.matrix.org/latest/client-server-api/#mroompower_levels
*/
private fun MutableList<Event>.createRoomPowerLevelsEvent() {
val powerLevelsContent = createLocalStateEvent(
type = EventType.STATE_ROOM_POWER_LEVELS,
content = (createRoomBody.powerLevelContentOverride ?: PowerLevelsContent()).let {
it.copy(
ban = it.banOrDefault(),
eventsDefault = it.eventsDefaultOrDefault(),
invite = it.inviteOrDefault(),
kick = it.kickOrDefault(),
redact = it.redactOrDefault(),
stateDefault = it.stateDefaultOrDefault(),
usersDefault = it.usersDefaultOrDefault(),
)
}.toContent(),
)
add(powerLevelsContent)
}
/**
* Generate the local room member state events related to the given user ids, if any.
*/
private suspend fun MutableList<Event>.createRoomMemberEvents(userIds: List<String>) {
val memberEvents = userIds
.mapNotNull { tryOrNull { userService.resolveUser(it) } }
.map { user ->
createLocalStateEvent(
type = EventType.STATE_ROOM_MEMBER,
content = RoomMemberContent(
isDirect = createRoomBody.isDirect.takeUnless { user.userId == myUserId }.orFalse(),
membership = if (user.userId == myUserId) Membership.JOIN else Membership.INVITE,
displayName = user.displayName,
avatarUrl = user.avatarUrl
).toContent(),
stateKey = user.userId
)
}
addAll(memberEvents)
}
/**
* Generate the local state events related to the given third party invites, if any.
*/
private fun MutableList<Event>.createRoomThreePidEvents() {
createRoomBody.invite3pids.orEmpty().forEach { body ->
val localThirdPartyInviteEvent = createLocalStateEvent(
type = EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
content = LocalRoomThirdPartyInviteContent(
isDirect = createRoomBody.isDirect.orFalse(),
membership = Membership.INVITE,
displayName = body.address,
thirdPartyInvite = body.toThreePid()
).toContent(),
)
val thirdPartyInviteEvent = createLocalStateEvent(
type = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
content = RoomThirdPartyInviteContent(
displayName = body.address,
keyValidityUrl = null,
publicKey = null,
publicKeys = null
).toContent(),
)
add(localThirdPartyInviteEvent)
add(thirdPartyInviteEvent)
}
}
/**
* Generate the local state event related to the given alias, if any.
*/
fun MutableList<Event>.createRoomAliasEvent() {
if (createRoomBody.roomAliasName != null) {
val canonicalAliasContent = createLocalStateEvent(
type = EventType.STATE_ROOM_CANONICAL_ALIAS,
content = RoomCanonicalAliasContent(
canonicalAlias = "${createRoomBody.roomAliasName}:${myUserId.getServerName()}"
).toContent(),
)
add(canonicalAliasContent)
}
}
/**
* Generate the local state events related to the given [CreateRoomPreset].
* Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
*/
private fun MutableList<Event>.createRoomPresetEvents() {
val preset = createRoomBody.preset ?: return
var joinRules: RoomJoinRules? = null
var historyVisibility: RoomHistoryVisibility? = null
var guestAccess: GuestAccess? = null
when (preset) {
CreateRoomPreset.PRESET_PRIVATE_CHAT,
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> {
joinRules = RoomJoinRules.INVITE
historyVisibility = RoomHistoryVisibility.SHARED
guestAccess = GuestAccess.CanJoin
}
CreateRoomPreset.PRESET_PUBLIC_CHAT -> {
joinRules = RoomJoinRules.PUBLIC
historyVisibility = RoomHistoryVisibility.SHARED
guestAccess = GuestAccess.Forbidden
}
}
add(createLocalStateEvent(EventType.STATE_ROOM_JOIN_RULES, RoomJoinRulesContent(joinRules.value).toContent()))
add(createLocalStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, RoomHistoryVisibilityContent(historyVisibility.value).toContent()))
add(createLocalStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, RoomGuestAccessContent(guestAccess.value).toContent()))
}
/**
* Generate the local state events related to the given initial states, if any.
* The given initial state events override the potential existing ones of the same type.
*/
private fun MutableList<Event>.createRoomInitialStateEvents() {
val initialStates = createRoomBody.initialStates ?: return
val initialStateEvents = initialStates.map { createLocalStateEvent(it.type, it.content, it.stateKey) }
// Erase existing events of the same type
removeAll { event -> event.type in initialStateEvents.map { it.type } }
// Add the initial state events to the list
addAll(initialStateEvents)
}
/**
* Generate the local events related to the given room name and topic, if any.
*/
private fun MutableList<Event>.createRoomNameAndTopicStateEvents() {
if (createRoomBody.name != null) {
add(createLocalStateEvent(EventType.STATE_ROOM_NAME, RoomNameContent(createRoomBody.name).toContent()))
}
if (createRoomBody.topic != null) {
add(createLocalStateEvent(EventType.STATE_ROOM_TOPIC, RoomTopicContent(createRoomBody.topic).toContent()))
}
}
/**
* Generate the local events which have not been set and are in that case provided by the server with default values.
* Default events:
* - m.room.history_visibility (https://spec.matrix.org/latest/client-server-api/#server-behaviour-5)
* - m.room.guest_access (https://spec.matrix.org/latest/client-server-api/#mroomguest_access)
*/
private fun MutableList<Event>.createRoomDefaultEvents() {
// HistoryVisibility
if (none { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }) {
add(
createLocalStateEvent(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
content = RoomHistoryVisibilityContent(RoomHistoryVisibility.SHARED.value).toContent(),
)
)
}
// GuestAccess
if (none { it.type == EventType.STATE_ROOM_GUEST_ACCESS }) {
add(
createLocalStateEvent(
type = EventType.STATE_ROOM_GUEST_ACCESS,
content = RoomGuestAccessContent(GuestAccess.Forbidden.value).toContent(),
)
)
}
}
/**
* Generate a local state event from the given parameters.
*
* @param type the event type, see [EventType]
* @param content the content of the event
* @param stateKey the stateKey, if any
*
* @return a local state event
*/
private fun createLocalStateEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
return Event(
type = type,
senderId = myUserId,
stateKey = stateKey,
content = content,
originServerTs = clock.epochMillis(),
eventId = LocalEcho.createLocalEchoId()
)
}
}

View File

@ -21,26 +21,15 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.createObject
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.mapper.asDomain
@ -48,6 +37,7 @@ import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -56,7 +46,6 @@ import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
@ -70,22 +59,22 @@ import javax.inject.Inject
internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
internal class DefaultCreateLocalRoomTask @Inject constructor(
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
@SessionDatabase private val realmConfiguration: RealmConfiguration,
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
private val userService: UserService,
private val cryptoService: DefaultCryptoService,
private val clock: Clock,
private val createLocalRoomStateEventsTask: CreateLocalRoomStateEventsTask,
) : CreateLocalRoomTask {
override suspend fun execute(params: CreateRoomParams): String {
val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
val createRoomBody = createRoomBodyBuilder.build(params)
val roomId = RoomLocalEcho.createLocalEchoId()
monarchy.awaitTransaction { realm ->
createLocalRoomEntity(realm, roomId, createRoomBody)
createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
createLocalRoomSummaryEntity(realm, roomId, params, createRoomBody)
}
// Wait for room to be created in DB
@ -114,14 +103,29 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
}
}
private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomParams: CreateRoomParams, createRoomBody: CreateRoomBody) {
// Create the room summary entity
val roomSummaryEntity = realm.createObject<RoomSummaryEntity>(roomId).apply {
val otherUserId = createRoomBody.getDirectUserId()
if (otherUserId != null) {
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
isDirect = true
directUserId = otherUserId
}
}
// Update the createRoomParams from the potential feature preset before saving
createRoomParams.featurePreset?.let { featurePreset ->
featurePreset.updateRoomParams(createRoomParams)
createRoomParams.initialStates.addAll(featurePreset.setupInitialStates().orEmpty())
}
// Create a LocalRoomSummaryEntity decorated by the related RoomSummaryEntity and the updated CreateRoomParams
realm.createObject<LocalRoomSummaryEntity>(roomId).also {
it.roomSummaryEntity = roomSummaryEntity
it.createRoomParams = createRoomParams
}
// Update the RoomSummaryEntity by simulating a fake sync response
roomSummaryUpdater.update(
realm = realm,
roomId = roomId,
@ -150,7 +154,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
isLastForward = true
}
val eventList = createLocalRoomEvents(createRoomBody)
val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (event in eventList) {
@ -169,6 +173,9 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
roomMemberEventHandler.handle(realm, roomId, event, false)
}
// Give info to crypto module
cryptoService.onStateEvent(roomId, event)
}
roomMemberContentsByUser.getOrPut(event.senderId) {
@ -187,81 +194,4 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
return chunkEntity
}
/**
* Build the list of the events related to the room creation params.
*
* @param createRoomBody the room creation params
*
* @return the list of events
*/
private suspend fun createLocalRoomEvents(createRoomBody: CreateRoomBody): List<Event> {
val myUser = userService.getUser(userId) ?: User(userId)
val invitedUsers = createRoomBody.invitedUserIds.orEmpty()
.mapNotNull { tryOrNull { userService.resolveUser(it) } }
val createRoomEvent = createLocalEvent(
type = EventType.STATE_ROOM_CREATE,
content = RoomCreateContent(
creator = userId
).toContent()
)
val myRoomMemberEvent = createLocalEvent(
type = EventType.STATE_ROOM_MEMBER,
content = RoomMemberContent(
membership = Membership.JOIN,
displayName = myUser.displayName,
avatarUrl = myUser.avatarUrl
).toContent(),
stateKey = userId
)
val roomMemberEvents = invitedUsers.map {
createLocalEvent(
type = EventType.STATE_ROOM_MEMBER,
content = RoomMemberContent(
isDirect = createRoomBody.isDirect.orFalse(),
membership = Membership.INVITE,
displayName = it.displayName,
avatarUrl = it.avatarUrl
).toContent(),
stateKey = it.userId
)
}
return buildList {
add(createRoomEvent)
add(myRoomMemberEvent)
addAll(createRoomBody.initialStates.orEmpty().map { createLocalEvent(it.type, it.content, it.stateKey) })
addAll(roomMemberEvents)
}
}
/**
* Generate a local event from the given parameters.
*
* @param type the event type, see [EventType]
* @param content the content of the Event
* @param stateKey the stateKey, if any
*
* @return a fake event
*/
private fun createLocalEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
return Event(
type = type,
senderId = userId,
stateKey = stateKey,
content = content,
originServerTs = clock.epochMillis(),
eventId = LocalEcho.createLocalEchoId()
)
}
/**
* Setup default values to the CreateRoomParams as the room is created locally (the default values will not be defined by the server).
*/
private fun CreateRoomParams.withDefault() = this.apply {
if (visibility == null) visibility = RoomDirectoryVisibility.PRIVATE
if (historyVisibility == null) historyVisibility = RoomHistoryVisibility.SHARED
if (guestAccess == null) guestAccess = GuestAccess.Forbidden
}
}

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
/**
@ -119,7 +120,13 @@ internal data class CreateRoomBody(
*/
@Json(name = "room_version")
val roomVersion: String?
)
) {
companion object {
fun fromJson(json: String?): CreateRoomBody? {
return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomBody::class.java).fromJson(it) }
}
}
}
/**
* Tells if the created room can be a direct chat one.

View File

@ -0,0 +1,149 @@
/*
* Copyright 2022 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.room.create
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
* Create a room on the server from a local room.
* The configuration of the local room will be use to configure the new room.
* The potential local room members will also be invited to this new room.
*
* A local tombstone event will be created to indicate that the local room has been replacing by the new one.
*/
internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> {
data class Params(val localRoomId: String)
}
internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor(
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val stateEventDataSource: StateEventDataSource,
private val clock: Clock,
) : CreateRoomFromLocalRoomTask {
private val realmConfiguration
get() = monarchy.realmConfiguration
override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
?.content.toModel<RoomTombstoneContent>()
?.replacementRoomId
if (replacementRoomId != null) {
return replacementRoomId
}
var createRoomParams: CreateRoomParams? = null
var isEncrypted = false
monarchy.doWithRealm { realm ->
realm.where<LocalRoomSummaryEntity>()
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId)
.findFirst()
?.let {
createRoomParams = it.createRoomParams
isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse()
}
}
val roomId = createRoomTask.execute(createRoomParams!!)
try {
// Wait for all the room events before triggering the replacement room
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
.equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0)
}
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY)
}
if (isEncrypted) {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
EventEntity.whereRoomId(realm, roomId)
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
}
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(roomId)
}
createTombstoneEvent(params, roomId)
return roomId
}
/**
* Create a Tombstone event to indicate that the local room has been replaced by a new one.
*/
private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) {
val now = clock.epochMillis()
val event = Event(
type = EventType.STATE_ROOM_TOMBSTONE,
senderId = userId,
originServerTs = now,
stateKey = "",
eventId = UUID.randomUUID().toString(),
content = RoomTombstoneContent(
replacementRoomId = roomId
).toContent()
)
monarchy.awaitTransaction { realm ->
val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
if (event.stateKey != null && event.type != null && event.eventId != null) {
CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity
}
}
}
}
}

View File

@ -54,8 +54,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
@SessionDatabase private val realmConfiguration: RealmConfiguration,
private val createRoomBodyBuilder: CreateRoomBodyBuilder,
private val globalErrorReceiver: GlobalErrorReceiver,
private val clock: Clock,
@ -71,7 +70,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
}
val createRoomBody = createRoomBodyBuilder.build(params)
val createRoomResponse = try {
executeRequest(globalErrorReceiver) {
roomAPI.createRoom(createRoomBody)
@ -90,6 +88,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
}
throw throwable
}
val roomId = createRoomResponse.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
try {

View File

@ -0,0 +1,34 @@
/*
* Copyright 2022 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.room.create
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.Membership
/**
* Class representing the EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE state event content
* This class is only used to store the third party invite data of a local room.
*/
@JsonClass(generateAdapter = true)
internal data class LocalRoomThirdPartyInviteContent(
@Json(name = "membership") val membership: Membership,
@Json(name = "displayname") val displayName: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: ThreePid? = null,
)

View File

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -70,6 +71,9 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
RoomEntity.where(realm, roomId = roomId).findAll()
?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
?.deleteAllFromRealm()
LocalRoomSummaryEntity.where(realm, roomId = roomId).findAll()
?.also { Timber.i("## DeleteLocalRoomTask - LocalRoomSummaryEntity - delete ${it.size} entries") }
?.deleteAllFromRealm()
}
} else {
Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")

View File

@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.membership.threepid
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.auth.data.ThreePidMedium
@JsonClass(generateAdapter = true)
internal data class ThreePidInviteBody(
@ -43,3 +45,9 @@ internal data class ThreePidInviteBody(
@Json(name = "address")
val address: String
)
internal fun ThreePidInviteBody.toThreePid() = when (medium) {
ThreePidMedium.EMAIL -> ThreePid.Email(address)
ThreePidMedium.MSISDN -> ThreePid.Msisdn(address)
else -> null
}

View File

@ -16,10 +16,12 @@
package org.matrix.android.sdk.internal.session.room.state
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
@ -35,11 +37,16 @@ internal interface SendStateTask : Task<SendStateTask.Params, String> {
internal class DefaultSendStateTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver
private val globalErrorReceiver: GlobalErrorReceiver,
private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
) : SendStateTask {
override suspend fun execute(params: SendStateTask.Params): String {
return executeRequest(globalErrorReceiver) {
if (RoomLocalEcho.isLocalEchoId(params.roomId)) {
// Room is local, so create a real one and send the event to this new room
createRoomAndSendEvent(params)
} else {
val response = if (params.stateKey.isEmpty()) {
roomAPI.sendStateEvent(
roomId = params.roomId,
@ -59,4 +66,11 @@ internal class DefaultSendStateTask @Inject constructor(
}
}
}
}
private suspend fun createRoomAndSendEvent(params: SendStateTask.Params): String {
val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.roomId))
Timber.d("State event: convert local room (${params.roomId}) to existing room ($roomId) before sending the event.")
return execute(params.copy(roomId = roomId))
}
}

View File

@ -0,0 +1,462 @@
/*
* Copyright (c) 2022 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.room.create
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldNotBeNull
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_EMAIL
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_MSISDN
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
import org.matrix.android.sdk.internal.util.time.DefaultClock
private const val MY_USER_ID = "my-user-id"
private const val MY_USER_DISPLAY_NAME = "my-user-display-name"
private const val MY_USER_AVATAR = "my-user-avatar"
@ExperimentalCoroutinesApi
internal class DefaultCreateLocalRoomStateEventsTaskTest {
private val clock = DefaultClock()
private val userService = mockk<UserService>()
private val defaultCreateLocalRoomStateEventsTask = DefaultCreateLocalRoomStateEventsTask(
myUserId = MY_USER_ID,
userService = userService,
clock = clock
)
lateinit var createRoomBody: CreateRoomBody
@Before
fun setup() {
createRoomBody = mockk {
every { roomVersion } returns null
every { creationContent } returns null
every { roomAliasName } returns null
every { topic } returns null
every { name } returns null
every { powerLevelContentOverride } returns null
every { initialStates } returns null
every { invite3pids } returns null
every { preset } returns null
every { isDirect } returns null
every { invitedUserIds } returns null
}
coEvery { userService.resolveUser(any()) } answers { User(firstArg()) }
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room create state event`() = runTest {
// Given
val aRoomVersion = "a_room_version"
every { createRoomBody.roomVersion } returns aRoomVersion
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val roomCreateEvent = result.find { it.type == EventType.STATE_ROOM_CREATE }
val roomCreateContent = roomCreateEvent?.content.toModel<RoomCreateContent>()
roomCreateContent?.creator shouldBeEqualTo MY_USER_ID
roomCreateContent?.roomVersion shouldBeEqualTo aRoomVersion
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct name and topic state events`() = runTest {
// Given
val aRoomName = "a_room_name"
val aRoomTopic = "a_room_topic"
every { createRoomBody.name } returns aRoomName
every { createRoomBody.topic } returns aRoomTopic
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val roomNameEvent = result.find { it.type == EventType.STATE_ROOM_NAME }
val roomTopicEvent = result.find { it.type == EventType.STATE_ROOM_TOPIC }
roomNameEvent?.content.toModel<RoomNameContent>()?.name shouldBeEqualTo aRoomName
roomTopicEvent?.content.toModel<RoomTopicContent>()?.topic shouldBeEqualTo aRoomTopic
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room member events`() = runTest {
// Given
data class RoomMember(val user: User, val membership: Membership)
val aRoomMemberList: List<RoomMember> = listOf(
RoomMember(User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR), Membership.JOIN),
RoomMember(User("userA_id", "userA_display_name", "userA_avatar"), Membership.INVITE),
RoomMember(User("userB_id", "userB_display_name", "userB_avatar"), Membership.INVITE)
)
every { createRoomBody.invitedUserIds } returns aRoomMemberList.filter { it.membership == Membership.INVITE }.map { it.user.userId }
coEvery { userService.resolveUser(any()) } answers {
aRoomMemberList.map { it.user }.find { it.userId == firstArg() } ?: User(firstArg())
}
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val roomMemberEvents = result.filter { it.type == EventType.STATE_ROOM_MEMBER }
roomMemberEvents.map { it.stateKey } shouldBeEqualTo aRoomMemberList.map { it.user.userId }
roomMemberEvents.forEach { event ->
val roomMemberContent = event.content.toModel<RoomMemberContent>()
val roomMember = aRoomMemberList.find { it.user.userId == event.stateKey }
roomMember.shouldNotBeNull()
roomMemberContent?.avatarUrl shouldBeEqualTo roomMember.user.avatarUrl
roomMemberContent?.displayName shouldBeEqualTo roomMember.user.displayName
roomMemberContent?.membership shouldBeEqualTo roomMember.membership
}
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct power levels event`() = runTest {
// Given
val aPowerLevelsContent = PowerLevelsContent(
ban = 1,
kick = 2,
invite = 3,
redact = 4,
eventsDefault = 5,
events = null,
usersDefault = 6,
users = null,
stateDefault = 7,
notifications = null
)
every { createRoomBody.powerLevelContentOverride } returns aPowerLevelsContent
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }
roomPowerLevelsEvent?.content.toModel<PowerLevelsContent>() shouldBeEqualTo aPowerLevelsContent
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct canonical alias event`() = runTest {
// Given
val aRoomAlias = "a_room_alias"
val expectedCanonicalAlias = "$aRoomAlias:${MY_USER_ID.getServerName()}"
every { createRoomBody.roomAliasName } returns aRoomAlias
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
roomPowerLevelsEvent?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias shouldBeEqualTo expectedCanonicalAlias
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct preset related events`() = runTest {
data class ExpectedResult(val joinRules: RoomJoinRules, val historyVisibility: RoomHistoryVisibility, val guestAccess: GuestAccess)
data class Case(val preset: CreateRoomPreset, val expectedResult: ExpectedResult)
CreateRoomPreset.values().forEach { aRoomPreset ->
// Given
val case = when (aRoomPreset) {
CreateRoomPreset.PRESET_PRIVATE_CHAT -> Case(
CreateRoomPreset.PRESET_PRIVATE_CHAT,
ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
)
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> Case(
CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
)
CreateRoomPreset.PRESET_PUBLIC_CHAT -> Case(
CreateRoomPreset.PRESET_PUBLIC_CHAT,
ExpectedResult(RoomJoinRules.PUBLIC, RoomHistoryVisibility.SHARED, GuestAccess.Forbidden)
)
}
every { createRoomBody.preset } returns case.preset
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
result.find { it.type == EventType.STATE_ROOM_JOIN_RULES }
?.content.toModel<RoomJoinRulesContent>()
?.joinRules shouldBeEqualTo case.expectedResult.joinRules
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
?.content.toModel<RoomHistoryVisibilityContent>()
?.historyVisibility shouldBeEqualTo case.expectedResult.historyVisibility
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
?.content.toModel<RoomGuestAccessContent>()
?.guestAccess shouldBeEqualTo case.expectedResult.guestAccess
}
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the initial state events`() = runTest {
// Given
val aListOfInitialStateEvents = listOf(
Event(
type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
content = EncryptionEventContent(MXCRYPTO_ALGORITHM_MEGOLM).toContent()
),
Event(
type = "a_custom_type",
content = mapOf("a_custom_map_to_integer" to 42),
stateKey = "a_state_key"
),
Event(
type = "another_custom_type",
content = mapOf("a_custom_map_to_boolean" to false),
stateKey = "another_state_key"
)
)
every { createRoomBody.initialStates } returns aListOfInitialStateEvents
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
aListOfInitialStateEvents.forEach { expected ->
val found = result.find { it.type == expected.type }
found.shouldNotBeNull()
found.content shouldBeEqualTo expected.content
found.stateKey shouldBeEqualTo expected.stateKey
}
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events contains the correct third party invite events`() = runTest {
// Given
val aListOfThreePids = listOf(
ThreePid.Email("bob@matrix.org"),
ThreePid.Msisdn("+11111111111"),
ThreePid.Email("alice@matrix.org"),
ThreePid.Msisdn("+22222222222"),
)
val aListOf3pids = aListOfThreePids.mapIndexed { index, threePid ->
ThreePidInviteBody(
idServer = "an_id_server_$index",
idAccessToken = "an_id_access_token_$index",
medium = when (threePid) {
is ThreePid.Email -> MEDIUM_EMAIL
is ThreePid.Msisdn -> MEDIUM_MSISDN
},
address = threePid.value
)
}
every { createRoomBody.invite3pids } returns aListOf3pids
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
val thirdPartyInviteEvents = result.filter { it.type == EventType.STATE_ROOM_THIRD_PARTY_INVITE }
val thirdPartyInviteContents = thirdPartyInviteEvents.map { it.content.toModel<RoomThirdPartyInviteContent>() }
val localThirdPartyInviteEvents = result.filter { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
val localThirdPartyInviteContents = localThirdPartyInviteEvents.map { it.content.toModel<LocalRoomThirdPartyInviteContent>() }
thirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
localThirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
aListOf3pids.forEach { expected ->
thirdPartyInviteContents.find { it?.displayName == expected.address }.shouldNotBeNull()
val localThirdPartyInviteContent = localThirdPartyInviteContents.find { it?.thirdPartyInvite == expected.toThreePid() }
localThirdPartyInviteContent.shouldNotBeNull()
localThirdPartyInviteContent.membership shouldBeEqualTo Membership.INVITE
localThirdPartyInviteContent.isDirect shouldBeEqualTo createRoomBody.isDirect.orFalse()
localThirdPartyInviteContent.displayName shouldBeEqualTo expected.address
}
}
@Test
fun `given a CreateRoomBody with default values when execute then the resulting list of events is correct`() = runTest {
// Given
// map of expected event types to occurrences
val expectedEventTypes = mapOf(
EventType.STATE_ROOM_CREATE to 1,
EventType.STATE_ROOM_POWER_LEVELS to 1,
EventType.STATE_ROOM_MEMBER to 1,
EventType.STATE_ROOM_GUEST_ACCESS to 1,
EventType.STATE_ROOM_HISTORY_VISIBILITY to 1,
)
coEvery { userService.resolveUser(any()) } answers {
if (firstArg<String>() == MY_USER_ID) User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR) else User(firstArg())
}
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
result.size shouldBeEqualTo expectedEventTypes.values.sum()
result.map { it.type }.toSet() shouldBeEqualTo expectedEventTypes.keys
// Room create
result.find { it.type == EventType.STATE_ROOM_CREATE }.shouldNotBeNull()
// Room member
result.singleOrNull { it.type == EventType.STATE_ROOM_MEMBER }?.stateKey shouldBeEqualTo MY_USER_ID
// Power levels
val powerLevelsContent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }?.content.toModel<PowerLevelsContent>()
powerLevelsContent.shouldNotBeNull()
powerLevelsContent.ban shouldBeEqualTo Role.Moderator.value
powerLevelsContent.kick shouldBeEqualTo Role.Moderator.value
powerLevelsContent.invite shouldBeEqualTo Role.Moderator.value
powerLevelsContent.redact shouldBeEqualTo Role.Moderator.value
powerLevelsContent.eventsDefault shouldBeEqualTo Role.Default.value
powerLevelsContent.usersDefault shouldBeEqualTo Role.Default.value
powerLevelsContent.stateDefault shouldBeEqualTo Role.Moderator.value
// Guest access
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
?.content.toModel<RoomGuestAccessContent>()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden
// History visibility
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo RoomHistoryVisibility.SHARED
}
@Test
fun `given a CreateRoomBody when execute then the resulting list of events is correctly ordered with the right values`() = runTest {
// Given
val expectedIsDirect = true
val expectedHistoryVisibility = RoomHistoryVisibility.WORLD_READABLE
every { createRoomBody.roomVersion } returns "a_room_version"
every { createRoomBody.roomAliasName } returns "a_room_alias_name"
every { createRoomBody.name } returns "a_name"
every { createRoomBody.topic } returns "a_topic"
every { createRoomBody.powerLevelContentOverride } returns PowerLevelsContent(
ban = 1,
kick = 2,
invite = 3,
redact = 4,
eventsDefault = 5,
events = null,
usersDefault = 6,
users = null,
stateDefault = 7,
notifications = null
)
every { createRoomBody.invite3pids } returns listOf(
ThreePidInviteBody(
idServer = "an_id_server",
idAccessToken = "an_id_access_token",
medium = MEDIUM_EMAIL,
address = "an_email@example.org"
)
)
every { createRoomBody.preset } returns CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
every { createRoomBody.initialStates } returns listOf(
Event(type = "a_custom_type", stateKey = ""),
// override the value from the preset
Event(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
content = RoomHistoryVisibilityContent(expectedHistoryVisibility.value).toContent()
)
)
every { createRoomBody.isDirect } returns expectedIsDirect
every { createRoomBody.invitedUserIds } returns listOf("a_user_id")
val orderedExpectedEventType = listOf(
EventType.STATE_ROOM_CREATE,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_ROOM_GUEST_ACCESS,
"a_custom_type",
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_MEMBER,
EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
)
// When
val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
val result = defaultCreateLocalRoomStateEventsTask.execute(params)
// Then
result.map { it.type } shouldBeEqualTo orderedExpectedEventType
result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo expectedHistoryVisibility
result.lastOrNull { it.type == EventType.STATE_ROOM_MEMBER }
?.content.toModel<RoomMemberContent>()?.isDirect shouldBeEqualTo expectedIsDirect
result.lastOrNull { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
?.content.toModel<LocalRoomThirdPartyInviteContent>()?.isDirect shouldBeEqualTo expectedIsDirect
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright (c) 2022 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.room.create
import io.mockk.coEvery
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.realm.kotlin.where
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.util.time.DefaultClock
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
private const val A_LOCAL_ROOM_ID = "local.a-local-room-id"
private const val AN_EXISTING_ROOM_ID = "an-existing-room-id"
private const val A_ROOM_ID = "a-room-id"
private const val MY_USER_ID = "my-user-id"
@ExperimentalCoroutinesApi
internal class DefaultCreateRoomFromLocalRoomTaskTest {
private val fakeMonarchy = FakeMonarchy()
private val clock = DefaultClock()
private val createRoomTask = mockk<CreateRoomTask>()
private val fakeStateEventDataSource = FakeStateEventDataSource()
private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask(
userId = MY_USER_ID,
monarchy = fakeMonarchy.instance,
createRoomTask = createRoomTask,
stateEventDataSource = fakeStateEventDataSource.instance,
clock = clock
)
@Before
fun setup() {
mockkStatic("org.matrix.android.sdk.internal.database.RealmQueryLatchKt")
coJustRun { awaitNotEmptyResult<Any>(realmConfiguration = any(), timeoutMillis = any(), builder = any()) }
mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
coEvery { any<EventEntity>().copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, any()) } answers { firstArg() }
mockkStatic("org.matrix.android.sdk.internal.database.query.CurrentStateEventEntityQueriesKt")
every { CurrentStateEventEntity.getOrCreate(fakeMonarchy.fakeRealm.instance, any(), any(), any()) } answers {
CurrentStateEventEntity(roomId = arg(2), stateKey = arg(3), type = arg(4))
}
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a local room id when execute then the existing room id is kept`() = runTest {
// Given
givenATombstoneEvent(
Event(
roomId = A_LOCAL_ROOM_ID,
type = EventType.STATE_ROOM_TOMBSTONE,
stateKey = "",
content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent()
)
)
// When
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then
verifyTombstoneEvent(AN_EXISTING_ROOM_ID)
result shouldBeEqualTo AN_EXISTING_ROOM_ID
}
@Test
fun `given a local room id when execute then it is correctly executed`() = runTest {
// Given
val aCreateRoomParams = mockk<CreateRoomParams>()
val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> {
every { roomSummaryEntity } returns mockk(relaxed = true)
every { createRoomParams } returns aCreateRoomParams
}
givenATombstoneEvent(null)
givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity)
coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID
// When
val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
val result = defaultCreateRoomFromLocalRoomTask.execute(params)
// Then
verifyTombstoneEvent(null)
// CreateRoomTask has been called with the initial CreateRoomParams
coVerify { createRoomTask.execute(aCreateRoomParams) }
// The resulting roomId matches the roomId returned by the createRoomTask
result shouldBeEqualTo A_ROOM_ID
// A tombstone state event has been created
coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) }
}
private fun givenATombstoneEvent(event: Event?) {
fakeStateEventDataSource.givenGetStateEventReturns(event)
}
private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) {
every {
fakeMonarchy.fakeRealm.instance
.where<LocalRoomSummaryEntity>()
.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID)
.findFirst()
} returns localRoomSummaryEntity
}
private fun verifyTombstoneEvent(expectedRoomId: String?) {
fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
?.content.toModel<RoomTombstoneContent>()
?.replacementRoomId shouldBeEqualTo expectedRoomId
}
}

View File

@ -22,6 +22,7 @@ import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
@ -69,7 +70,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
fakeStateEventDataSource.verifyGetStateEvent(
roomId = params.roomId,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
stateKey = A_USER_ID
stateKey = QueryStringValue.Equals(A_USER_ID)
)
}
}

View File

@ -33,7 +33,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction
internal class FakeMonarchy {
val instance = mockk<Monarchy>()
private val fakeRealm = FakeRealm()
val fakeRealm = FakeRealm()
init {
mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
@ -42,6 +42,12 @@ internal class FakeMonarchy {
} coAnswers {
secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
}
coEvery {
instance.doWithRealm(any())
} coAnswers {
firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance)
}
every { instance.realmConfiguration } returns mockk()
}
inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.test.fakes
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.QueryStateEventValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
@ -37,12 +37,12 @@ internal class FakeStateEventDataSource {
} returns event
}
fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: QueryStateEventValue) {
verify {
instance.getStateEvent(
roomId = roomId,
eventType = eventType,
stateKey = QueryStringValue.Equals(stateKey)
stateKey = stateKey
)
}
}

View File

@ -82,6 +82,7 @@ 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.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.RelationType
@ -1269,11 +1270,26 @@ class TimelineViewModel @AssistedInject constructor(
}
}
room.getStateEvent(EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)?.also {
setState { copy(tombstoneEvent = it) }
onRoomTombstoneUpdated(it)
}
}
}
private var roomTombstoneHandled = false
private fun onRoomTombstoneUpdated(tombstoneEvent: Event) = withState { state ->
if (roomTombstoneHandled) return@withState
if (state.isLocalRoom()) {
// Local room has been replaced, so navigate to the new room
val roomId = tombstoneEvent.getClearContent()?.toModel<RoomTombstoneContent>()
?.replacementRoomId
?: return@withState
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId, closeCurrentRoom = true))
roomTombstoneHandled = true
} else {
setState { copy(tombstoneEvent = tombstoneEvent) }
}
}
/**
* Navigates to the appropriate event (by paginating the thread timeline until the event is found
* in the snapshot. The main reason for this function is to support the /relations api

View File

@ -229,8 +229,9 @@ class TimelineEventVisibilityHelper @Inject constructor(
// Hide fake events for local rooms
if (RoomLocalEcho.isLocalEchoId(roomId) &&
root.getClearType() == EventType.STATE_ROOM_MEMBER ||
root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY) {
(root.getClearType() == EventType.STATE_ROOM_MEMBER ||
root.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY ||
root.getClearType() == EventType.STATE_ROOM_THIRD_PARTY_INVITE)) {
return true
}