fix: squash audit log branch changes

Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
Zomatree
2026-04-20 03:47:49 +01:00
parent 057f2bb8b3
commit ada0bc0f8e
57 changed files with 1923 additions and 153 deletions

View File

@@ -75,6 +75,10 @@ max_concurrent_connections = 50
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.audit_logs]
# How long audit log entries last before being removed, in seconds
expires_after = 2592000 # 30d
[api.livekit.nodes]
[api.users]

View File

@@ -233,6 +233,12 @@ pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiAuditLogs {
/// How long audit log entries last before being removed, in seconds
pub expires_after: u64,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub registration: ApiRegistration,
@@ -241,6 +247,7 @@ pub struct Api {
pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers,
pub audit_logs: ApiAuditLogs,
}
#[derive(Deserialize, Debug, Clone)]

View File

@@ -3,15 +3,16 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook,
AuditLogEntry, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite,
Member, MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan,
Snapshot, User, UserSettings, Webhook,
};
database_derived!(
/// Reference implementation
#[derive(Default, Debug)]
pub struct ReferenceDb {
pub audit_logs: Arc<Mutex<HashMap<String, AuditLogEntry>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,

View File

@@ -77,6 +77,78 @@ macro_rules! auto_derived_partial {
};
}
/// Internal macro for `generate_diff!`, you should not need to use this yourself.
macro_rules! generate_field_diff {
(optional, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
$before.$field = $self.$field.clone();
};
};
(optional, default, $remove:ident, $fieldsmember:path, $self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() || $remove.contains(&$fieldsmember) {
$before.$field = Some($self.$field.clone());
};
};
($self:ident, $before:ident, $partial:ident, $field:ident) => {
if $partial.$field.is_some() {
$before.$field = Some($self.$field.clone());
};
};
}
/// Generates a partial model containing the data which has changed in an update
///
/// ## Usage:
/// `before` is the "output" containing what the model had before being updated,
/// this will corraspond to `partial` which is what the data is being changed too.
///
/// ```rs
/// let mut before = PartialModel::default();
///
/// generate_diff!(
/// self, // database model
/// before, // mutable empty partial corrasponding to the current model
/// partial, // partial containing what is being updated
/// remove, // slice of fields being removed
/// (
/// name, // regular non-nullable non-removable field
/// (FieldsEnum::Nickname) nickname, // optional removable field
/// ((default) FieldsEnum::Roles) roles, // optional removable field with custom default
/// )
/// );
/// ```
///
/// See `Member::generate_diff` `Server::generate_diff` `Role::generate_diff` for full examples
macro_rules! generate_diff {
(
$self:ident,
$before:ident,
$partial:ident,
$remove:ident,
(
$(
$(
$(@$optional:tt)? (
$($(@$default:tt)? (default))?
$fieldsmember:path
)
)?
$field: ident
),*
$(,)?
)
) => {
$(
generate_field_diff!(
$( $($optional)? optional, $($($default)? default,)? $remove, $fieldsmember,)?
$self, $before, $partial, $field
);
)*
}
}
mod drivers;
pub use drivers::*;
@@ -115,7 +187,6 @@ pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t

View File

@@ -98,6 +98,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create pubsub collection.");
db.create_collection("audit_logs")
.await
.expect("Failed to create audit_logs collection");
db.run_command(doc! {
"createIndexes": "users",
"indexes": [
@@ -263,5 +267,31 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create ratelimit_events index.");
db.run_command(doc! {
"createIndexes": "audit_logs",
"indexes": [
{
"key": {
"expires_at": 1_i32,
},
"name": "expires_at_ttl",
// We set the expire after to 0 because we store when it expires instead of when the document was inserted,
// this is because mongo cant read the timestamp from the ulid so we need to do this workaround.
// relevant docs: https://www.mongodb.com/docs/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time
"expireAfterSeconds": 0
},
{
"key": {
"server": 1_i32,
"user": 1_i32,
"action.type": 1_i32,
},
"name": "audit_log_filters",
},
]
})
.await
.expect("Failed to create audit_logs index");
info!("Created database.");
}

View File

@@ -25,7 +25,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 50; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 51; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -1306,6 +1306,39 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
};
if revision >= 50 {
info!("Running migration [revision 50 / 28-11-2025]: Add audit logs collection");
db.db()
.create_collection("audit_logs")
.await
.expect("Failed to create audit_logs collection");
db.db()
.run_command(doc! {
"createIndexes": "audit_logs",
"indexes": [
{
"key": {
"expires_at": 1_i32,
},
"name": "expires_at_ttl",
"expireAfterSeconds": 0
},
{
"key": {
"server": 1_i32,
"user": 1_i32,
"action.type": 1_i32,
},
"name": "audit_log_filters",
},
]
})
.await
.expect("Failed to create audit_logs index");
};
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,269 @@
use std::{collections::HashSet, time::Duration};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use ulid::Ulid;
use crate::{Database, PartialChannel, PartialMember, PartialRole, PartialServer, User};
use revolt_models::v0;
use revolt_permissions::OverrideField;
use revolt_result::Result;
auto_derived!(
/// Audit log entry
pub struct AuditLogEntry {
/// Unique ID
#[serde(rename = "_id")]
pub id: String,
/// When the audit log entry gets auto-deleted
///
/// This is only stored in the database and not given to users.
pub expires_at: Timestamp,
/// The server the entry happened in
pub server: String,
/// User provided reason
pub reason: Option<String>,
/// User who ran the action
pub user: String,
/// User this action is targetting
pub target: Option<String>,
/// The action ran
pub action: AuditLogEntryAction,
}
/// Indivual audit log action
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum AuditLogEntryAction {
MessageDelete {
author: String,
channel: String,
},
MessageBulkDelete {
channel: String,
count: usize,
},
MessagePin {
message: String,
author: String,
channel: String,
},
MessageUnpin {
message: String,
author: String,
channel: String,
},
BanCreate {
user: String,
},
BanDelete {
user: String,
},
ChannelCreate {
channel: String,
name: String,
},
ChannelEdit {
channel: String,
before: PartialChannel,
after: PartialChannel,
},
ChannelRolePermissionsEdit {
channel: String,
role: String,
permissions: OverrideField,
},
ChannelDelete {
channel: String,
name: String,
},
MemberEdit {
user: String,
before: PartialMember,
after: PartialMember,
},
MemberKick {
user: String,
},
ServerEdit {
before: PartialServer,
after: PartialServer,
},
RoleEdit {
role: String,
before: PartialRole,
after: PartialRole,
},
RoleCreate {
role: String,
name: String,
},
RoleDelete {
role: String,
name: String,
},
RolesReorder {
before: Vec<String>,
after: Vec<String>,
},
InviteCreate {
invite: String,
channel: String,
},
InviteDelete {
invite: String,
channel: String,
},
WebhookCreate {
webhook: String,
name: String,
channel: String,
},
WebhookDelete {
webhook: String,
name: String,
channel: String,
},
EmojiCreate {
emoji: String,
name: String,
},
EmojiDelete {
emoji: String,
name: String,
},
}
/// Audit Log Query
pub struct AuditLogQuery {
/// Filter by who ran the action
pub user: Option<String>,
/// Filter by who the action is targetting
pub target: Option<String>,
/// Filter by the action type
pub r#type: Option<Vec<String>>,
/// Entries before a certain entry id
pub before: Option<String>,
/// Entries after a certain entry id
pub after: Option<String>,
/// Maximum number of entries to fetch
pub limit: i64,
}
);
impl AuditLogEntryAction {
// TODO: migrate this to a rabbitmq queue to avoid spawning lots of tasks
/// Generates an `AuditLogEntry` for the current action and inserts it into the database
pub async fn insert<R: Into<Option<String>>>(
self,
db: &Database,
server: String,
reason: R,
user: String,
target: Option<String>,
) -> AuditLogEntry {
let config = config().await;
let id = Ulid::new();
let expires_at = id
.datetime()
.checked_add(Duration::from_secs(config.api.audit_logs.expires_after))
.unwrap()
.into();
let entry = AuditLogEntry {
id: id.to_string(),
expires_at,
server,
reason: reason.into(),
user,
target,
action: self,
};
// running the insert inside a task can cause race conditions in the test so for now just dont use a task for tests for now
// this will need to be redone for when we migrate to using rabbitmq here anyway.
#[cfg(not(test))]
async_std::task::spawn({
let db = db.clone();
let entry = entry.clone();
async move { revolt_config::report_internal_error!(db.insert_audit_log_entry(&entry).await) }
});
#[cfg(test)]
db.insert_audit_log_entry(&entry).await.unwrap();
entry
}
}
impl AuditLogEntry {
/// Fetches the corrasponding users and members for each audit log entry
pub async fn with_users(
db: &Database,
server_id: &str,
user: &User,
entries: &[Self],
) -> Result<(Vec<v0::User>, Vec<v0::Member>)> {
let mut user_ids = HashSet::new();
for entry in entries {
user_ids.insert(entry.user.clone());
match &entry.action {
AuditLogEntryAction::MessageDelete { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::BanCreate { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::BanDelete { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::ChannelCreate { .. } => {}
AuditLogEntryAction::MemberEdit { user, .. } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::MemberKick { user } => {
user_ids.insert(user.clone());
}
AuditLogEntryAction::MessagePin { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::MessageUnpin { author, .. } => {
user_ids.insert(author.clone());
}
AuditLogEntryAction::ServerEdit { .. } => {}
AuditLogEntryAction::RoleEdit { .. } => {}
AuditLogEntryAction::RoleCreate { .. } => {}
AuditLogEntryAction::RoleDelete { .. } => {}
AuditLogEntryAction::RolesReorder { .. } => {}
AuditLogEntryAction::MessageBulkDelete { .. } => {}
AuditLogEntryAction::ChannelEdit { .. } => {}
AuditLogEntryAction::ChannelRolePermissionsEdit { .. } => {}
AuditLogEntryAction::ChannelDelete { .. } => {}
AuditLogEntryAction::InviteCreate { .. } => {}
AuditLogEntryAction::InviteDelete { .. } => {}
AuditLogEntryAction::WebhookCreate { .. } => {}
AuditLogEntryAction::WebhookDelete { .. } => {}
AuditLogEntryAction::EmojiCreate { .. } => {}
AuditLogEntryAction::EmojiDelete { .. } => {}
};
}
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
let users = User::fetch_many_ids_as_mutuals(db, user, &user_ids).await?;
let members = db
.fetch_members(server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok((users, members))
}
}

View File

@@ -0,0 +1,20 @@
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAuditLogs: Sync + Send {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()>;
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>>;
}

View File

@@ -0,0 +1,66 @@
use mongodb::options::FindOptions;
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery, MongoDb};
use super::AbstractAuditLogs;
static COL: &str = "audit_logs";
#[async_trait]
impl AbstractAuditLogs for MongoDb {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
query!(self, insert_one, COL, entry).map(|_| ())
}
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>> {
let mut filter = doc! {
"server": server
};
if let Some(user) = query.user {
filter.insert("user", user);
};
if let Some(target) = query.target {
filter.insert("target", target);
}
if let Some(types) = query.r#type {
filter.insert("action.type", doc! { "$in": types });
};
if let Some(doc) = match (query.before, query.after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
};
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(query.limit)
.sort(doc! { "_id": -1 })
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}

View File

@@ -0,0 +1,81 @@
use revolt_result::Result;
use crate::{AuditLogEntry, AuditLogQuery, ReferenceDb};
use super::AbstractAuditLogs;
#[async_trait]
impl AbstractAuditLogs for ReferenceDb {
/// Inserts an entry into the server's audit log
async fn insert_audit_log_entry(&self, entry: &AuditLogEntry) -> Result<()> {
self.audit_logs
.lock()
.await
.insert(entry.id.clone(), entry.clone());
Ok(())
}
/// Fetches a server's audit logs using the provided query options
async fn get_server_audit_logs(
&self,
server: &str,
query: AuditLogQuery,
) -> Result<Vec<AuditLogEntry>> {
let lock = self.audit_logs.lock().await;
let mut logs = lock
.values()
.filter(|entry| {
if entry.server != server {
return false;
};
if let Some(user) = &query.user {
if &entry.user != user {
return false;
}
}
if query.target.is_some() && entry.target != query.target {
return false;
}
if let Some(before) = &query.before {
if &entry.id > before {
return false;
};
};
if let Some(after) = &query.after {
if &entry.id < after {
return false;
};
};
if let Some(action_types) = &query.r#type {
let entry_type = serde_json::to_value(entry.action.clone())
.unwrap()
.as_object()
.unwrap()
.get("type")
.unwrap()
.as_str()
.unwrap()
.to_string();
if !action_types.contains(&entry_type) {
return false;
}
};
true
})
.cloned()
.collect::<Vec<_>>();
logs.sort_by(|a, b| b.id.cmp(&a.id));
logs.truncate(query.limit as usize);
Ok(logs)
}
}

View File

@@ -642,6 +642,117 @@ impl Channel {
}
}
/// Generates a PartialChannel containing the data which has changed in an update
pub fn generate_diff(
&self,
partial: &PartialChannel,
remove: &[FieldsChannel],
) -> PartialChannel {
let mut before = PartialChannel::default();
match self {
Channel::SavedMessages { .. } => {}
Channel::DirectMessage {
active,
last_message_id,
..
} => {
if partial.active.is_some() {
before.active = Some(*active);
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
}
Channel::Group {
name,
owner,
description,
icon,
last_message_id,
permissions,
nsfw,
..
} => {
if partial.name.is_some() {
before.name = Some(name.clone());
};
if partial.owner.is_some() {
before.owner = Some(owner.clone());
};
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
before.description = description.clone();
};
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
before.icon = icon.clone();
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
if partial.permissions.is_some() {
before.permissions = *permissions;
};
if partial.nsfw.is_some() {
before.nsfw = Some(*nsfw);
};
}
Channel::TextChannel {
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
voice,
..
} => {
if partial.name.is_some() {
before.name = Some(name.clone());
};
if partial.description.is_some() || remove.contains(&FieldsChannel::Description) {
before.description = description.clone();
};
if partial.icon.is_some() || remove.contains(&FieldsChannel::Icon) {
before.icon = icon.clone();
};
if partial.last_message_id.is_some() {
before.last_message_id = last_message_id.clone()
};
if partial.default_permissions.is_some()
|| remove.contains(&FieldsChannel::DefaultPermissions)
{
before.default_permissions = *default_permissions;
};
if partial.role_permissions.is_some() {
before.role_permissions = Some(role_permissions.clone());
};
if partial.nsfw.is_some() {
before.nsfw = Some(*nsfw);
};
if partial.voice.is_some() || remove.contains(&FieldsChannel::Voice) {
before.voice = voice.clone();
};
}
}
before
}
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
EventV1::ChannelAck {

View File

@@ -65,14 +65,14 @@ impl Emoji {
}
/// Delete an emoji
pub async fn delete(self, db: &Database) -> Result<()> {
pub async fn delete(&self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_emoji(&self).await
db.detach_emoji(self).await
}
/// Check whether we can use a given emoji

View File

@@ -998,11 +998,13 @@ impl Message {
}
/// Delete a message
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
pub async fn delete(&self, db: &Database) -> Result<()> {
let file_ids = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
.iter()
.flatten()
.map(|file| file.id.clone())
.collect::<Vec<_>>();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
@@ -1011,10 +1013,10 @@ impl Message {
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id,
id: self.id.clone(),
channel: self.channel.clone(),
}
.p(self.channel)
.p(self.channel.clone())
.await;
Ok(())
}

View File

@@ -1,4 +1,5 @@
mod admin_migrations;
mod audit_logs;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -19,6 +20,7 @@ mod user_settings;
mod users;
pub use admin_migrations::*;
pub use audit_logs::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -47,6 +49,7 @@ pub trait AbstractDatabase:
Sync
+ Send
+ admin_migrations::AbstractMigrations
+ audit_logs::AbstractAuditLogs
+ bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites

View File

@@ -239,6 +239,25 @@ impl Member {
}
}
/// Generates a PartialMember containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialMember, remove: &[FieldsMember]) -> PartialMember {
let mut before = PartialMember::default();
generate_diff!(
self, before, partial, remove,
(
(FieldsMember::Nickname) nickname,
(FieldsMember::Avatar) avatar,
(FieldsMember::Timeout) timeout,
((default) FieldsMember::Roles) roles,
((default) FieldsMember::CanPublish) can_publish,
((default) FieldsMember::CanReceive) can_receive,
)
);
before
}
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
let mut value = i64::MAX;
@@ -264,7 +283,7 @@ impl Member {
/// Remove member from server
pub async fn remove(
self,
&self,
db: &Database,
server: &Server,
intention: RemovalIntention,
@@ -291,9 +310,9 @@ impl Member {
})
{
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user.clone() },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user.clone() },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user.clone() },
}
.into_message(id.to_string())
// TODO: support notifications here in the future?

