1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-05 15:52:47 +02:00

Merge pull request #9065 from element-hq/feature/room_v12_support

Room v12 support
This commit is contained in:
ganfra
2025-08-04 20:20:15 +02:00
committed by GitHub
77 changed files with 857 additions and 701 deletions

View File

@@ -40,8 +40,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@@ -500,12 +500,12 @@ class SpaceHierarchyTest : InstrumentedTest {
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
commonTestHelper.retryPeriodically {
val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
val roomPowerLevels = aliceSession.getRoom(bobRoomId)!!
.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
?.let { RoomPowerLevels(it) }
roomPowerLevels!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
}
aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))

View File

@@ -30,15 +30,21 @@ object MatrixPatterns {
// Note: TLD is not mandatory (localhost, IP address...)
private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?"
private const val BASE_64_ALPHABET = "[0-9A-Za-z/\\+=]+"
private const val BASE_64_URL_SAFE_ALPHABET = "[0-9A-Za-z/\\-_]+"
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room ids in a string.
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "^!.+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
private const val MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX = "!$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS = MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX.toRegex()
// regex pattern to find room aliases in a string.
private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
@@ -48,11 +54,11 @@ object MatrixPatterns {
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+"
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$$BASE_64_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE)
// Ref: https://matrix.org/docs/spec/rooms/v4#event-ids
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+"
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find group ids in a string.
@@ -76,7 +82,10 @@ object MatrixPatterns {
PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ALIAS,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4,
PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
)
@@ -97,7 +106,9 @@ object MatrixPatterns {
* @return true if the string is a valid room Id
*/
fun isRoomId(str: String?): Boolean {
return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER
return str != null &&
(str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER ||
str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS)
}
/**

View File

@@ -16,8 +16,7 @@
package org.matrix.android.sdk.api.session.pushrules
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.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
class SenderNotificationPermissionCondition(
/**
@@ -35,8 +34,7 @@ class SenderNotificationPermissionCondition(
override fun technicalDescription() = "User power level <$key>"
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevels.notificationLevel(key)
fun isSatisfied(event: Event, roomPowerLevels: RoomPowerLevels): Boolean {
return event.senderId != null && roomPowerLevels.isUserAbleToTriggerNotification(event.senderId, key)
}
}

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room
import org.matrix.android.sdk.api.query.QueryStateEventValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
@@ -34,3 +35,10 @@ fun Room.getTimelineEvent(eventId: String): TimelineEvent? =
*/
fun Room.getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? =
stateService().getStateEvent(eventType, stateKey)
/**
* Get the current RoomPowerLevels of the room.
*/
fun Room.getRoomPowerLevels(): RoomPowerLevels {
return stateService().getRoomPowerLevels()
}

View File

@@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent.Companion.NOTIFICATIONS_ROOM_KEY
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
/**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
@@ -34,7 +35,7 @@ data class PowerLevelsContent(
*/
@Json(name = "kick") val kick: Int? = null,
/**
* The level required to invite a user. Defaults to 50 if unspecified.
* The level required to invite a user. Defaults to 0 if unspecified.
*/
@Json(name = "invite") val invite: Int? = null,
/**
@@ -88,7 +89,7 @@ data class PowerLevelsContent(
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level, default to Moderator if the key is not found
* @return the level
*/
fun notificationLevel(key: String): Int {
return when (val value = notifications.orEmpty()[key]) {
@@ -96,10 +97,9 @@ data class PowerLevelsContent(
is String -> value.toInt()
is Double -> value.toInt()
is Int -> value
else -> Role.Moderator.value
else -> defaultNotificationLevel(key)
}
}
companion object {
/**
* Key to use for content.notifications and get the level required to trigger an @room notification. Defaults to 50 if unspecified.
@@ -108,11 +108,20 @@ data class PowerLevelsContent(
}
}
private fun defaultNotificationLevel(key: String): Int {
return when (key) {
NOTIFICATIONS_ROOM_KEY -> UserPowerLevel.Moderator.value
else -> UserPowerLevel.User.value
}
}
// Fallback to default value, defined in the Matrix specification
fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value
fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value
fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value
fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value
fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value
fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value
fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value
fun PowerLevelsContent?.banOrDefault() = this?.ban ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.kickOrDefault() = this?.kick ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.inviteOrDefault() = this?.invite ?: UserPowerLevel.User.value
fun PowerLevelsContent?.redactOrDefault() = this?.redact ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.eventsDefaultOrDefault() = this?.eventsDefault ?: UserPowerLevel.User.value
fun PowerLevelsContent?.usersDefaultOrDefault() = this?.usersDefault ?: UserPowerLevel.User.value
fun PowerLevelsContent?.stateDefaultOrDefault() = this?.stateDefault ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.notificationLevelOrDefault(key: String) = this?.notificationLevel(key) ?: defaultNotificationLevel(key)

View File

@@ -18,15 +18,39 @@ package org.matrix.android.sdk.api.session.room.model.create
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
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.toModel
/**
* Content of a m.room.create type event.
*/
@JsonClass(generateAdapter = true)
data class RoomCreateContent(
// Creator should be replaced by the sender of the event
@Json(name = "creator") val creator: String? = null,
@Json(name = "room_version") val roomVersion: String? = null,
@Json(name = "predecessor") val predecessor: Predecessor? = null,
// Defines the room type, see #RoomType (user extensible)
@Json(name = "type") val type: String? = null
@Json(name = "type") val type: String? = null,
@Json(name = "additional_creators") val additionalCreators: List<String>? = null,
)
data class RoomCreateContentWithSender(
val senderId: String,
val inner: RoomCreateContent
) {
val creators = setOf(senderId) + inner.additionalCreators.orEmpty().toSet()
}
fun Event.getRoomCreateContentWithSender(): RoomCreateContentWithSender? {
if (this.type != EventType.STATE_ROOM_CREATE) return null
val innerContent = getClearContent().toModel<RoomCreateContent>() ?: return null
val senderId = senderId ?: return null
return RoomCreateContentWithSender(senderId, innerContent)
}
fun RoomCreateContent.explicitlyPrivilegeRoomCreators(): Boolean {
val supportedRoomVersions = listOf("org.matrix.hydra.11", "12")
return supportedRoomVersions.contains(roomVersion)
}

View File

@@ -17,26 +17,21 @@
package org.matrix.android.sdk.api.session.room.powerlevels
sealed class Role(open val value: Int) : Comparable<Role> {
object Admin : Role(100)
object Moderator : Role(50)
object Default : Role(0)
data class Custom(override val value: Int) : Role(value)
override fun compareTo(other: Role): Int {
return value.compareTo(other.value)
}
enum class Role {
Creator,
SuperAdmin,
Admin,
Moderator,
User;
companion object {
// Order matters, default value should be checked after defined roles
fun fromValue(value: Int, default: Int): Role {
return when (value) {
Admin.value -> Admin
Moderator.value -> Moderator
Default.value,
default -> Default
else -> Custom(value)
fun getSuggestedRole(userPowerLevel: UserPowerLevel): Role {
return when {
userPowerLevel == UserPowerLevel.Infinite -> Creator
userPowerLevel >= UserPowerLevel.SuperAdmin -> SuperAdmin
userPowerLevel >= UserPowerLevel.Admin -> Admin
userPowerLevel >= UserPowerLevel.Moderator -> Moderator
else -> User
}
}
}

View File

@@ -19,17 +19,23 @@ package org.matrix.android.sdk.api.session.room.powerlevels
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.banOrDefault
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.model.create.explicitlyPrivilegeRoomCreators
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.notificationLevelOrDefault
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
/**
* This class is an helper around PowerLevelsContent.
* This class is an helper around PowerLevelsContent and RoomCreateContent.
*/
class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
class RoomPowerLevels(
val powerLevelsContent: PowerLevelsContent?,
private val roomCreateContent: RoomCreateContentWithSender?,
) {
/**
* Returns the user power level of a dedicated user Id.
@@ -37,10 +43,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserPowerLevelValue(userId: String): Int {
return powerLevelsContent.users
fun getUserPowerLevel(userId: String): UserPowerLevel {
if (shouldGiveInfinitePowerLevel(userId)) return UserPowerLevel.Infinite
if (powerLevelsContent == null) return UserPowerLevel.User
val value = powerLevelsContent.users
?.get(userId)
?: powerLevelsContent.usersDefaultOrDefault()
return UserPowerLevel.Value(value)
}
/**
@@ -49,10 +59,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserRole(userId: String): Role {
val value = getUserPowerLevelValue(userId)
// I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web
return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault())
fun getSuggestedRole(userId: String): Role {
val value = getUserPowerLevel(userId)
return Role.getSuggestedRole(value)
}
/**
@@ -65,14 +74,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
*/
fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevelValue(userId)
val minimumPowerLevel = powerLevelsContent.events?.get(eventType)
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = powerLevelsContent?.events?.get(eventType)
?: if (isState) {
powerLevelsContent.stateDefaultOrDefault()
} else {
powerLevelsContent.eventsDefaultOrDefault()
}
powerLevel >= minimumPowerLevel
powerLevel >= UserPowerLevel.Value(minimumPowerLevel)
} else false
}
@@ -82,8 +91,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to invite
*/
fun isUserAbleToInvite(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.inviteOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.inviteOrDefault())
}
/**
@@ -92,8 +101,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to ban
*/
fun isUserAbleToBan(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.banOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.banOrDefault())
}
/**
@@ -102,8 +111,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to kick
*/
fun isUserAbleToKick(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.kickOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.kickOrDefault())
}
/**
@@ -112,7 +121,22 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to redact
*/
fun isUserAbleToRedact(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redactOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.redactOrDefault())
}
fun isUserAbleToTriggerNotification(userId: String, notificationKey: String): Boolean {
val userPowerLevel = getUserPowerLevel(userId)
val notificationPowerLevel = UserPowerLevel.Value(powerLevelsContent.notificationLevelOrDefault(key = notificationKey))
return userPowerLevel >= notificationPowerLevel
}
private fun shouldGiveInfinitePowerLevel(userId: String): Boolean {
if (roomCreateContent == null) return false
return if (roomCreateContent.inner.explicitlyPrivilegeRoomCreators()) {
roomCreateContent.creators.contains(userId)
} else {
false
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.api.session.room.powerlevels
sealed interface UserPowerLevel : Comparable<UserPowerLevel> {
data object Infinite : UserPowerLevel
@JvmInline
value class Value(val value: Int) : UserPowerLevel
override fun compareTo(other: UserPowerLevel): Int {
return when (this) {
Infinite -> when (other) {
Infinite -> 0
is Value -> 1
}
is Value -> when (other) {
Infinite -> -1
is Value -> value.compareTo(other.value)
}
}
}
companion object {
val User = Value(0)
val Moderator = Value(50)
val Admin = Value(100)
val SuperAdmin = Value(150)
}
}

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
@@ -106,4 +107,6 @@ interface StateService {
suspend fun setJoinRulePublic()
suspend fun setJoinRuleInviteOnly()
suspend fun setJoinRuleRestricted(allowList: List<String>)
fun getRoomPowerLevels(): RoomPowerLevels
fun getRoomPowerLevelsLive(): LiveData<RoomPowerLevels>
}

View File

@@ -17,15 +17,11 @@
package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
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.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import java.net.URLEncoder
import javax.inject.Inject
@@ -101,10 +97,7 @@ internal class ViaParameterFinder @Inject constructor(
}
fun userCanInvite(userId: String, roomId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAbleToInvite(userId)
}
}

View File

@@ -15,24 +15,20 @@
*/
package org.matrix.android.sdk.internal.session.pushers
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.toModel
import org.matrix.android.sdk.api.session.pushrules.ConditionResolver
import org.matrix.android.sdk.api.session.pushrules.ContainsDisplayNameCondition
import org.matrix.android.sdk.api.session.pushrules.EventMatchCondition
import org.matrix.android.sdk.api.session.pushrules.RoomMemberCountCondition
import org.matrix.android.sdk.api.session.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import javax.inject.Inject
internal class DefaultConditionResolver @Inject constructor(
private val roomGetter: RoomGetter,
@UserId private val userId: String
@UserId private val userId: String,
) : ConditionResolver {
override fun resolveEventMatchCondition(
@@ -55,13 +51,8 @@ internal class DefaultConditionResolver @Inject constructor(
): Boolean {
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()
return condition.isSatisfied(event, powerLevelsContent)
val roomPowerLevels = room.getRoomPowerLevels()
return condition.isSatisfied(event, roomPowerLevels)
}
override fun resolveContainsDisplayNameCondition(

View File

@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.room
import io.realm.Realm
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event
@@ -27,7 +26,6 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.getRelationContent
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.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
@@ -36,7 +34,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
@@ -62,6 +59,7 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
@@ -216,9 +214,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
in EventType.POLL_END.values -> {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(event.roomId)
pollAggregationProcessor.handlePollEndEvent(session, roomPowerLevels, realm, event)
}
}
in EventType.STATE_ROOM_BEACON_INFO.values -> {
@@ -381,12 +378,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? {
return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
}
private fun handleInitialAggregatedRelations(
realm: Realm,
event: Event,

View File

@@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@@ -160,13 +160,13 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
return true
}
override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
override fun handlePollEndEvent(session: Session, roomPowerLevels: RoomPowerLevels, realm: Realm, event: Event): Boolean {
val roomId = event.roomId ?: return false
val pollEventId = event.getRelationContent()?.eventId ?: return false
val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) {
if (!isPollOwner && !roomPowerLevels.isUserAbleToRedact(event.senderId ?: "")) {
return false
}

View File

@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
internal interface PollAggregationProcessor {
/**
@@ -48,7 +48,7 @@ internal interface PollAggregationProcessor {
*/
fun handlePollEndEvent(
session: Session,
powerLevelsHelper: PowerLevelsHelper,
roomPowerLevels: RoomPowerLevels,
realm: Realm,
event: Event
): Boolean

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 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.powerlevels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
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.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.getRoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal fun StateEventDataSource.getRoomPowerLevels(roomId: String): RoomPowerLevels {
val powerLevelsEvent = getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
val roomCreateEvent = getStateEvent(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty)
return createRoomPowerLevels(powerLevelsEvent = powerLevelsEvent, roomCreateEvent = roomCreateEvent)
}
internal fun StateEventDataSource.getRoomPowerLevelsLive(roomId: String): LiveData<RoomPowerLevels> {
val powerLevelsEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
val roomCreateEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty)
val resultLiveData = MediatorLiveData<RoomPowerLevels>()
fun emitIfReady(powerLevelEvent: Optional<Event>?, roomCreateEvent: Optional<Event>?) {
if (powerLevelEvent != null && roomCreateEvent != null) {
val roomPowerLevels = createRoomPowerLevels(powerLevelEvent.get(), roomCreateEvent.get())
resultLiveData.postValue(roomPowerLevels)
}
}
resultLiveData.apply {
var powerLevelEvent: Optional<Event>? = null
var roomCreateEvent: Optional<Event>? = null
addSource(powerLevelsEventLive) {
powerLevelEvent = it
emitIfReady(powerLevelEvent, roomCreateEvent)
}
addSource(roomCreateEventLive) {
roomCreateEvent = it
emitIfReady(powerLevelEvent, roomCreateEvent)
}
}
return resultLiveData
}
private fun createRoomPowerLevels(powerLevelsEvent: Event?, roomCreateEvent: Event?): RoomPowerLevels {
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
val roomCreateContent = roomCreateEvent?.getRoomCreateContentWithSender()
return RoomPowerLevels(powerLevelsContent, roomCreateContent)
}

View File

@@ -31,11 +31,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevelsLive
internal class DefaultStateService @AssistedInject constructor(
@Assisted private val roomId: String,
@@ -65,6 +68,14 @@ internal class DefaultStateService @AssistedInject constructor(
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun getRoomPowerLevels(): RoomPowerLevels {
return stateEventDataSource.getRoomPowerLevels(roomId)
}
override fun getRoomPowerLevelsLive(): LiveData<RoomPowerLevels> {
return stateEventDataSource.getRoomPowerLevelsLive(roomId)
}
override suspend fun sendStateEvent(
eventType: String,
stateKey: String,

View File

@@ -34,7 +34,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
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.sync.model.RoomSyncUnreadNotifications
@@ -313,13 +314,25 @@ internal class RoomSummaryUpdater @Inject constructor(
// check if sender can post child relation in parent?
val senderId = parentInfo.stateEventSender
val parentRoomId = parentInfo.roomId
val powerLevelsHelper = CurrentStateEventEntity
val powerLevelsContent = CurrentStateEventEntity
.getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_POWER_LEVELS)
?.root
?.let { ContentMapper.map(it.content).toModel<PowerLevelsContent>() }
?.let { PowerLevelsHelper(it) }
isValidRelation = powerLevelsHelper?.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD) ?: false
val roomCreateContent = CurrentStateEventEntity
.getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_CREATE)
?.root
?.let {
val content = ContentMapper.map(it.content).toModel<RoomCreateContent>()
val sender = it.sender
if (content != null && sender != null) {
RoomCreateContentWithSender(sender, content)
} else {
null
}
}
val roomPowerLevels = RoomPowerLevels(powerLevelsContent, roomCreateContent)
isValidRelation = roomPowerLevels.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD)
}
if (isValidRelation) {

View File

@@ -23,11 +23,10 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.version.RoomVersionService
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal class DefaultRoomVersionService @AssistedInject constructor(
@@ -71,11 +70,8 @@ internal class DefaultRoomVersionService @AssistedInject constructor(
}
override fun userMayUpgradeRoom(userId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE)
}
companion object {

View File

@@ -35,8 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.api.session.space.Space
@@ -47,11 +46,13 @@ import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent
import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.SpaceGetter
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
@@ -83,7 +84,7 @@ internal class DefaultSpaceService @Inject constructor(
if (isPublic) {
this.roomAliasName = roomAliasLocalPart
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = if (isPublic) Role.Default.value else Role.Moderator.value
invite = UserPowerLevel.User.value
)
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
@@ -253,15 +254,8 @@ internal class DefaultSpaceService @Inject constructor(
if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) {
throw UnsupportedOperationException("Cannot add canonical child if not member of parent")
}
val powerLevelsEvent = stateEventDataSource.getStateEvent(
roomId = parentSpaceId,
eventType = EventType.STATE_ROOM_POWER_LEVELS,
stateKey = QueryStringValue.IsEmpty
)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
?: throw UnsupportedOperationException("Cannot add canonical child, missing power level")
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) {
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(parentSpaceId)
if (!roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) {
throw UnsupportedOperationException("Cannot add canonical child, not enough power level")
}
}

View File

@@ -30,15 +30,13 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
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.toModel
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory
@@ -200,12 +198,7 @@ internal class WidgetManager @Inject constructor(
}
fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
val powerLevelsEvent = stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = EventType.STATE_ROOM_POWER_LEVELS,
stateKey = QueryStringValue.IsEmpty
)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
}
}

View File

@@ -33,7 +33,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
@@ -255,9 +255,9 @@ class DefaultPollAggregationProcessorTest {
every { room.getTimelineEvent(eventId) } returns if (hasExistingTimelineEvent) A_TIMELINE_EVENT else null
}
private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper {
val powerLevelsHelper = mockk<PowerLevelsHelper>()
every { powerLevelsHelper.isUserAbleToRedact(userId) } returns isAbleToRedact
return powerLevelsHelper
private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): RoomPowerLevels {
val roomPowerLevels = mockk<RoomPowerLevels>()
every { roomPowerLevels.isUserAbleToRedact(userId) } returns isAbleToRedact
return roomPowerLevels
}
}

View File

@@ -51,7 +51,7 @@ 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.room.powerlevels.UserPowerLevel
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
@@ -372,13 +372,13 @@ internal class DefaultCreateLocalRoomStateEventsTaskTest {
// 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
powerLevelsContent.ban shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.kick shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.invite shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.redact shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.eventsDefault shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.usersDefault shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.stateDefault shouldBeEqualTo UserPowerLevel.Moderator.value
// Guest access
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
?.content.toModel<RoomGuestAccessContent>()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden