feat: Audit Logs (#466)

* fix: squash audit log branch changes

Signed-off-by: Zomatree <me@zomatree.live>

* feat: emoji edit + pronoun

Signed-off-by: Zomatree <me@zomatree.live>

* fix: use tokio instead of async-std

Signed-off-by: Zomatree <me@zomatree.live>

* feat: add slowmode to audit log

Signed-off-by: Zomatree <me@zomatree.live>

* chore: update comments on permissions

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
Zomatree
2026-07-13 13:39:28 -07:00
committed by GitHub
parent f60c8340f4
commit 502203d37c
57 changed files with 2014 additions and 150 deletions
+40 -11
View File
@@ -11,15 +11,19 @@ use revolt_database::{
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
UserVoiceChannel, VoiceClient,
},
Database, File, PartialMember, User,
AuditLogEntryAction, Database, FieldsMember, File, PartialMember, User,
};
use revolt_models::v0::{self, FieldsMember};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission};
use revolt_permissions::{
calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission,
};
use revolt_result::{create_error, Result};
use rocket::{form::validate::Contains, serde::json::Json, State};
use validator::Validate;
use crate::util::audit_log_reason::AuditLogReason;
/// # Edit Member
///
/// Edit a member by their id.
@@ -29,6 +33,7 @@ pub async fn edit(
db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User,
reason: AuditLogReason,
server_id: Reference<'_>,
member_id: Reference<'_>,
data: Json<v0::DataMemberEdit>,
@@ -76,7 +81,7 @@ pub async fn edit(
} else if data.remove.contains(&v0::FieldsMember::Avatar) {
permissions.throw_if_lacking_channel_permission(ChannelPermission::RemoveAvatars)?;
} else {
return Err(create_error!(InvalidOperation))
return Err(create_error!(InvalidOperation));
}
}
@@ -106,11 +111,11 @@ pub async fn edit(
permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?;
}
if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) {
if data.voice_channel.is_some() && data.remove.contains(&v0::FieldsMember::VoiceChannel) {
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(&v0::FieldsMember::VoiceChannel) {
if !voice_client.is_enabled() {
return Err(create_error!(LiveKitUnavailable));
};
@@ -132,7 +137,8 @@ pub async fn edit(
Err(create_error!(UnknownChannel))?
}
let channel_permissions = calculate_channel_permissions(&mut query.clone().channel(&channel)).await;
let channel_permissions =
calculate_channel_permissions(&mut query.clone().channel(&channel)).await;
channel_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
if get_user_voice_channel_in_server(&target_user.id, &server.id)
@@ -210,9 +216,28 @@ pub async fn edit(
partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?);
}
member
.update(db, partial, remove.clone().into_iter().map(Into::into).collect())
.await?;
let remove = remove
.into_iter()
.map(Into::into)
.collect::<Vec<FieldsMember>>();
let before = member.generate_diff(&partial, &remove);
member.update(db, partial.clone(), remove.clone()).await?;
AuditLogEntryAction::MemberEdit {
user: member.id.user.clone(),
before,
after: partial,
}
.insert(
db,
server.id.clone(),
reason,
user.id.clone(),
Some(member.id.user.clone()),
)
.await;
if let Some(new_voice_channel) = new_voice_channel {
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
@@ -264,7 +289,11 @@ pub async fn edit(
.private(target_user.id.clone())
.await;
};
} else if can_publish.is_some() || can_receive.is_some() || remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive) {
} else if can_publish.is_some()
|| can_receive.is_some()
|| remove.contains(FieldsMember::CanPublish)
|| remove.contains(FieldsMember::CanReceive)
{
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
{
let node = get_channel_node(&channel).await?.unwrap();