View File

@@ -231,6 +231,31 @@ impl Server {
}
}
/// Generates a PartialServer containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialServer, remove: &[FieldsServer]) -> PartialServer {
let mut before = PartialServer::default();
generate_diff!(
self, before, partial, remove,
(
owner,
name,
(FieldsServer::Description) description,
(FieldsServer::Categories) categories,
(FieldsServer::SystemMessages) system_messages,
roles,
default_permissions,
(FieldsServer::Icon) icon,
(FieldsServer::Banner) banner,
nsfw,
analytics,
discoverable,
)
);
before
}
/// Ordered roles list
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
@@ -370,8 +395,26 @@ impl Role {
}
}
/// Generates a PartialRole containing the data which has changed in an update
pub fn generate_diff(&self, partial: &PartialRole, remove: &[FieldsRole]) -> PartialRole {
let mut before = PartialRole::default();
generate_diff!(
self, before, partial, remove,
(
name,
permissions,
(FieldsRole::Colour) colour,
hoist,
rank,
)
);
before
}
/// Delete a role
pub async fn delete(self, db: &Database, server_id: &str) -> Result<()> {
pub async fn delete(&self, db: &Database, server_id: &str) -> Result<()> {
EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: self.id.clone(),

View File

@@ -247,6 +247,13 @@ impl MongoDb {
})
.await?;
self.col::<Document>("audit_logs")
.delete_many(doc! {
"server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", "audit_logs"))?;
Ok(())
}
}

