diff --git a/Cargo.lock b/Cargo.lock index 69e98842..c32da194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4182,6 +4182,7 @@ dependencies = [ name = "revolt-result" version = "0.7.1" dependencies = [ + "log", "revolt_okapi", "revolt_rocket_okapi", "rocket", diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index 473c5cb3..f9222bfe 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -236,7 +236,7 @@ pub enum EventV1 { UserVoiceStateUpdate { id: String, channel_id: String, - data: PartialUserVoiceState + data: PartialUserVoiceState, } } diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs index fd068aee..fb9c4417 100644 --- a/crates/core/database/src/models/server_members/model.rs +++ b/crates/core/database/src/models/server_members/model.rs @@ -32,11 +32,11 @@ auto_derived_partial!( pub timeout: Option, /// Whether the member is server-wide voice muted - #[serde(skip_serializing_if = "if_false")] - pub can_publish: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub can_publish: Option, /// Whether the member is server-wide voice deafened - #[serde(skip_serializing_if = "if_false")] - pub can_receive: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub can_receive: Option, }, "PartialMember" ); @@ -57,6 +57,8 @@ auto_derived!( Avatar, Roles, Timeout, + CanReceive, + CanPublish, } /// Member removal intention @@ -76,8 +78,8 @@ impl Default for Member { avatar: None, roles: vec![], timeout: None, - can_publish: false, - can_receive: false, + can_publish: None, + can_receive: None, } } } @@ -199,6 +201,8 @@ impl Member { FieldsMember::Nickname => self.nickname = None, FieldsMember::Roles => self.roles.clear(), FieldsMember::Timeout => self.timeout = None, + FieldsMember::CanReceive => self.can_receive = None, + FieldsMember::CanPublish => self.can_publish = None } } diff --git a/crates/core/database/src/models/server_members/ops/mongodb.rs b/crates/core/database/src/models/server_members/ops/mongodb.rs index 8ab52679..1c511690 100644 --- a/crates/core/database/src/models/server_members/ops/mongodb.rs +++ b/crates/core/database/src/models/server_members/ops/mongodb.rs @@ -173,6 +173,8 @@ impl IntoDocumentPath for FieldsMember { FieldsMember::Nickname => "nickname", FieldsMember::Roles => "roles", FieldsMember::Timeout => "timeout", + FieldsMember::CanPublish => "can_publish", + FieldsMember::CanReceive => "can_receive" }) } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 042f10c8..de7505be 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -690,6 +690,8 @@ impl From for FieldsMember { crate::FieldsMember::Nickname => FieldsMember::Nickname, crate::FieldsMember::Roles => FieldsMember::Roles, crate::FieldsMember::Timeout => FieldsMember::Timeout, + crate::FieldsMember::CanReceive => FieldsMember::CanReceive, + crate::FieldsMember::CanPublish => FieldsMember::CanPublish, } } } @@ -701,6 +703,8 @@ impl From for crate::FieldsMember { FieldsMember::Nickname => crate::FieldsMember::Nickname, FieldsMember::Roles => crate::FieldsMember::Roles, FieldsMember::Timeout => crate::FieldsMember::Timeout, + FieldsMember::CanReceive => crate::FieldsMember::CanReceive, + FieldsMember::CanPublish => crate::FieldsMember::CanPublish, } } } diff --git a/crates/core/database/src/util/permissions.rs b/crates/core/database/src/util/permissions.rs index 3adb2ec3..08fe3a9e 100644 --- a/crates/core/database/src/util/permissions.rs +++ b/crates/core/database/src/util/permissions.rs @@ -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 /// Get the type of the channel diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index 0ed31fe0..d1cbe965 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -60,11 +60,11 @@ auto_derived_partial!( pub timeout: Option, /// Whether the member is server-wide voice muted - #[serde(skip_serializing_if = "if_false")] - pub can_publish: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub can_publish: Option, /// Whether the member is server-wide voice deafened - #[serde(skip_serializing_if = "if_false")] - pub can_receive: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub can_receive: Option, }, "PartialMember" ); @@ -85,6 +85,8 @@ auto_derived!( Avatar, Roles, Timeout, + CanReceive, + CanPublish, } /// Member removal intention diff --git a/crates/core/permissions/src/impl.rs b/crates/core/permissions/src/impl.rs index 9975eeda..f175d6bd 100644 --- a/crates/core/permissions/src/impl.rs +++ b/crates/core/permissions/src/impl.rs @@ -61,6 +61,14 @@ pub async fn calculate_server_permissions(query: &mut P) -> 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 { permissions.restrict(*ALLOW_IN_TIMEOUT); } diff --git a/crates/core/permissions/src/models/channel.rs b/crates/core/permissions/src/models/channel.rs index 17762b1e..0b2867ae 100644 --- a/crates/core/permissions/src/models/channel.rs +++ b/crates/core/permissions/src/models/channel.rs @@ -89,6 +89,8 @@ pub enum ChannelPermission { DeafenMembers = 1 << 34, /// Move members between voice channels MoveMembers = 1 << 35, + /// Listen to other users + Listen = 1 << 36, // * Misc. permissions // % Bits 36 to 52: free area @@ -124,7 +126,8 @@ pub static DEFAULT_PERMISSION: Lazy = Lazy::new(|| { + ChannelPermission::SendEmbeds + ChannelPermission::UploadFiles + ChannelPermission::Connect - + ChannelPermission::Speak, + + ChannelPermission::Speak + + ChannelPermission::Listen ) }); diff --git a/crates/core/permissions/src/test.rs b/crates/core/permissions/src/test.rs index e4dadd48..93e1dea2 100644 --- a/crates/core/permissions/src/test.rs +++ b/crates/core/permissions/src/test.rs @@ -64,6 +64,14 @@ async fn validate_user_permissions() { 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 { ChannelType::DirectMessage } @@ -153,6 +161,14 @@ async fn validate_group_permissions() { 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 { ChannelType::Group } @@ -254,6 +270,14 @@ async fn validate_server_permissions() { 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 { ChannelType::ServerChannel } @@ -346,6 +370,14 @@ async fn validate_timed_out_member() { 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 { ChannelType::ServerChannel } diff --git a/crates/core/permissions/src/trait.rs b/crates/core/permissions/src/trait.rs index d4a7c3d3..96b42caf 100644 --- a/crates/core/permissions/src/trait.rs +++ b/crates/core/permissions/src/trait.rs @@ -39,6 +39,12 @@ pub trait PermissionQuery { /// Is our perspective user timed out on this server? 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 /// Get the type of the channel diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index c7f1ec04..fae07bd6 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -28,3 +28,6 @@ schemars = { version = "0.8.8", optional = true } rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } revolt_rocket_okapi = { version = "0.9.1", optional = true } revolt_okapi = { version = "0.9.1", optional = true } + +# utilities +log = "0.4" \ No newline at end of file diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index e3bdb4f2..713f5a18 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -181,9 +181,11 @@ pub trait ToRevoltError { fn to_internal_error(self) -> Result; } -impl ToRevoltError for Result { +impl ToRevoltError for Result { fn to_internal_error(self) -> Result { - self.map_err(|_| { + self + .inspect_err(|e| log::error!("{e:?}")) + .map_err(|_| { let loc = Location::caller(); Error { diff --git a/crates/core/voice/src/lib.rs b/crates/core/voice/src/lib.rs index 226364d5..0e0c714b 100644 --- a/crates/core/voice/src/lib.rs +++ b/crates/core/voice/src/lib.rs @@ -1,5 +1,5 @@ -use livekit_api::{access_token::{AccessToken, VideoGrants}, services::room::{CreateRoomOptions, RoomClient}}; -use livekit_protocol::Room; +use livekit_api::{access_token::{AccessToken, VideoGrants}, services::room::{CreateRoomOptions, RoomClient, UpdateParticipantOptions}}; +use livekit_protocol::{ParticipantInfo, ParticipantPermission, Room}; use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands}; use revolt_database::{Channel, User}; 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> { + let mut conn = get_connection() + .await + .to_internal_error()?; + + let unique_key = format!( + "{}-{}", + user_id, + server_id + ); + + conn.get::<&str, Option>(&unique_key) + .await + .to_internal_error() +} + pub fn get_allowed_sources(permissions: PermissionValue) -> Vec { 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() .sadd(format!("vc-members-{channel_id}"), user_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_receive-{unique_key}"), voice_state.can_receive) .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!("screensharing-{unique_key}"), format!("camera-{unique_key}"), + unique_key.clone(), ]) .query_async(&mut get_connection() .await @@ -243,6 +261,7 @@ impl VoiceClient { .with_grants(VideoGrants { room_join: true, can_publish_sources: allowed_sources, + can_subscribe: permissions.has_channel_permission(ChannelPermission::Listen), room: channel.id().to_string(), ..Default::default() }) @@ -261,6 +280,21 @@ impl VoiceClient { ..Default::default() }) .await - .map_err(|_| create_error!(InternalError)) + .to_internal_error() + } + + pub async fn update_permissions(&self, user: &User, channel_id: &str, new_permissions: ParticipantPermission) -> Result { + self.rooms + .update_participant( + channel_id, + &user.id, + UpdateParticipantOptions { + permission: Some(new_permissions), + name: "".to_string(), + metadata: "".to_string(), + }, + ) + .await + .to_internal_error() } } \ No newline at end of file diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index d8677f8b..63a731fa 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -105,8 +105,8 @@ pub async fn root() -> Result> { url: config.hosts.january, }, livekit: VoiceFeature { - enabled: !config.api.livekit.url.is_empty(), - url: config.api.livekit.url.to_string(), + enabled: !config.hosts.livekit.is_empty(), + url: config.hosts.livekit.to_string(), }, }, ws: config.hosts.events, diff --git a/crates/delta/src/routes/servers/ban_create.rs b/crates/delta/src/routes/servers/ban_create.rs index 8003e79e..09666ac4 100644 --- a/crates/delta/src/routes/servers/ban_create.rs +++ b/crates/delta/src/routes/servers/ban_create.rs @@ -5,11 +5,11 @@ use revolt_database::{ use revolt_models::v0; use revolt_permissions::{ - calculate_channel_permissions, calculate_server_permissions, ChannelPermission, + calculate_server_permissions, ChannelPermission, }; 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 serde::{Deserialize, Serialize}; use validator::Validate; /// # Ban User @@ -19,6 +19,7 @@ use validator::Validate; #[put("//bans/", data = "")] pub async fn ban( db: &State, + voice: &State, user: User, server: Reference, target: Reference, @@ -57,6 +58,11 @@ pub async fn ban( member .remove(db, &server, RemovalIntention::Ban, false) .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) diff --git a/crates/delta/src/routes/servers/member_edit.rs b/crates/delta/src/routes/servers/member_edit.rs index 3506e955..218eddbc 100644 --- a/crates/delta/src/routes/servers/member_edit.rs +++ b/crates/delta/src/routes/servers/member_edit.rs @@ -7,10 +7,11 @@ use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands}; use revolt_database::{ 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_result::{create_error, Result, ToRevoltError}; +use revolt_voice::VoiceClient; use rocket::{form::validate::Contains, serde::json::Json, State}; use validator::Validate; @@ -20,7 +21,7 @@ use validator::Validate; #[openapi(tag = "Server Members")] #[patch("//members/", data = "")] pub async fn edit( - room_client: &State, + voice_client: &State, db: &State, user: User, server: Reference, @@ -37,6 +38,7 @@ pub async fn edit( // Fetch server, target member and current permissions let mut server = server.as_server(db).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) .server(&server) .member(&member); @@ -153,8 +155,8 @@ pub async fn edit( roles, timeout, remove, - can_publish, - can_receive, + mut can_publish, + mut can_receive, voice_channel } = data; @@ -181,6 +183,10 @@ pub async fn edit( 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 .update( db, @@ -191,7 +197,11 @@ pub async fn edit( ) .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 unique_key = format!("{}-{}", &member.id.user, &member.id.server); @@ -206,6 +216,22 @@ pub async fn edit( let mut pipeline = Pipeline::new(); 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 { pipeline.set( format!("can_publish-{}", unique_key), @@ -235,18 +261,7 @@ pub async fn edit( .await .to_internal_error()?; - room_client - .update_participant( - &channel, - &member.id.user, - UpdateParticipantOptions { - permission: Some(new_perms), - name: "".to_string(), - metadata: "".to_string(), - }, - ) - .await - .to_internal_error()?; + voice_client.update_permissions(&user, &channel, new_perms).await?; EventV1::UserVoiceStateUpdate { id: member.id.user.clone(), diff --git a/crates/voice-ingress/src/main.rs b/crates/voice-ingress/src/main.rs index 4ff6d360..f4c9f877 100644 --- a/crates/voice-ingress/src/main.rs +++ b/crates/voice-ingress/src/main.rs @@ -96,7 +96,7 @@ async fn ingress(db: &State, voice_client: &State, body: EventV1::UserVoiceStateUpdate { id: user_id.clone(), channel_id: channel_id.clone(), - data: partial, + data: partial } .p(channel_id.clone()) .await;