feat(livekit): more permission handling

This commit is contained in:
Zomatree
2024-07-23 04:07:34 +01:00
parent ffbc899792
commit 120ca449b8
18 changed files with 177 additions and 39 deletions

1
Cargo.lock generated
View File

@@ -4182,6 +4182,7 @@ dependencies = [
name = "revolt-result" name = "revolt-result"
version = "0.7.1" version = "0.7.1"
dependencies = [ dependencies = [
"log",
"revolt_okapi", "revolt_okapi",
"revolt_rocket_okapi", "revolt_rocket_okapi",
"rocket", "rocket",

View File

@@ -236,7 +236,7 @@ pub enum EventV1 {
UserVoiceStateUpdate { UserVoiceStateUpdate {
id: String, id: String,
channel_id: String, channel_id: String,
data: PartialUserVoiceState data: PartialUserVoiceState,
} }
} }

View File

@@ -32,11 +32,11 @@ auto_derived_partial!(
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted /// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "if_false")] #[serde(skip_serializing_if = "Option::is_none")]
pub can_publish: bool, pub can_publish: Option<bool>,
/// Whether the member is server-wide voice deafened /// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "if_false")] #[serde(skip_serializing_if = "Option::is_none")]
pub can_receive: bool, pub can_receive: Option<bool>,
}, },
"PartialMember" "PartialMember"
); );
@@ -57,6 +57,8 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
CanReceive,
CanPublish,
} }
/// Member removal intention /// Member removal intention
@@ -76,8 +78,8 @@ impl Default for Member {
avatar: None, avatar: None,
roles: vec![], roles: vec![],
timeout: None, timeout: None,
can_publish: false, can_publish: None,
can_receive: false, can_receive: None,
} }
} }
} }
@@ -199,6 +201,8 @@ impl Member {
FieldsMember::Nickname => self.nickname = None, FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(), FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None, FieldsMember::Timeout => self.timeout = None,
FieldsMember::CanReceive => self.can_receive = None,
FieldsMember::CanPublish => self.can_publish = None
} }
} }

View File

@@ -173,6 +173,8 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Nickname => "nickname", FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles", FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout", FieldsMember::Timeout => "timeout",
FieldsMember::CanPublish => "can_publish",
FieldsMember::CanReceive => "can_receive"
}) })
} }
} }

View File

@@ -690,6 +690,8 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname, crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles, crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout, crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::CanReceive => FieldsMember::CanReceive,
crate::FieldsMember::CanPublish => FieldsMember::CanPublish,
} }
} }
} }
@@ -701,6 +703,8 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname, FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles, FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout, FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::CanReceive => crate::FieldsMember::CanReceive,
FieldsMember::CanPublish => crate::FieldsMember::CanPublish,
} }
} }
} }

View File

@@ -179,6 +179,22 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
} }
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_publish.unwrap_or(true)
} else {
false
}
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_receive.unwrap_or(true)
} else {
false
}
}
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel

View File

@@ -60,11 +60,11 @@ auto_derived_partial!(
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted /// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "if_false")] #[serde(skip_serializing_if = "Option::is_none")]
pub can_publish: bool, pub can_publish: Option<bool>,
/// Whether the member is server-wide voice deafened /// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "if_false")] #[serde(skip_serializing_if = "Option::is_none")]
pub can_receive: bool, pub can_receive: Option<bool>,
}, },
"PartialMember" "PartialMember"
); );
@@ -85,6 +85,8 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
CanReceive,
CanPublish,
} }
/// Member removal intention /// Member removal intention

View File

@@ -61,6 +61,14 @@ pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) ->
permissions.apply(role_override); permissions.apply(role_override);
} }
if !query.do_we_have_publish_overwrites().await {
permissions.revoke(ChannelPermission::Speak as u64);
}
if !query.do_we_have_receive_overwrites().await {
permissions.revoke(ChannelPermission::Listen as u64);
}
if query.are_we_timed_out().await { if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT); permissions.restrict(*ALLOW_IN_TIMEOUT);
} }

View File

@@ -89,6 +89,8 @@ pub enum ChannelPermission {
DeafenMembers = 1 << 34, DeafenMembers = 1 << 34,
/// Move members between voice channels /// Move members between voice channels
MoveMembers = 1 << 35, MoveMembers = 1 << 35,
/// Listen to other users
Listen = 1 << 36,
// * Misc. permissions // * Misc. permissions
// % Bits 36 to 52: free area // % Bits 36 to 52: free area
@@ -124,7 +126,8 @@ pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
+ ChannelPermission::SendEmbeds + ChannelPermission::SendEmbeds
+ ChannelPermission::UploadFiles + ChannelPermission::UploadFiles
+ ChannelPermission::Connect + ChannelPermission::Connect
+ ChannelPermission::Speak, + ChannelPermission::Speak
+ ChannelPermission::Listen
) )
}); });

View File

@@ -64,6 +64,14 @@ async fn validate_user_permissions() {
unreachable!() unreachable!()
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::DirectMessage ChannelType::DirectMessage
} }
@@ -153,6 +161,14 @@ async fn validate_group_permissions() {
unreachable!() unreachable!()
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::Group ChannelType::Group
} }
@@ -254,6 +270,14 @@ async fn validate_server_permissions() {
false false
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel ChannelType::ServerChannel
} }
@@ -346,6 +370,14 @@ async fn validate_timed_out_member() {
true true
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel ChannelType::ServerChannel
} }

View File

@@ -39,6 +39,12 @@ pub trait PermissionQuery {
/// Is our perspective user timed out on this server? /// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool; async fn are_we_timed_out(&mut self) -> bool;
/// Is the member muted?
async fn do_we_have_publish_overwrites(&mut self) -> bool;
/// Is the member deafend?
async fn do_we_have_receive_overwrites(&mut self) -> bool;
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel

View File

@@ -28,3 +28,6 @@ schemars = { version = "0.8.8", optional = true }
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
revolt_rocket_okapi = { version = "0.9.1", optional = true } revolt_rocket_okapi = { version = "0.9.1", optional = true }
revolt_okapi = { version = "0.9.1", optional = true } revolt_okapi = { version = "0.9.1", optional = true }
# utilities
log = "0.4"

View File

@@ -181,9 +181,11 @@ pub trait ToRevoltError<T> {
fn to_internal_error(self) -> Result<T, Error>; fn to_internal_error(self) -> Result<T, Error>;
} }
impl<T, E> ToRevoltError<T> for Result<T, E> { impl<T, E: std::fmt::Debug> ToRevoltError<T> for Result<T, E> {
fn to_internal_error(self) -> Result<T, Error> { fn to_internal_error(self) -> Result<T, Error> {
self.map_err(|_| { self
.inspect_err(|e| log::error!("{e:?}"))
.map_err(|_| {
let loc = Location::caller(); let loc = Location::caller();
Error { Error {

View File

@@ -1,5 +1,5 @@
use livekit_api::{access_token::{AccessToken, VideoGrants}, services::room::{CreateRoomOptions, RoomClient}}; use livekit_api::{access_token::{AccessToken, VideoGrants}, services::room::{CreateRoomOptions, RoomClient, UpdateParticipantOptions}};
use livekit_protocol::Room; use livekit_protocol::{ParticipantInfo, ParticipantPermission, Room};
use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands}; use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands};
use revolt_database::{Channel, User}; use revolt_database::{Channel, User};
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState}; use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
@@ -31,6 +31,22 @@ pub async fn raise_if_in_voice(user: &User, target: &str) -> Result<()> {
} }
} }
pub async fn get_user_voice_channel_in_server(user_id: &str, server_id: &str) -> Result<Option<String>> {
let mut conn = get_connection()
.await
.to_internal_error()?;
let unique_key = format!(
"{}-{}",
user_id,
server_id
);
conn.get::<&str, Option<String>>(&unique_key)
.await
.to_internal_error()
}
pub fn get_allowed_sources(permissions: PermissionValue) -> Vec<String> { pub fn get_allowed_sources(permissions: PermissionValue) -> Vec<String> {
let mut allowed_sources = Vec::new(); let mut allowed_sources = Vec::new();
@@ -67,6 +83,7 @@ pub async fn create_voice_state(channel_id: &str, server_id: Option<&str>, user_
Pipeline::new() Pipeline::new()
.sadd(format!("vc-members-{channel_id}"), user_id) .sadd(format!("vc-members-{channel_id}"), user_id)
.sadd(format!("vc-{user_id}"), channel_id) .sadd(format!("vc-{user_id}"), channel_id)
.set(&unique_key, channel_id)
.set(format!("can_publish-{unique_key}"), voice_state.can_publish) .set(format!("can_publish-{unique_key}"), voice_state.can_publish)
.set(format!("can_receive-{unique_key}"), voice_state.can_receive) .set(format!("can_receive-{unique_key}"), voice_state.can_receive)
.set(format!("screensharing-{unique_key}"), voice_state.screensharing) .set(format!("screensharing-{unique_key}"), voice_state.screensharing)
@@ -96,6 +113,7 @@ pub async fn delete_voice_state(channel_id: &str, server_id: Option<&str>, user_
format!("can_receive-{unique_key}"), format!("can_receive-{unique_key}"),
format!("screensharing-{unique_key}"), format!("screensharing-{unique_key}"),
format!("camera-{unique_key}"), format!("camera-{unique_key}"),
unique_key.clone(),
]) ])
.query_async(&mut get_connection() .query_async(&mut get_connection()
.await .await
@@ -243,6 +261,7 @@ impl VoiceClient {
.with_grants(VideoGrants { .with_grants(VideoGrants {
room_join: true, room_join: true,
can_publish_sources: allowed_sources, can_publish_sources: allowed_sources,
can_subscribe: permissions.has_channel_permission(ChannelPermission::Listen),
room: channel.id().to_string(), room: channel.id().to_string(),
..Default::default() ..Default::default()
}) })
@@ -261,6 +280,21 @@ impl VoiceClient {
..Default::default() ..Default::default()
}) })
.await .await
.map_err(|_| create_error!(InternalError)) .to_internal_error()
}
pub async fn update_permissions(&self, user: &User, channel_id: &str, new_permissions: ParticipantPermission) -> Result<ParticipantInfo> {
self.rooms
.update_participant(
channel_id,
&user.id,
UpdateParticipantOptions {
permission: Some(new_permissions),
name: "".to_string(),
metadata: "".to_string(),
},
)
.await
.to_internal_error()
} }
} }

View File

@@ -105,8 +105,8 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
url: config.hosts.january, url: config.hosts.january,
}, },
livekit: VoiceFeature { livekit: VoiceFeature {
enabled: !config.api.livekit.url.is_empty(), enabled: !config.hosts.livekit.is_empty(),
url: config.api.livekit.url.to_string(), url: config.hosts.livekit.to_string(),
}, },
}, },
ws: config.hosts.events, ws: config.hosts.events,

View File

@@ -5,11 +5,11 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{ use revolt_permissions::{
calculate_channel_permissions, calculate_server_permissions, ChannelPermission, calculate_server_permissions, ChannelPermission,
}; };
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use revolt_voice::{delete_voice_state, get_user_voice_channel_in_server, VoiceClient};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
/// # Ban User /// # Ban User
@@ -19,6 +19,7 @@ use validator::Validate;
#[put("/<server>/bans/<target>", data = "<data>")] #[put("/<server>/bans/<target>", data = "<data>")]
pub async fn ban( pub async fn ban(
db: &State<Database>, db: &State<Database>,
voice: &State<VoiceClient>,
user: User, user: User,
server: Reference, server: Reference,
target: Reference, target: Reference,
@@ -57,6 +58,11 @@ pub async fn ban(
member member
.remove(db, &server, RemovalIntention::Ban, false) .remove(db, &server, RemovalIntention::Ban, false)
.await?; .await?;
// If the member is in a voice channel while banned kick them from the voice channel
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?
}
} }
ServerBan::create(db, &server, &target.id, data.reason) ServerBan::create(db, &server, &target.id, data.reason)

View File

@@ -7,10 +7,11 @@ use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands};
use revolt_database::{ use revolt_database::{
events::client::EventV1, util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, File, PartialMember, User events::client::EventV1, util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, File, PartialMember, User
}; };
use revolt_models::v0::{self, PartialUserVoiceState}; use revolt_models::v0::{self, FieldsMember, PartialUserVoiceState};
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result, ToRevoltError}; use revolt_result::{create_error, Result, ToRevoltError};
use revolt_voice::VoiceClient;
use rocket::{form::validate::Contains, serde::json::Json, State}; use rocket::{form::validate::Contains, serde::json::Json, State};
use validator::Validate; use validator::Validate;
@@ -20,7 +21,7 @@ use validator::Validate;
#[openapi(tag = "Server Members")] #[openapi(tag = "Server Members")]
#[patch("/<server>/members/<target>", data = "<data>")] #[patch("/<server>/members/<target>", data = "<data>")]
pub async fn edit( pub async fn edit(
room_client: &State<RoomClient>, voice_client: &State<VoiceClient>,
db: &State<Database>, db: &State<Database>,
user: User, user: User,
server: Reference, server: Reference,
@@ -37,6 +38,7 @@ pub async fn edit(
// Fetch server, target member and current permissions // Fetch server, target member and current permissions
let mut server = server.as_server(db).await?; let mut server = server.as_server(db).await?;
let mut member = target.as_member(db, &server.id).await?; let mut member = target.as_member(db, &server.id).await?;
let target_user = target.as_user(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user) let mut query = DatabasePermissionQuery::new(db, &user)
.server(&server) .server(&server)
.member(&member); .member(&member);
@@ -153,8 +155,8 @@ pub async fn edit(
roles, roles,
timeout, timeout,
remove, remove,
can_publish, mut can_publish,
can_receive, mut can_receive,
voice_channel voice_channel
} = data; } = data;
@@ -181,6 +183,10 @@ pub async fn edit(
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?); partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
} }
let remove_contains_voice = remove
.as_ref()
.map(|r| r.contains(FieldsMember::CanPublish) || r.contains(FieldsMember::CanReceive)).unwrap_or_default();
member member
.update( .update(
db, db,
@@ -191,7 +197,11 @@ pub async fn edit(
) )
.await?; .await?;
if can_publish.is_some() || can_receive.is_some() || voice_channel.is_some() { if can_publish.is_some() ||
can_receive.is_some() ||
voice_channel.is_some() ||
remove_contains_voice
{
let mut conn = get_connection().await.to_internal_error()?; let mut conn = get_connection().await.to_internal_error()?;
let unique_key = format!("{}-{}", &member.id.user, &member.id.server); let unique_key = format!("{}-{}", &member.id.user, &member.id.server);
@@ -206,6 +216,22 @@ pub async fn edit(
let mut pipeline = Pipeline::new(); let mut pipeline = Pipeline::new();
let mut new_perms = ParticipantPermission::default(); let mut new_perms = ParticipantPermission::default();
if remove_contains_voice {
let mut query = DatabasePermissionQuery::new(db, &target_user)
.server(&server)
.member(&member);
let permissions = calculate_server_permissions(&mut query).await;
if !permissions.has_channel_permission(ChannelPermission::Speak) {
can_publish = Some(false)
}
if !permissions.has_channel_permission(ChannelPermission::Listen) {
can_receive = Some(false)
}
}
if let Some(can_publish) = can_publish { if let Some(can_publish) = can_publish {
pipeline.set( pipeline.set(
format!("can_publish-{}", unique_key), format!("can_publish-{}", unique_key),
@@ -235,18 +261,7 @@ pub async fn edit(
.await .await
.to_internal_error()?; .to_internal_error()?;
room_client voice_client.update_permissions(&user, &channel, new_perms).await?;
.update_participant(
&channel,
&member.id.user,
UpdateParticipantOptions {
permission: Some(new_perms),
name: "".to_string(),
metadata: "".to_string(),
},
)
.await
.to_internal_error()?;
EventV1::UserVoiceStateUpdate { EventV1::UserVoiceStateUpdate {
id: member.id.user.clone(), id: member.id.user.clone(),

View File

@@ -96,7 +96,7 @@ async fn ingress(db: &State<Database>, voice_client: &State<VoiceClient>, body:
EventV1::UserVoiceStateUpdate { EventV1::UserVoiceStateUpdate {
id: user_id.clone(), id: user_id.clone(),
channel_id: channel_id.clone(), channel_id: channel_id.clone(),
data: partial, data: partial
} }
.p(channel_id.clone()) .p(channel_id.clone())
.await; .await;