fix: store server id in redis and in room metadata to be able to delete voice state in all scenarios (#656)

* fix: store server id in redis values and in room metadata to be able to fully delete voice state in all scenarios

* fix: undo logging change
This commit is contained in:
Angelo Kontaxis
2026-03-10 22:14:32 +00:00
committed by GitHub
parent b0b728fb0d
commit 49c6289580
15 changed files with 322 additions and 154 deletions

View File

@@ -4,7 +4,7 @@ use futures::future::join_all;
use revolt_database::{ use revolt_database::{
events::client::{EventV1, ReadyPayloadFields}, events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery, util::permissions::DatabasePermissionQuery,
voice::get_channel_voice_state, voice::{get_channel_voice_state, UserVoiceChannel},
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus, Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -167,7 +167,9 @@ impl State {
| Channel::TextChannel { voice: Some(_), .. } | Channel::TextChannel { voice: Some(_), .. }
) )
}) { }) {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await { if let Ok(Some(voice_state)) =
get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
{
if let Some(server) = channel.server() { if let Some(server) = channel.server() {
let set = voice_state_server_members let set = voice_state_server_members
.entry(server.to_string()) .entry(server.to_string())

View File

@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::{ use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
Database, File, Server, SystemMessage, User, Server, SystemMessage, User,
}; };
fn default_true() -> bool { fn default_true() -> bool {
@@ -45,7 +45,6 @@ auto_derived_partial!(
/// Whether the member is server-wide voice deafened /// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "is_true", default = "default_true")] #[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_receive: bool, pub can_receive: bool,
// This value only exists in the database, not the models. // This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries. // If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp> // pub pending_deletion_at: Option<Timestamp>
@@ -153,7 +152,11 @@ impl Member {
#[cfg(feature = "voice")] #[cfg(feature = "voice")]
for channel in &channels { for channel in &channels {
if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(channel).await { if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(
&crate::voice::UserVoiceChannel::from_channel(channel),
)
.await
{
voice_states.push(voice_state) voice_states.push(voice_state)
} }
} }
@@ -175,7 +178,7 @@ impl Member {
.map(|channel| channel.into()) .map(|channel| channel.into())
.collect(), .collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(), emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
voice_states voice_states,
} }
.private(user.id.clone()) .private(user.id.clone())
.await; .await;
@@ -225,14 +228,14 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) { pub fn remove_field(&mut self, field: &FieldsMember) {
match field { match field {
FieldsMember::JoinedAt => {}, FieldsMember::JoinedAt => {}
FieldsMember::Avatar => self.avatar = None, FieldsMember::Avatar => self.avatar = None,
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 = true, FieldsMember::CanReceive => self.can_receive = true,
FieldsMember::CanPublish => self.can_publish = true, FieldsMember::CanPublish => self.can_publish = true,
FieldsMember::VoiceChannel => {}, FieldsMember::VoiceChannel => {}
} }
} }

View File

@@ -1,3 +1,5 @@
use std::fmt::{Display, Write};
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
models::{Channel, User}, models::{Channel, User},
@@ -6,7 +8,11 @@ use crate::{
}; };
use iso8601_timestamp::{Duration, Timestamp}; use iso8601_timestamp::{Duration, Timestamp};
use livekit_protocol::ParticipantPermission; use livekit_protocol::ParticipantPermission;
use redis_kiss::{get_connection as _get_connection, redis::Pipeline, AsyncCommands, Conn}; use redis_kiss::{
get_connection as _get_connection,
redis::{FromRedisValue, Pipeline, RedisError, RedisWrite, ToRedisArgs, Value},
AsyncCommands, Conn,
};
use revolt_config::FeaturesLimits; use revolt_config::FeaturesLimits;
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState}; use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
@@ -21,13 +27,13 @@ async fn get_connection() -> Result<Conn> {
.map_err(|_| create_error!(InternalError)) .map_err(|_| create_error!(InternalError))
} }
pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> { pub async fn raise_if_in_voice(user: &User, channel: &UserVoiceChannel) -> Result<()> {
let mut conn = get_connection().await?; let mut conn = get_connection().await?;
if user.bot.is_some() { if user.bot.is_some() {
// bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to // bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to
if conn if conn
.sismember(format!("vc:{}", &user.id), channel_id) .sismember(format!("vc:{}", &user.id), channel)
.await .await
.to_internal_error()? .to_internal_error()?
{ {
@@ -45,31 +51,31 @@ pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
Ok(()) Ok(())
} }
pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> { pub async fn set_channel_node(channel_id: &str, node: &str) -> Result<()> {
get_connection() get_connection()
.await? .await?
.set(format!("node:{channel}"), node) .set(format!("node:{channel_id}"), node)
.await .await
.to_internal_error() .to_internal_error()
} }
pub async fn get_channel_node(channel: &str) -> Result<Option<String>> { pub async fn get_channel_node(channel_id: &str) -> Result<Option<String>> {
get_connection() get_connection()
.await? .await?
.get(format!("node:{channel}")) .get(format!("node:{channel_id}"))
.await .await
.to_internal_error() .to_internal_error()
} }
pub async fn delete_channel_node(channel: &str) -> Result<()> { pub async fn delete_channel_node(channel_id: &str) -> Result<()> {
get_connection() get_connection()
.await? .await?
.del(format!("node:{channel}")) .del(format!("node:{channel_id}"))
.await .await
.to_internal_error() .to_internal_error()
} }
pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> { pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<UserVoiceChannel>> {
get_connection() get_connection()
.await? .await?
.smembers(format!("vc:{user_id}")) .smembers(format!("vc:{user_id}"))
@@ -78,14 +84,14 @@ pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> {
} }
pub async fn set_user_moved_from_voice( pub async fn set_user_moved_from_voice(
old_channel: &str, old_channel_id: &str,
new_channel: &str, new_channel: &UserVoiceChannel,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
get_connection() get_connection()
.await? .await?
.set_ex( .set_ex(
format!("moved_from:{user_id}:{old_channel}"), format!("moved_from:{user_id}:{old_channel_id}"),
new_channel, new_channel,
10, 10,
) )
@@ -102,18 +108,25 @@ pub async fn get_user_moved_from_voice(channel_id: &str, user_id: &str) -> Resul
} }
pub async fn set_user_moved_to_voice( pub async fn set_user_moved_to_voice(
new_channel: &str, new_channel_id: &str,
old_channel: &str, old_channel: &UserVoiceChannel,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
get_connection() get_connection()
.await? .await?
.set_ex(format!("moved_to:{user_id}:{new_channel}"), old_channel, 10) .set_ex(
format!("moved_to:{user_id}:{new_channel_id}"),
old_channel,
10,
)
.await .await
.to_internal_error() .to_internal_error()
} }
pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> { pub async fn get_user_moved_to_voice(
channel_id: &str,
user_id: &str,
) -> Result<Option<UserVoiceChannel>> {
get_connection() get_connection()
.await? .await?
.get_del(format!("moved_to:{user_id}:{channel_id}")) .get_del(format!("moved_to:{user_id}:{channel_id}"))
@@ -121,10 +134,10 @@ pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<
.to_internal_error() .to_internal_error()
} }
pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result<bool> { pub async fn is_in_voice_channel(user_id: &str, channel: &UserVoiceChannel) -> Result<bool> {
get_connection() get_connection()
.await? .await?
.sismember(format!("vc:{user_id}"), channel_id) .sismember(format!("vc:{user_id}"), channel)
.await .await
.to_internal_error() .to_internal_error()
} }
@@ -158,12 +171,15 @@ pub fn get_allowed_sources(
} }
pub async fn create_voice_state( pub async fn create_voice_state(
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
user_id: &str, user_id: &str,
joined_at: Timestamp, joined_at: Timestamp,
) -> Result<UserVoiceState> { ) -> Result<UserVoiceState> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); let unique_key = format!(
"{}:{}",
&user_id,
channel.server_id.as_ref().unwrap_or(&channel.id)
);
let voice_state = UserVoiceState { let voice_state = UserVoiceState {
joined_at, joined_at,
@@ -175,9 +191,9 @@ pub async fn create_voice_state(
}; };
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)
.set(&unique_key, channel_id) .set(&unique_key, &channel.id)
.set( .set(
format!("joined_at:{unique_key}"), format!("joined_at:{unique_key}"),
joined_at joined_at
@@ -204,16 +220,16 @@ pub async fn create_voice_state(
Ok(voice_state) Ok(voice_state)
} }
pub async fn delete_voice_state( pub async fn delete_voice_state(channel: &UserVoiceChannel, user_id: &str) -> Result<()> {
channel_id: &str, let unique_key = format!(
server_id: Option<&str>, "{}:{}",
user_id: &str, &user_id,
) -> Result<()> { channel.server_id.as_ref().unwrap_or(&channel.id)
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); );
Pipeline::new() Pipeline::new()
.srem(format!("vc_members:{channel_id}"), user_id) .srem(format!("vc_members:{}", &channel.id), user_id)
.srem(format!("vc:{user_id}"), channel_id) .srem(format!("vc:{user_id}"), channel)
.del(&[ .del(&[
format!("joined_at:{unique_key}"), format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"), format!("is_publishing:{unique_key}"),
@@ -228,20 +244,19 @@ pub async fn delete_voice_state(
} }
pub async fn delete_channel_voice_state( pub async fn delete_channel_voice_state(
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
user_ids: &[String], user_ids: &[String],
) -> Result<()> { ) -> Result<()> {
let parent_id = server_id.unwrap_or(channel_id); let parent_id = channel.server_id.as_ref().unwrap_or(&channel.id);
let mut pipeline = Pipeline::new(); let mut pipeline = Pipeline::new();
pipeline.del(format!("vc_members:{channel_id}")); pipeline.del(format!("vc_members:{}", &channel.id));
pipeline.del(format!("node:{channel_id}")); pipeline.del(format!("node:{}", &channel.id));
for user_id in user_ids { for user_id in user_ids {
let unique_key = format!("{user_id}:{parent_id}"); let unique_key = format!("{user_id}:{parent_id}");
pipeline.srem(format!("vc:{user_id}"), channel_id).del(&[ pipeline.srem(format!("vc:{user_id}"), channel).del(&[
format!("joined_at:{unique_key}"), format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"), format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"), format!("is_receiving:{unique_key}"),
@@ -258,8 +273,7 @@ pub async fn delete_channel_voice_state(
} }
pub async fn update_voice_state_tracks( pub async fn update_voice_state_tracks(
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
user_id: &str, user_id: &str,
added: bool, added: bool,
track: i32, track: i32,
@@ -284,18 +298,21 @@ pub async fn update_voice_state_tracks(
_ => unreachable!(), _ => unreachable!(),
}; };
update_voice_state(channel_id, server_id, user_id, &partial).await?; update_voice_state(channel, user_id, &partial).await?;
Ok(partial) Ok(partial)
} }
pub async fn update_voice_state( pub async fn update_voice_state(
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
user_id: &str, user_id: &str,
partial: &PartialUserVoiceState, partial: &PartialUserVoiceState,
) -> Result<()> { ) -> Result<()> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); let unique_key = format!(
"{}:{}",
&user_id,
channel.server_id.as_ref().unwrap_or(&channel.id)
);
let mut pipeline = Pipeline::new(); let mut pipeline = Pipeline::new();
@@ -321,21 +338,24 @@ pub async fn update_voice_state(
.to_internal_error() .to_internal_error()
} }
pub async fn get_voice_channel_members(channel_id: &str) -> Result<Option<Vec<String>>> { pub async fn get_voice_channel_members(channel: &UserVoiceChannel) -> Result<Option<Vec<String>>> {
get_connection() get_connection()
.await? .await?
.smembers::<_, Option<Vec<String>>>(format!("vc_members:{channel_id}")) .smembers::<_, Option<Vec<String>>>(format!("vc_members:{}", &channel.id))
.await .await
.to_internal_error() .to_internal_error()
.map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) })) .map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) }))
} }
pub async fn get_voice_state( pub async fn get_voice_state(
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
user_id: &str, user_id: &str,
) -> Result<Option<UserVoiceState>> { ) -> Result<Option<UserVoiceState>> {
let unique_key = format!("{}:{}", user_id, server_id.unwrap_or(channel_id)); let unique_key = format!(
"{}:{}",
&user_id,
channel.server_id.as_ref().unwrap_or(&channel.id)
);
let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection() let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection()
.await? .await?
@@ -376,21 +396,21 @@ pub async fn get_voice_state(
} }
} }
pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::ChannelVoiceState>> { pub async fn get_channel_voice_state(
let members = get_voice_channel_members(channel.id()).await?; channel: &UserVoiceChannel,
) -> Result<Option<v0::ChannelVoiceState>> {
let server = channel.server(); let members = get_voice_channel_members(channel).await?;
if let Some(members) = members { if let Some(members) = members {
let mut participants = Vec::with_capacity(members.len()); let mut participants = Vec::with_capacity(members.len());
for user_id in members { for user_id in members {
if let Some(voice_state) = get_voice_state(channel.id(), server, &user_id).await? { if let Some(voice_state) = get_voice_state(channel, &user_id).await? {
participants.push(voice_state); participants.push(voice_state);
} else { } else {
log::info!("Voice state not found but member in voice channel members, removing."); log::info!("Voice state not found but member in voice channel members, removing.");
delete_voice_state(channel.id(), server, &user_id).await?; delete_voice_state(channel, &user_id).await?;
} }
} }
@@ -398,7 +418,7 @@ pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::Cha
participants.shrink_to_fit(); participants.shrink_to_fit();
Ok(Some(v0::ChannelVoiceState { Ok(Some(v0::ChannelVoiceState {
id: channel.id().to_string(), id: channel.id.clone(),
participants, participants,
})) }))
} else { } else {
@@ -406,12 +426,12 @@ pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::Cha
} }
} }
pub async fn move_user(user: &str, from: &str, to: &str) -> Result<()> { pub async fn move_user(user: &str, from_channel_id: &str, to_channel_id: &str) -> Result<()> {
get_connection() get_connection()
.await? .await?
.smove( .smove(
format!("vc-members-{from}"), format!("vc_members:{from_channel_id}"),
format!("vc-members-{to}"), format!("vc_members:{to_channel_id}"),
user, user,
) )
.await .await
@@ -425,11 +445,13 @@ pub async fn sync_voice_permissions(
server: Option<&Server>, server: Option<&Server>,
role_id: Option<&str>, role_id: Option<&str>,
) -> Result<()> { ) -> Result<()> {
let user_voice_channel = UserVoiceChannel::from_channel(channel);
let Some(node) = get_channel_node(channel.id()).await? else { let Some(node) = get_channel_node(channel.id()).await? else {
return Ok(()); return Ok(());
}; };
for user_id in get_voice_channel_members(channel.id()) for user_id in get_voice_channel_members(&user_voice_channel)
.await? .await?
.iter() .iter()
.flatten() .flatten()
@@ -469,7 +491,9 @@ pub async fn sync_user_voice_permissions(
.as_ref() .as_ref()
.is_none_or(|member| member.roles.iter().any(|r| r == role_id)) .is_none_or(|member| member.roles.iter().any(|r| r == role_id))
}) { }) {
let Some(voice_state) = get_voice_state(channel_id, server_id, &user.id).await? else { let user_voice_channel = UserVoiceChannel::from_channel(channel);
let Some(voice_state) = get_voice_state(&user_voice_channel, &user.id).await? else {
return Ok(()); return Ok(());
}; };
@@ -500,7 +524,7 @@ pub async fn sync_user_voice_permissions(
update_event.screensharing = voice_state.screensharing.then_some(can_video); update_event.screensharing = voice_state.screensharing.then_some(can_video);
update_event.is_publishing = voice_state.is_publishing.then_some(can_speak); update_event.is_publishing = voice_state.is_publishing.then_some(can_speak);
update_voice_state(channel_id, server_id, &user.id, &update_event).await?; update_voice_state(&user_voice_channel, &user.id, &update_event).await?;
voice_client voice_client
.update_permissions( .update_permissions(
@@ -579,46 +603,94 @@ pub async fn get_call_notification_recipients(
} }
pub async fn remove_user_from_voice_channels( pub async fn remove_user_from_voice_channels(
db: &Database,
voice_client: &VoiceClient, voice_client: &VoiceClient,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
for channel_id in get_user_voice_channels(user_id).await? { for channel in get_user_voice_channels(user_id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, user_id).await?; remove_user_from_voice_channel(voice_client, &channel, user_id).await?;
} }
Ok(()) Ok(())
} }
pub async fn remove_user_from_voice_channel( pub async fn remove_user_from_voice_channel(
db: &Database,
voice_client: &VoiceClient, voice_client: &VoiceClient,
channel_id: &str, channel: &UserVoiceChannel,
user_id: &str, user_id: &str,
) -> Result<()> { ) -> Result<()> {
if let Some(node) = get_channel_node(channel_id).await? { if let Some(node) = get_channel_node(&channel.id).await? {
let _ = voice_client.remove_user(&node, user_id, channel_id).await; let _ = voice_client.remove_user(&node, user_id, &channel.id).await;
} }
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; delete_voice_state(channel, user_id).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
Ok(()) Ok(())
} }
pub async fn delete_voice_channel( pub async fn delete_voice_channel(
voice_client: &VoiceClient, voice_client: &VoiceClient,
channel_id: &str, channel: &UserVoiceChannel,
server_id: Option<&str>,
) -> Result<()> { ) -> Result<()> {
if let Some(users) = get_voice_channel_members(channel_id).await? { if let Some(users) = get_voice_channel_members(channel).await? {
let node = get_channel_node(channel_id).await?.unwrap(); let node = get_channel_node(&channel.id).await?.unwrap();
voice_client.delete_room(&node, &channel.id).await?;
voice_client.delete_room(&node, channel_id).await?; delete_channel_voice_state(channel, &users).await?;
delete_channel_voice_state(channel_id, server_id, &users).await?;
}; };
Ok(()) Ok(())
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomMetadata {
pub server: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserVoiceChannel {
pub id: String,
pub server_id: Option<String>,
}
impl UserVoiceChannel {
pub fn from_string(input: String) -> Self {
let mut parts = input.splitn(2, '-');
Self {
id: parts.next().unwrap().to_string(),
server_id: parts.next().map(ToString::to_string),
}
}
pub fn from_channel(channel: &Channel) -> Self {
Self {
id: channel.id().to_string(),
server_id: channel.server().map(ToString::to_string),
}
}
}
impl Display for UserVoiceChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.id)?;
if let Some(server_id) = &self.server_id {
f.write_char('-')?;
f.write_str(server_id)?
};
Ok(())
}
}
impl ToRedisArgs for UserVoiceChannel {
fn write_redis_args<W: ?Sized + RedisWrite>(&self, out: &mut W) {
out.write_arg_fmt(self);
}
}
impl FromRedisValue for UserVoiceChannel {
fn from_redis_value(v: &Value) -> Result<Self, RedisError> {
String::from_redis_value(v).map(UserVoiceChannel::from_string)
}
}

View File

@@ -1,5 +1,6 @@
use crate::{ use crate::{
models::{Channel, User}, models::{Channel, User},
voice::RoomMetadata,
Database, Database,
}; };
use livekit_api::{ use livekit_api::{
@@ -102,11 +103,16 @@ impl VoiceClient {
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> { pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> {
let room = self.get_node(node)?; let room = self.get_node(node)?;
let metadata = RoomMetadata {
server: channel.server().map(|id| id.to_string()),
};
room.client room.client
.create_room( .create_room(
channel.id(), channel.id(),
CreateRoomOptions { CreateRoomOptions {
empty_timeout: 5 * 60, // 5 minutes, empty_timeout: 5 * 60, // 5 minutes,
metadata: serde_json::to_string(&metadata).to_internal_error()?,
..Default::default() ..Default::default()
}, },
) )

View File

@@ -5,8 +5,9 @@ use revolt_database::{
iso8601_timestamp::{Duration, Timestamp}, iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference, util::reference::Reference,
voice::{ voice::{
create_voice_state, delete_channel_node, delete_voice_state, get_user_moved_from_voice, create_voice_state, delete_channel_voice_state, delete_voice_state,
get_user_moved_to_voice, get_voice_channel_members, update_voice_state_tracks, VoiceClient, get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
RoomMetadata, UserVoiceChannel, VoiceClient,
}, },
Database, AMQP, Database, AMQP,
}; };
@@ -50,27 +51,34 @@ pub async fn ingress(
let channel_id = event.room.as_ref().map(|r| &r.name); let channel_id = event.room.as_ref().map(|r| &r.name);
let user_id = event.participant.as_ref().map(|r| &r.identity); let user_id = event.participant.as_ref().map(|r| &r.identity);
let room_metadata = if let Some(room) = event.room.as_ref() {
Some(serde_json::from_str::<RoomMetadata>(&room.metadata).to_internal_error()?)
} else {
None
};
match event.event.as_str() { match event.event.as_str() {
// User joined a channel // User joined a channel
"participant_joined" => { "participant_joined" => {
let channel_id = channel_id.to_internal_error()?; let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?; let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let joined_at = Timestamp::UNIX_EPOCH let joined_at = Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at)) .checked_add(Duration::seconds(event.created_at))
.unwrap(); .unwrap();
let voice_state = let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
create_voice_state(channel_id, channel.server(), user_id, joined_at).await?;
// Only publish one event when a user is moved from one channel to another. // Only publish one event when a user is moved from one channel to another.
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? { if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
EventV1::VoiceChannelMove { EventV1::VoiceChannelMove {
user: user_id.to_string(), user: user_id.to_string(),
from: moved_from, from: moved_from.id,
to: channel_id.to_string(), to: channel_id.to_string(),
state: voice_state, state: voice_state,
} }
@@ -135,10 +143,13 @@ pub async fn ingress(
"participant_left" => { "participant_left" => {
let channel_id = channel_id.to_internal_error()?; let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?; let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; delete_voice_state(&channel, user_id).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
// Dont send leave event when a user is moved // Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id) if get_user_moved_from_voice(channel_id, user_id)
@@ -159,8 +170,6 @@ pub async fn ingress(
// let members = get_voice_channel_members(channel_id).await?; // let members = get_voice_channel_members(channel_id).await?;
// if members.is_none_or(|m| m.is_empty()) { // if members.is_none_or(|m| m.is_empty()) {
// delete_channel_node(channel_id).await?;
//
// // The channel is empty so send out an "end" message for ringing // // The channel is empty so send out an "end" message for ringing
// if let Err(e) = amqp // if let Err(e) = amqp
// .dm_call_updated(user_id, channel_id, None, true, None) // .dm_call_updated(user_id, channel_id, None, true, None)
@@ -204,8 +213,11 @@ pub async fn ingress(
let channel_id = channel_id.to_internal_error()?; let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?; let user_id = user_id.to_internal_error()?;
let track = event.track.as_ref().to_internal_error()?; let track = event.track.as_ref().to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let user = Reference::from_unchecked(user_id).as_user(db).await?; let user = Reference::from_unchecked(user_id).as_user(db).await?;
@@ -245,15 +257,14 @@ pub async fn ingress(
log::debug!("Removing user {user_id} from channel {channel_id} {event:?} due to forbidden track."); log::debug!("Removing user {user_id} from channel {channel_id} {event:?} due to forbidden track.");
let _ = voice_client.remove_user(node, user_id, channel_id).await; let _ = voice_client.remove_user(node, user_id, channel_id).await;
delete_voice_state(channel_id, channel.server(), user_id).await?; delete_voice_state(&channel, user_id).await?;
return Ok(EmptyResponse); return Ok(EmptyResponse);
}; };
}; };
let partial = update_voice_state_tracks( let partial = update_voice_state_tracks(
channel_id, &channel,
channel.server(),
user_id, user_id,
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
track.source, track.source,
@@ -268,6 +279,16 @@ pub async fn ingress(
.p(channel_id.clone()) .p(channel_id.clone())
.await; .await;
} }
"room_finished" => {
let channel_id = channel_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
delete_channel_voice_state(&channel, &[]).await?;
}
_ => {} _ => {}
}; };

View File

@@ -1,4 +1,8 @@
use revolt_database::{Database, User, util::reference::Reference, voice::{VoiceClient, remove_user_from_voice_channels}}; use revolt_database::{
util::reference::Reference,
voice::{remove_user_from_voice_channels, VoiceClient},
Database, User,
};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
@@ -21,7 +25,7 @@ pub async fn delete_bot(
bot.delete(db).await?; bot.delete(db).await?;
remove_user_from_voice_channels(db, voice_client, &bot.id).await?; remove_user_from_voice_channels(voice_client, &bot.id).await?;
Ok(EmptyResponse) Ok(EmptyResponse)
} }

View File

@@ -1,7 +1,10 @@
use revolt_database::{ use revolt_database::{
AMQP, Channel, Database, PartialChannel, User, util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{ util::{permissions::DatabasePermissionQuery, reference::Reference},
VoiceClient, delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel voice::{
} delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel,
UserVoiceChannel, VoiceClient,
},
Channel, Database, PartialChannel, User, AMQP,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -54,15 +57,17 @@ pub async fn delete(
) )
.await?; .await?;
if is_in_voice_channel(&user.id, channel.id()).await? { let user_voice_channel = UserVoiceChannel::from_channel(&channel);
remove_user_from_voice_channel(db, voice_client, channel.id(), &user.id).await?;
if is_in_voice_channel(&user.id, &user_voice_channel).await? {
remove_user_from_voice_channel(voice_client, &user_voice_channel, &user.id).await?;
}; };
} }
Channel::TextChannel { .. } => { Channel::TextChannel { .. } => {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await?; channel.delete(db).await?;
delete_voice_channel(voice_client, channel.id(), channel.server()).await?; delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
} }
}; };

View File

@@ -1,6 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_channel, VoiceClient}, voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP, Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -260,7 +260,7 @@ pub async fn edit(
.await?; .await?;
if channel.voice().is_none() { if channel.voice().is_none() {
delete_voice_channel(voice_client, channel.id(), channel.server()).await?; delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
} }
Ok(Json(channel.into())) Ok(Json(channel.into()))

View File

@@ -1,7 +1,5 @@
use revolt_database::{ use revolt_database::{
util::reference::Reference, AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
voice::{is_in_voice_channel, remove_user_from_voice_channel, VoiceClient},
Channel, Database, User, AMQP,
}; };
use revolt_permissions::ChannelPermission; use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -54,8 +52,10 @@ pub async fn remove_member(
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
}; };
if is_in_voice_channel(&member.id, channel.id()).await? { let user_voice_channel = UserVoiceChannel::from_channel(&channel);
remove_user_from_voice_channel(db, voice_client, channel.id(), &member.id).await?;
if is_in_voice_channel(member.id, &user_voice_channel).await? {
remove_user_from_voice_channel(voice_client, &user_voice_channel, member.id).await?;
}; };
Ok(EmptyResponse) Ok(EmptyResponse)

View File

@@ -3,7 +3,8 @@ use revolt_database::{
util::{permissions::perms, reference::Reference}, util::{permissions::perms, reference::Reference},
voice::{ voice::{
delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members, delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members,
raise_if_in_voice, set_call_notification_recipients, set_channel_node, VoiceClient, raise_if_in_voice, set_call_notification_recipients, set_channel_node, UserVoiceChannel,
VoiceClient,
}, },
Database, User, Database, User,
}; };
@@ -50,7 +51,9 @@ pub async fn call(
let current_permissions = calculate_channel_permissions(&mut permissions).await; let current_permissions = calculate_channel_permissions(&mut permissions).await;
current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?; current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
if get_voice_channel_members(channel.id()) let user_voice_channel = UserVoiceChannel::from_channel(&channel);
if get_voice_channel_members(&user_voice_channel)
.await? .await?
.zip(voice_info.max_users) .zip(voice_info.max_users)
.is_some_and(|(ms, max_users)| ms.len() >= max_users) .is_some_and(|(ms, max_users)| ms.len() >= max_users)
@@ -79,22 +82,18 @@ pub async fn call(
// Finds and disconnects any existing voice connections by the user, // Finds and disconnects any existing voice connections by the user,
// should only ever loop once but just to cover our backs. // should only ever loop once but just to cover our backs.
for channel_id in get_user_voice_channels(&user.id).await? { for previous_channel in get_user_voice_channels(&user.id).await? {
if let Some(node) = get_channel_node(&channel_id).await? { if let Some(node) = get_channel_node(&previous_channel.id).await? {
// if this errors its just a mismatching state - ignore and proceed to still delete our state // if this errors its just a mismatching state - ignore and proceed to still delete our state
let _ = voice_client.remove_user(&node, &user.id, &channel_id).await; let _ = voice_client
.remove_user(&node, &user.id, &previous_channel.id)
.await;
}; };
let channel = Reference::from_unchecked(&channel_id) delete_voice_state(&previous_channel, &user.id).await?;
.as_channel(db)
.await;
if channel.is_ok() {
delete_voice_state(&channel_id, channel.unwrap().server(), &user.id).await?;
}
} }
} else { } else {
raise_if_in_voice(&user, channel.id()).await?; raise_if_in_voice(&user, &user_voice_channel).await?;
} }
let token = voice_client let token = voice_client

View File

@@ -1,6 +1,6 @@
use revolt_database::{ use revolt_database::{
util::reference::Reference, util::reference::Reference,
voice::{get_voice_state, VoiceClient}, voice::{get_voice_state, UserVoiceChannel, VoiceClient},
Channel, Database, User, AMQP, Channel, Database, User, AMQP,
}; };
use revolt_result::{create_error, Result, ToRevoltError}; use revolt_result::{create_error, Result, ToRevoltError};
@@ -32,9 +32,15 @@ pub async fn stop_ring(
return Err(create_error!(NoEffect)); return Err(create_error!(NoEffect));
} }
if get_voice_state(channel.id(), None, &user.id) if get_voice_state(
.await? &UserVoiceChannel {
.is_none() id: channel.id().to_string(),
server_id: None,
},
&user.id,
)
.await?
.is_none()
{ {
return Err(create_error!(NotConnected)); return Err(create_error!(NotConnected));
} }
@@ -46,7 +52,7 @@ pub async fn stop_ring(
_ => return Err(create_error!(NoEffect)), _ => return Err(create_error!(NoEffect)),
}; };
if members.iter().any(|m| &target_user.id == m) { if members.iter().any(|m| target_user.id == m) {
if let Err(e) = amqp if let Err(e) = amqp
.dm_call_updated( .dm_call_updated(
&user.id, &user.id,

View File

@@ -1,6 +1,9 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient}, voice::{
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
VoiceClient,
},
Database, RemovalIntention, ServerBan, User, Database, RemovalIntention, ServerBan, User,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -58,8 +61,16 @@ pub async fn ban(
.await?; .await?;
// If the member is in a voice channel while banned kick them from the voice channel // 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(&target.id, &server.id).await? { if let Some(channel_id) = get_user_voice_channel_in_server(target.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?; remove_user_from_voice_channel(
voice_client,
&UserVoiceChannel {
id: channel_id,
server_id: Some(server.id.clone()),
},
target.id,
)
.await?;
} }
} }

View File

@@ -9,7 +9,7 @@ use revolt_database::{
voice::{ voice::{
get_channel_node, get_user_voice_channel_in_server, set_channel_node, get_channel_node, get_user_voice_channel_in_server, set_channel_node,
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions, set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
VoiceClient, UserVoiceChannel, VoiceClient,
}, },
Database, File, PartialMember, User, Database, File, PartialMember, User,
}; };
@@ -52,7 +52,9 @@ pub async fn edit(
let permissions = calculate_server_permissions(&mut query).await; let permissions = calculate_server_permissions(&mut query).await;
// Fetch target permissions // Fetch target permissions
let mut target_query = DatabasePermissionQuery::new(db, &target_user).server(&server).member(&member); let mut target_query = DatabasePermissionQuery::new(db, &target_user)
.server(&server)
.member(&member);
let target_permissions = calculate_server_permissions(&mut target_query).await; let target_permissions = calculate_server_permissions(&mut target_query).await;
// Check permissions in server // Check permissions in server
@@ -83,7 +85,7 @@ pub async fn edit(
} }
if target_permissions.has_channel_permission(ChannelPermission::TimeoutMembers) { if target_permissions.has_channel_permission(ChannelPermission::TimeoutMembers) {
return Err(create_error!(IsElevated)) return Err(create_error!(IsElevated));
} }
} }
@@ -99,7 +101,7 @@ pub async fn edit(
} }
if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) { if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) {
return Err(create_error!(InvalidOperation)) return Err(create_error!(InvalidOperation));
} }
if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) { if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) {
@@ -217,8 +219,19 @@ pub async fn edit(
} }
}; };
set_user_moved_from_voice(&channel, new_voice_channel.id(), &target_user.id).await?; let new_user_voice_channel = UserVoiceChannel::from_channel(&new_voice_channel);
set_user_moved_to_voice(new_voice_channel.id(), &channel, &target_user.id).await?; let old_user_voice_channel = UserVoiceChannel {
id: channel.clone(),
server_id: new_user_voice_channel.server_id.clone(),
};
set_user_moved_from_voice(&channel, &new_user_voice_channel, &target_user.id).await?;
set_user_moved_to_voice(
new_voice_channel.id(),
&old_user_voice_channel,
&target_user.id,
)
.await?;
let mut query = perms(db, &target_user).channel(&new_voice_channel); let mut query = perms(db, &target_user).channel(&new_voice_channel);
let permissions = calculate_channel_permissions(&mut query).await; let permissions = calculate_channel_permissions(&mut query).await;

View File

@@ -1,6 +1,9 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient}, voice::{
get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
VoiceClient,
},
Database, RemovalIntention, User, Database, RemovalIntention, User,
}; };
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -46,8 +49,16 @@ pub async fn kick(
.remove(db, &server, RemovalIntention::Kick, false) .remove(db, &server, RemovalIntention::Kick, false)
.await?; .await?;
if let Some(channel_id) = get_user_voice_channel_in_server(&target.id, &server.id).await? { if let Some(channel_id) = get_user_voice_channel_in_server(target.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?; remove_user_from_voice_channel(
voice_client,
&UserVoiceChannel {
id: channel_id,
server_id: Some(server.id.clone()),
},
target.id,
)
.await?;
}; };
Ok(EmptyResponse) Ok(EmptyResponse)

View File

@@ -2,7 +2,7 @@ use revolt_database::{
util::reference::Reference, util::reference::Reference,
voice::{ voice::{
delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel, delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel,
VoiceClient, UserVoiceChannel, VoiceClient,
}, },
Database, RemovalIntention, User, Database, RemovalIntention, User,
}; };
@@ -29,13 +29,28 @@ pub async fn delete(
if server.owner == user.id { if server.owner == user.id {
for channel_id in &server.channels { for channel_id in &server.channels {
delete_voice_channel(voice_client, channel_id, Some(&server.id)).await?; delete_voice_channel(
voice_client,
&UserVoiceChannel {
id: channel_id.clone(),
server_id: Some(server.id.clone()),
},
)
.await?;
} }
server.delete(db).await server.delete(db).await
} else { } else {
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? { if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &user.id).await?; remove_user_from_voice_channel(
voice_client,
&UserVoiceChannel {
id: channel_id,
server_id: Some(server.id.clone()),
},
&user.id,
)
.await?;
}; };
member member