feat: initial permissions implementation

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-09-10 15:25:24 +02:00
parent 4206bcf304
commit dc9de08bc5
6 changed files with 212 additions and 15 deletions

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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<String>? = 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<String, DefaultPermissions>? = null,
val rolePermissions: Map<String, PermissionDescription>? = 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!
) {

View File

@ -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

View File

@ -274,6 +274,10 @@ fun ChannelScreen(
}
}
LaunchedEffect(viewModel.activeChannel, RevoltAPI.channelCache, RevoltAPI.serverCache) {
viewModel.checkShouldDenyMessageField()
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.BottomEnd

View File

@ -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