feat: initial permissions implementation
Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
parent
4206bcf304
commit
dc9de08bc5
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
package chat.revolt.api.internals
|
package chat.revolt.api.internals
|
||||||
|
|
||||||
import chat.revolt.api.RevoltAPI
|
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.Role
|
||||||
|
import chat.revolt.api.schemas.Server
|
||||||
|
import chat.revolt.api.schemas.User
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
object Roles {
|
object Roles {
|
||||||
// lowest rank = highest role
|
// lowest rank = highest role
|
||||||
|
|
@ -39,4 +46,70 @@ object Roles {
|
||||||
|
|
||||||
return server.roles?.values?.filter(predicate)?.sortedBy { it.rank } ?: emptyList()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package chat.revolt.api.schemas
|
package chat.revolt.api.schemas
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -32,7 +33,9 @@ data class Member(
|
||||||
|
|
||||||
val avatar: AutumnResource? = null,
|
val avatar: AutumnResource? = null,
|
||||||
val roles: List<String>? = null,
|
val roles: List<String>? = null,
|
||||||
val nickname: String? = null
|
val nickname: String? = null,
|
||||||
|
|
||||||
|
val timeout: String? = null,
|
||||||
) {
|
) {
|
||||||
fun mergeWithPartial(other: Member): Member {
|
fun mergeWithPartial(other: Member): Member {
|
||||||
return Member(
|
return Member(
|
||||||
|
|
@ -40,9 +43,14 @@ data class Member(
|
||||||
joinedAt = other.joinedAt ?: joinedAt,
|
joinedAt = other.joinedAt ?: joinedAt,
|
||||||
avatar = other.avatar ?: avatar,
|
avatar = other.avatar ?: avatar,
|
||||||
roles = other.roles ?: roles,
|
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
|
@Serializable
|
||||||
|
|
@ -63,9 +71,9 @@ data class Channel(
|
||||||
val permissions: Long? = null,
|
val permissions: Long? = null,
|
||||||
val server: String? = null,
|
val server: String? = null,
|
||||||
@SerialName("role_permissions")
|
@SerialName("role_permissions")
|
||||||
val rolePermissions: Map<String, DefaultPermissions>? = null,
|
val rolePermissions: Map<String, PermissionDescription>? = null,
|
||||||
@SerialName("default_permissions")
|
@SerialName("default_permissions")
|
||||||
val defaultPermissions: DefaultPermissions? = null,
|
val defaultPermissions: PermissionDescription? = null,
|
||||||
val nsfw: Boolean? = null,
|
val nsfw: Boolean? = null,
|
||||||
val type: String? = null, // this is _only_ used for websocket events!
|
val type: String? = null, // this is _only_ used for websocket events!
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,16 @@ data class SystemMessages(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Role(
|
data class Role(
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val permissions: DefaultPermissions? = null,
|
val permissions: PermissionDescription? = null,
|
||||||
val colour: String? = null,
|
val colour: String? = null,
|
||||||
val hoist: Boolean? = null,
|
val hoist: Boolean? = null,
|
||||||
val rank: Double? = null
|
val rank: Double? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DefaultPermissions(
|
data class PermissionDescription(
|
||||||
val a: Long? = null,
|
val a: Long,
|
||||||
val d: Long? = null
|
val d: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,10 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.activeChannel, RevoltAPI.channelCache, RevoltAPI.serverCache) {
|
||||||
|
viewModel.checkShouldDenyMessageField()
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentAlignment = Alignment.BottomEnd
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@ import chat.revolt.R
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
import chat.revolt.api.RevoltJson
|
import chat.revolt.api.RevoltJson
|
||||||
import chat.revolt.api.internals.ChannelUtils
|
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.SpecialUsers
|
||||||
import chat.revolt.api.internals.ULID
|
import chat.revolt.api.internals.ULID
|
||||||
|
import chat.revolt.api.internals.hasPermission
|
||||||
import chat.revolt.api.realtime.RealtimeSocketFrames
|
import chat.revolt.api.realtime.RealtimeSocketFrames
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelStartTypingFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
import chat.revolt.api.realtime.frames.receivable.ChannelStopTypingFrame
|
||||||
|
|
@ -160,8 +163,6 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
activeChannel = RevoltAPI.channelCache[id]
|
activeChannel = RevoltAPI.channelCache[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
checkShouldDenyMessageField()
|
|
||||||
|
|
||||||
if (activeChannel?.lastMessageID != null) {
|
if (activeChannel?.lastMessageID != null) {
|
||||||
ackNewest()
|
ackNewest()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -438,13 +439,19 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
pendingMessageContent = ""
|
pendingMessageContent = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkShouldDenyMessageField() {
|
suspend fun checkShouldDenyMessageField() {
|
||||||
// TODO Check for send message permission.
|
|
||||||
val hasPermission = true
|
|
||||||
|
|
||||||
if (activeChannel == null) return
|
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 {
|
denyMessageField = when {
|
||||||
partnerId == SpecialUsers.PLATFORM_MODERATION_USER -> true
|
partnerId == SpecialUsers.PLATFORM_MODERATION_USER -> true
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue