From dc9de08bc59d87e4f49f34547738a06f139f70c2 Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 10 Sep 2023 15:25:24 +0200 Subject: [PATCH] feat: initial permissions implementation Signed-off-by: Infi --- .../chat/revolt/api/internals/Permissions.kt | 105 ++++++++++++++++++ .../java/chat/revolt/api/internals/Roles.kt | 73 ++++++++++++ .../java/chat/revolt/api/schemas/Channel.kt | 16 ++- .../java/chat/revolt/api/schemas/Server.kt | 8 +- .../chat/views/channel/ChannelScreen.kt | 4 + .../views/channel/ChannelScreenViewModel.kt | 21 ++-- 6 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/internals/Permissions.kt diff --git a/app/src/main/java/chat/revolt/api/internals/Permissions.kt b/app/src/main/java/chat/revolt/api/internals/Permissions.kt new file mode 100644 index 00000000..9389b5e1 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/internals/Permissions.kt @@ -0,0 +1,105 @@ +package chat.revolt.api.internals + +enum class PermissionBit(val value: Long) { + // * Generic permissions + ManageChannel(1L shl 0), + ManageServer(1L shl 1), + ManagePermissions(1L shl 2), + ManageRole(1L shl 3), + ManageCustomisation(1L shl 4), + + // % 1 bit reserved + + // * Member permissions + KickMembers(1L shl 6), + BanMembers(1L shl 7), + TimeoutMembers(1L shl 8), + AssignRoles(1L shl 9), + ChangeNickname(1L shl 10), + ManageNicknames(1L shl 11), + ChangeAvatar(1L shl 12), + RemoveAvatars(1L shl 13), + + // % 7 bits reserved + + // * Channel permissions + ViewChannel(1L shl 20), + ReadMessageHistory(1L shl 21), + SendMessage(1L shl 22), + ManageMessages(1L shl 23), + ManageWebhooks(1L shl 24), + InviteOthers(1L shl 25), + SendEmbeds(1L shl 26), + UploadFiles(1L shl 27), + Masquerade(1L shl 28), + React(1L shl 29), + + // * Voice permissions + Connect(1L shl 30), + Speak(1L shl 31), + Video(1L shl 32), + MuteMembers(1L shl 33), + DeafenMembers(1L shl 34), + MoveMembers(1L shl 35), + + // * Misc. permissions + // % Bits 36 to 52: free area + // % Bits 53 to 64: do not use + + // * Grant all permissions + GrantAllSafe(0x000FFFFFFFFFFFFFL), + GrantAll(Long.MAX_VALUE); + + operator fun plus(other: PermissionBit): Long { + return this.value or other.value + } + + operator fun plus(other: Long): Long { + return this.value or other + } +} + +operator fun Long.plus(other: PermissionBit): Long { + return this or other.value +} + +fun Long.hasPermission(permission: PermissionBit): Boolean { + return this and permission.value == permission.value +} + +object BitDefaults { + val AllowedInTimeout = + PermissionBit.ViewChannel + PermissionBit.ReadMessageHistory + + val ViewOnly = + PermissionBit.ViewChannel + PermissionBit.ReadMessageHistory + + val Default = + ViewOnly + + PermissionBit.SendMessage + + PermissionBit.InviteOthers + + PermissionBit.SendEmbeds + + PermissionBit.UploadFiles + + PermissionBit.Connect + + PermissionBit.Speak + + val SavedMessages = + PermissionBit.GrantAllSafe.value + + val DirectMessages = + Default + + PermissionBit.ManageChannel + + PermissionBit.React + + val Server = + Default + + PermissionBit.React + + PermissionBit.ChangeNickname + + PermissionBit.ChangeAvatar + + val Webhook = + PermissionBit.SendMessage + + PermissionBit.SendEmbeds + + PermissionBit.Masquerade + + PermissionBit.React +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/internals/Roles.kt b/app/src/main/java/chat/revolt/api/internals/Roles.kt index 9174f1d7..e1d87771 100644 --- a/app/src/main/java/chat/revolt/api/internals/Roles.kt +++ b/app/src/main/java/chat/revolt/api/internals/Roles.kt @@ -1,7 +1,14 @@ package chat.revolt.api.internals import chat.revolt.api.RevoltAPI +import chat.revolt.api.schemas.Channel +import chat.revolt.api.schemas.ChannelType +import chat.revolt.api.schemas.Member +import chat.revolt.api.schemas.PermissionDescription import chat.revolt.api.schemas.Role +import chat.revolt.api.schemas.Server +import chat.revolt.api.schemas.User +import kotlinx.datetime.Clock object Roles { // lowest rank = highest role @@ -39,4 +46,70 @@ object Roles { return server.roles?.values?.filter(predicate)?.sortedBy { it.rank } ?: emptyList() } + + fun permissionFor(server: Server, member: Member): Long { + val user = RevoltAPI.userCache[member.id?.user] ?: return 0L + + if (user.privileged == true) return PermissionBit.GrantAllSafe.value + if (server.owner == member.id?.user) return PermissionBit.GrantAllSafe.value + + var calculated = server.defaultPermissions ?: 0L + + member.roles?.forEach { roleId -> + val role = server.roles?.get(roleId) ?: return@forEach + val permissions = role.permissions ?: PermissionDescription(0, 0) + + calculated = calculated or permissions.a and permissions.d.inv() + } + + if (member.timeoutTimestamp()?.let { it > Clock.System.now() } == true) { + calculated = calculated and BitDefaults.AllowedInTimeout + } + + return calculated + } + + // TODO may not be exactly accurate + // See https://github.com/revoltchat/revolt.js/blob/2ba023c879b2a53f9a3cc7042e6721c28dd970ba/src/permissions/calculator.ts#L80-L158 + fun permissionFor(channel: Channel, user: User? = null, member: Member? = null): Long { + return when (channel.channelType) { + ChannelType.SavedMessages -> BitDefaults.SavedMessages + + ChannelType.DirectMessage -> BitDefaults.DirectMessages + ChannelType.Group -> if (channel.owner == user?.id) PermissionBit.GrantAllSafe.value else BitDefaults.DirectMessages + + ChannelType.TextChannel, ChannelType.VoiceChannel -> { + val server = RevoltAPI.serverCache[channel.server] ?: return 0L + + if (server.owner == user?.id) return PermissionBit.GrantAllSafe.value + + val chMember = member ?: RevoltAPI.members.getMember( + server.id ?: return 0L, + user?.id ?: return 0L + ) ?: return 0L + + var calculated = permissionFor(server, chMember) + + if (channel.defaultPermissions != null) { + calculated = + calculated or channel.defaultPermissions.a and channel.defaultPermissions.d.inv() + } + + if (chMember.roles?.isNotEmpty() == true) { + chMember.roles.forEach { roleId -> + val override = channel.rolePermissions?.get(roleId) ?: return@forEach + calculated = calculated or override.a and override.d.inv() + } + } + + if (chMember.timeoutTimestamp()?.let { it > Clock.System.now() } == true) { + calculated = calculated and BitDefaults.AllowedInTimeout + } + + return calculated + } + + null -> 0L + } + } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Channel.kt b/app/src/main/java/chat/revolt/api/schemas/Channel.kt index bd762aee..c907abc5 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Channel.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Channel.kt @@ -1,5 +1,6 @@ package chat.revolt.api.schemas +import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -32,7 +33,9 @@ data class Member( val avatar: AutumnResource? = null, val roles: List? = null, - val nickname: String? = null + val nickname: String? = null, + + val timeout: String? = null, ) { fun mergeWithPartial(other: Member): Member { return Member( @@ -40,9 +43,14 @@ data class Member( joinedAt = other.joinedAt ?: joinedAt, avatar = other.avatar ?: avatar, roles = other.roles ?: roles, - nickname = other.nickname ?: nickname + nickname = other.nickname ?: nickname, + timeout = other.timeout ?: timeout, ) } + + fun timeoutTimestamp(): Instant? { + return timeout?.let { Instant.parse(it) } + } } @Serializable @@ -63,9 +71,9 @@ data class Channel( val permissions: Long? = null, val server: String? = null, @SerialName("role_permissions") - val rolePermissions: Map? = null, + val rolePermissions: Map? = null, @SerialName("default_permissions") - val defaultPermissions: DefaultPermissions? = null, + val defaultPermissions: PermissionDescription? = null, val nsfw: Boolean? = null, val type: String? = null, // this is _only_ used for websocket events! ) { diff --git a/app/src/main/java/chat/revolt/api/schemas/Server.kt b/app/src/main/java/chat/revolt/api/schemas/Server.kt index 9b7606cc..d29ddf9a 100644 --- a/app/src/main/java/chat/revolt/api/schemas/Server.kt +++ b/app/src/main/java/chat/revolt/api/schemas/Server.kt @@ -59,16 +59,16 @@ data class SystemMessages( @Serializable data class Role( val name: String? = null, - val permissions: DefaultPermissions? = null, + val permissions: PermissionDescription? = null, val colour: String? = null, val hoist: Boolean? = null, val rank: Double? = null ) @Serializable -data class DefaultPermissions( - val a: Long? = null, - val d: Long? = null +data class PermissionDescription( + val a: Long, + val d: Long ) @Serializable diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 86ad8fb6..21c41de1 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -274,6 +274,10 @@ fun ChannelScreen( } } + LaunchedEffect(viewModel.activeChannel, RevoltAPI.channelCache, RevoltAPI.serverCache) { + viewModel.checkShouldDenyMessageField() + } + Box( modifier = Modifier.weight(1f), contentAlignment = Alignment.BottomEnd diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt index b2028400..0af595a7 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreenViewModel.kt @@ -14,8 +14,11 @@ import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltJson import chat.revolt.api.internals.ChannelUtils +import chat.revolt.api.internals.PermissionBit +import chat.revolt.api.internals.Roles import chat.revolt.api.internals.SpecialUsers import chat.revolt.api.internals.ULID +import chat.revolt.api.internals.hasPermission import chat.revolt.api.realtime.RealtimeSocketFrames import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame @@ -160,8 +163,6 @@ class ChannelScreenViewModel : ViewModel() { activeChannel = RevoltAPI.channelCache[id] } - checkShouldDenyMessageField() - if (activeChannel?.lastMessageID != null) { ackNewest() } else { @@ -438,13 +439,19 @@ class ChannelScreenViewModel : ViewModel() { pendingMessageContent = "" } - private fun checkShouldDenyMessageField() { - // TODO Check for send message permission. - val hasPermission = true - + suspend fun checkShouldDenyMessageField() { if (activeChannel == null) return - val partnerId = ChannelUtils.resolveDMPartner(activeChannel!!) ?: return + val selfUser = RevoltAPI.userCache[RevoltAPI.selfId] ?: return + val selfMember = + activeChannel?.server?.let { RevoltAPI.members.getMember(it, selfUser.id!!) } + ?: fetchMember(activeChannel!!.server!!, selfUser.id!!) + + val hasPermission = + Roles.permissionFor(activeChannel!!, selfUser, selfMember) + .hasPermission(PermissionBit.SendMessage) + + val partnerId = ChannelUtils.resolveDMPartner(activeChannel!!) denyMessageField = when { partnerId == SpecialUsers.PLATFORM_MODERATION_USER -> true