View File

@@ -190,7 +190,7 @@ impl From<crate::Channel> for Channel {
role_permissions,
nsfw,
voice,
slowmode
slowmode,
} => Channel::TextChannel {
id,
server,
@@ -202,7 +202,7 @@ impl From<crate::Channel> for Channel {
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
slowmode
slowmode,
},
}
}
@@ -256,7 +256,7 @@ impl From<Channel> for crate::Channel {
role_permissions,
nsfw,
voice,
slowmode
slowmode,
} => crate::Channel::TextChannel {
id,
server,
@@ -268,7 +268,7 @@ impl From<Channel> for crate::Channel {
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
slowmode
slowmode,
},
}
}
@@ -307,7 +307,7 @@ impl From<PartialChannel> for crate::PartialChannel {
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
voice: value.voice.map(|voice| voice.into()),
slowmode: value.slowmode
slowmode: value.slowmode,
}
}
}
@@ -1401,6 +1401,14 @@ impl From<FieldsMessage> for crate::FieldsMessage {
}
}
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users,
}
}
}
impl From<VoiceInformation> for crate::VoiceInformation {
fn from(value: VoiceInformation) -> Self {
crate::VoiceInformation {
@@ -1409,10 +1417,142 @@ impl From<VoiceInformation> for crate::VoiceInformation {
}
}
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users,
impl From<crate::AuditLogEntryAction> for AuditLogEntryAction {
fn from(value: crate::AuditLogEntryAction) -> Self {
match value {
crate::AuditLogEntryAction::MessageDelete { author, channel } => {
AuditLogEntryAction::MessageDelete { author, channel }
}
crate::AuditLogEntryAction::BanCreate { user } => {
AuditLogEntryAction::BanCreate { user }
}
crate::AuditLogEntryAction::BanDelete { user } => {
AuditLogEntryAction::BanDelete { user }
}
crate::AuditLogEntryAction::ChannelCreate { channel, name } => {
AuditLogEntryAction::ChannelCreate { channel, name }
}
crate::AuditLogEntryAction::MemberEdit {
user,
before,
after,
} => AuditLogEntryAction::MemberEdit {
user,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::MemberKick { user } => {
AuditLogEntryAction::MemberKick { user }
}
crate::AuditLogEntryAction::ServerEdit { before, after } => {
AuditLogEntryAction::ServerEdit {
before: before.into(),
after: after.into(),
}
}
crate::AuditLogEntryAction::RoleEdit {
role,
before,
after,
} => AuditLogEntryAction::RoleEdit {
role,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::RoleCreate { role, name } => {
AuditLogEntryAction::RoleCreate { role, name }
}
crate::AuditLogEntryAction::RoleDelete { role, name } => {
AuditLogEntryAction::RoleDelete { role, name }
}
crate::AuditLogEntryAction::RolesReorder { before, after } => {
AuditLogEntryAction::RolesReorder { before, after }
}
crate::AuditLogEntryAction::MessageBulkDelete { channel, count } => {
AuditLogEntryAction::MessageBulkDelete { channel, count }
}
crate::AuditLogEntryAction::ChannelEdit {
channel,
before,
after,
} => AuditLogEntryAction::ChannelEdit {
channel,
before: before.into(),
after: after.into(),
},
crate::AuditLogEntryAction::ChannelRolePermissionsEdit {
channel,
role,
permissions,
} => AuditLogEntryAction::ChannelRolePermissionsEdit {
channel,
role,
permissions: permissions.into(),
},
crate::AuditLogEntryAction::ChannelDelete { channel, name } => {
AuditLogEntryAction::ChannelDelete { channel, name }
}
crate::AuditLogEntryAction::InviteDelete { invite, channel } => {
AuditLogEntryAction::InviteDelete { invite, channel }
}
crate::AuditLogEntryAction::WebhookCreate {
webhook,
name,
channel,
} => AuditLogEntryAction::WebhookCreate {
webhook,
name,
channel,
},
crate::AuditLogEntryAction::WebhookDelete {
webhook,
name,
channel,
} => AuditLogEntryAction::WebhookDelete {
webhook,
name,
channel,
},
crate::AuditLogEntryAction::EmojiCreate { emoji, name } => {
AuditLogEntryAction::EmojiCreate { emoji, name }
}
crate::AuditLogEntryAction::EmojiDelete { emoji, name } => {
AuditLogEntryAction::EmojiDelete { emoji, name }
}
crate::AuditLogEntryAction::MessagePin {
message,
author,
channel,
} => AuditLogEntryAction::MessagePin {
message,
author,
channel,
},
crate::AuditLogEntryAction::MessageUnpin {
message,
author,
channel,
} => AuditLogEntryAction::MessageUnpin {
message,
author,
channel,
},
crate::AuditLogEntryAction::InviteCreate { invite, channel } => {
AuditLogEntryAction::InviteCreate { invite, channel }
}
}
}
}
impl From<crate::AuditLogEntry> for AuditLogEntry {
fn from(value: crate::AuditLogEntry) -> Self {
AuditLogEntry {
id: value.id,
server: value.server,
reason: value.reason,
user: value.user,
target: value.target,
action: value.action.into(),
}
}
}

View File

@@ -0,0 +1,158 @@
use crate::v0::{Member, PartialChannel, PartialMember, PartialRole, PartialServer, User};
use revolt_permissions::Override;
auto_derived!(
/// Audit log entry
pub struct AuditLogEntry {
/// Unique ID
#[serde(rename = "_id")]
pub id: String,
/// The server the entry happened in
pub server: String,
/// User provided reason
pub reason: Option<String>,
/// User who ran the action
pub user: String,
/// User this action is targetting
pub target: Option<String>,
/// The action ran
pub action: AuditLogEntryAction,
}
/// Indivual action stored on the audit log
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum AuditLogEntryAction {
MessageDelete {
author: String,
channel: String,
},
MessageBulkDelete {
channel: String,
count: usize,
},
MessagePin {
message: String,
author: String,
channel: String,
},
MessageUnpin {
message: String,
author: String,
channel: String,
},
BanCreate {
user: String,
},
BanDelete {
user: String,
},
ChannelCreate {
channel: String,
name: String,
},
ChannelEdit {
channel: String,
before: PartialChannel,
after: PartialChannel,
},
ChannelRolePermissionsEdit {
channel: String,
role: String,
permissions: Override,
},
ChannelDelete {
channel: String,
name: String,
},
MemberEdit {
user: String,
before: PartialMember,
after: PartialMember,
},
MemberKick {
user: String,
},
ServerEdit {
before: PartialServer,
after: PartialServer,
},
RoleEdit {
role: String,
before: PartialRole,
after: PartialRole,
},
RoleCreate {
role: String,
name: String,
},
RoleDelete {
role: String,
name: String,
},
RolesReorder {
before: Vec<String>,
after: Vec<String>,
},
InviteCreate {
invite: String,
channel: String,
},
InviteDelete {
invite: String,
channel: String,
},
WebhookCreate {
webhook: String,
name: String,
channel: String,
},
WebhookDelete {
webhook: String,
name: String,
channel: String,
},
EmojiCreate {
emoji: String,
name: String,
},
EmojiDelete {
emoji: String,
name: String,
},
}
/// Audit log query filters
#[cfg_attr(feature = "validator", derive(validator::Validate))]
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
pub struct OptionsAuditLogQuery {
/// Filter by who ran the action
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub user: Option<String>,
/// Filter by who the action is targetting
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub target: Option<String>,
/// Filter by the action type
pub r#type: Option<Vec<String>>,
/// Entries before a certain entry id
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Entries after a certain entry id
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Maximum number of entries to fetch
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
}
/// Response containing the audit log entries and the users involved
pub struct AuditLogQueryResponse {
/// List of audit logs
pub audit_logs: Vec<AuditLogEntry>,
/// List of users
pub users: Vec<User>,
/// List of members
pub members: Vec<Member>,
}
);

View File

@@ -1,3 +1,4 @@
mod audit_logs;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -15,6 +16,7 @@ mod servers;
mod user_settings;
mod users;
pub use audit_logs::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;

View File

@@ -100,6 +100,8 @@ pub enum ChannelPermission {
/// Mention roles
MentionRoles = 1 << 38,
ViewAuditLogs = 1 << 40,
// * Misc. permissions
// % Bits 39 to 52: free area
// % Bits 53 to 64: do not use

View File

@@ -87,6 +87,7 @@ impl IntoResponse for Error {
ErrorType::UnknownNode => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::HeaderTooLarge => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,

View File

@@ -163,6 +163,7 @@ pub enum ErrorType {
FailedValidation {
error: String,
},
HeaderTooLarge,
// ? Voice errors
LiveKitUnavailable,

View File

@@ -92,6 +92,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotConnected => Status::BadRequest,
ErrorType::UnknownNode => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::HeaderTooLarge => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity,