Skip to main content

revolt_database/models/audit_logs/
model.rs

1use std::{collections::HashSet, time::Duration};
2
3use iso8601_timestamp::Timestamp;
4use revolt_config::config;
5use ulid::Ulid;
6
7use crate::{Database, PartialChannel, PartialMember, PartialRole, PartialServer, User, PartialEmoji};
8use revolt_models::v0;
9use revolt_permissions::OverrideField;
10use revolt_result::Result;
11
12auto_derived!(
13    /// Audit log entry
14    pub struct AuditLogEntry {
15        /// Unique ID
16        #[serde(rename = "_id")]
17        pub id: String,
18
19        /// When the audit log entry gets auto-deleted
20        ///
21        /// This is only stored in the database and not given to users.
22        pub expires_at: Timestamp,
23
24        /// The server the entry happened in
25        pub server: String,
26        /// User provided reason
27        pub reason: Option<String>,
28        /// User who ran the action
29        pub user: String,
30        /// User this action is targetting
31        pub target: Option<String>,
32        /// The action ran
33        pub action: AuditLogEntryAction,
34    }
35
36    /// Indivual audit log action
37    #[serde(tag = "type")]
38    #[allow(clippy::large_enum_variant)]
39    pub enum AuditLogEntryAction {
40        MessageDelete {
41            author: String,
42            channel: String,
43        },
44        MessageBulkDelete {
45            channel: String,
46            count: usize,
47        },
48        MessagePin {
49            message: String,
50            author: String,
51            channel: String,
52        },
53        MessageUnpin {
54            message: String,
55            author: String,
56            channel: String,
57        },
58        BanCreate {
59            user: String,
60        },
61        BanDelete {
62            user: String,
63        },
64        ChannelCreate {
65            channel: String,
66            name: String,
67        },
68        ChannelEdit {
69            channel: String,
70            before: PartialChannel,
71            after: PartialChannel,
72        },
73        ChannelRolePermissionsEdit {
74            channel: String,
75            role: String,
76            permissions: OverrideField,
77        },
78        ChannelDelete {
79            channel: String,
80            name: String,
81        },
82        MemberEdit {
83            user: String,
84            before: PartialMember,
85            after: PartialMember,
86        },
87        MemberKick {
88            user: String,
89        },
90        ServerEdit {
91            before: PartialServer,
92            after: PartialServer,
93        },
94        RoleEdit {
95            role: String,
96            before: PartialRole,
97            after: PartialRole,
98        },
99        RoleCreate {
100            role: String,
101            name: String,
102        },
103        RoleDelete {
104            role: String,
105            name: String,
106        },
107        RolesReorder {
108            before: Vec<String>,
109            after: Vec<String>,
110        },
111        InviteCreate {
112            invite: String,
113            channel: String,
114        },
115        InviteDelete {
116            invite: String,
117            channel: String,
118        },
119        WebhookCreate {
120            webhook: String,
121            name: String,
122            channel: String,
123        },
124        WebhookDelete {
125            webhook: String,
126            name: String,
127            channel: String,
128        },
129        EmojiCreate {
130            emoji: String,
131            name: String,
132        },
133        EmojiUpdate {
134            emoji: String,
135            before: PartialEmoji,
136            after: PartialEmoji,
137        },
138        EmojiDelete {
139            emoji: String,
140            name: String,
141        },
142    }
143
144    /// Audit Log Query
145    pub struct AuditLogQuery {
146        /// Filter by who ran the action
147        pub user: Option<String>,
148        /// Filter by who the action is targetting
149        pub target: Option<String>,
150        /// Filter by the action type
151        pub r#type: Option<Vec<String>>,
152        /// Entries before a certain entry id
153        pub before: Option<String>,
154        /// Entries after a certain entry id
155        pub after: Option<String>,
156        /// Maximum number of entries to fetch
157        pub limit: i64,
158    }
159);
160
161impl AuditLogEntryAction {
162    // TODO: migrate this to a rabbitmq queue to avoid spawning lots of tasks
163    /// Generates an `AuditLogEntry` for the current action and inserts it into the database
164    pub async fn insert<R: Into<Option<String>>>(
165        self,
166        db: &Database,
167        server: String,
168        reason: R,
169        user: String,
170        target: Option<String>,
171    ) -> AuditLogEntry {
172        let config = config().await;
173
174        let id = Ulid::new();
175        let expires_at = id
176            .datetime()
177            .checked_add(Duration::from_secs(config.api.audit_logs.expires_after))
178            .unwrap()
179            .into();
180
181        let entry = AuditLogEntry {
182            id: id.to_string(),
183            expires_at,
184            server,
185            reason: reason.into(),
186            user,
187            target,
188            action: self,
189        };
190
191        // 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
192        // this will need to be redone for when we migrate to using rabbitmq here anyway.
193        #[cfg(not(test))]
194        tokio::task::spawn({
195            let db = db.clone();
196            let entry = entry.clone();
197
198            async move { revolt_config::report_internal_error!(db.insert_audit_log_entry(&entry).await) }
199        });
200
201        #[cfg(test)]
202        db.insert_audit_log_entry(&entry).await.unwrap();
203
204        entry
205    }
206}
207
208impl AuditLogEntry {
209    /// Fetches the corrasponding users and members for each audit log entry
210    pub async fn with_users(
211        db: &Database,
212        server_id: &str,
213        user: &User,
214        entries: &[Self],
215    ) -> Result<(Vec<v0::User>, Vec<v0::Member>)> {
216        let mut user_ids = HashSet::new();
217
218        for entry in entries {
219            user_ids.insert(entry.user.clone());
220
221            match &entry.action {
222                AuditLogEntryAction::MessageDelete { author, .. } => {
223                    user_ids.insert(author.clone());
224                }
225                AuditLogEntryAction::BanCreate { user } => {
226                    user_ids.insert(user.clone());
227                }
228                AuditLogEntryAction::BanDelete { user } => {
229                    user_ids.insert(user.clone());
230                }
231                AuditLogEntryAction::ChannelCreate { .. } => {}
232                AuditLogEntryAction::MemberEdit { user, .. } => {
233                    user_ids.insert(user.clone());
234                }
235                AuditLogEntryAction::MemberKick { user } => {
236                    user_ids.insert(user.clone());
237                }
238                AuditLogEntryAction::MessagePin { author, .. } => {
239                    user_ids.insert(author.clone());
240                }
241                AuditLogEntryAction::MessageUnpin { author, .. } => {
242                    user_ids.insert(author.clone());
243                }
244                AuditLogEntryAction::ServerEdit { .. } => {}
245                AuditLogEntryAction::RoleEdit { .. } => {}
246                AuditLogEntryAction::RoleCreate { .. } => {}
247                AuditLogEntryAction::RoleDelete { .. } => {}
248                AuditLogEntryAction::RolesReorder { .. } => {}
249                AuditLogEntryAction::MessageBulkDelete { .. } => {}
250                AuditLogEntryAction::ChannelEdit { .. } => {}
251                AuditLogEntryAction::ChannelRolePermissionsEdit { .. } => {}
252                AuditLogEntryAction::ChannelDelete { .. } => {}
253                AuditLogEntryAction::InviteCreate { .. } => {}
254                AuditLogEntryAction::InviteDelete { .. } => {}
255                AuditLogEntryAction::WebhookCreate { .. } => {}
256                AuditLogEntryAction::WebhookDelete { .. } => {}
257                AuditLogEntryAction::EmojiCreate { .. } => {}
258                AuditLogEntryAction::EmojiUpdate { .. } => {}
259                AuditLogEntryAction::EmojiDelete { .. } => {}
260            };
261        }
262
263        let user_ids = user_ids.into_iter().collect::<Vec<_>>();
264
265        let users = User::fetch_many_ids_as_mutuals(db, user, &user_ids).await?;
266        let members = db
267            .fetch_members(server_id, &user_ids)
268            .await?
269            .into_iter()
270            .map(Into::into)
271            .collect();
272
273        Ok((users, members))
274    }
275}