1use crate::{models::InsertQueryBuilder, prelude::*};
2use compact_str::ToCompactString;
3use garde::Validate;
4use serde::{Deserialize, Serialize};
5use sqlx::{Row, postgres::PgRow};
6use std::{
7 collections::BTreeMap,
8 sync::{Arc, LazyLock},
9};
10use utoipa::ToSchema;
11
12#[derive(Serialize, Deserialize)]
13pub struct ServerActivity {
14 pub user: Option<super::user::User>,
15 pub impersonator: Option<Fetchable<super::user::User>>,
16 pub api_key: Option<Fetchable<super::user_api_key::UserApiKey>>,
17 pub schedule: Option<Fetchable<super::server_schedule::ServerSchedule>>,
18
19 pub event: compact_str::CompactString,
20 pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
21 pub data: serde_json::Value,
22
23 pub created: chrono::NaiveDateTime,
24
25 extension_data: super::ModelExtensionData,
26}
27
28impl BaseModel for ServerActivity {
29 const NAME: &'static str = "server_activity";
30
31 fn get_extension_list() -> &'static super::ModelExtensionList {
32 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
33 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
34
35 &EXTENSIONS
36 }
37
38 fn get_extension_data(&self) -> &super::ModelExtensionData {
39 &self.extension_data
40 }
41
42 #[inline]
43 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
44 let prefix = prefix.unwrap_or_default();
45
46 let mut columns = BTreeMap::from([
47 (
48 "server_activities.impersonator_uuid",
49 compact_str::format_compact!("{prefix}impersonator_uuid"),
50 ),
51 (
52 "server_activities.api_key_uuid",
53 compact_str::format_compact!("{prefix}api_key_uuid"),
54 ),
55 (
56 "server_activities.schedule_uuid",
57 compact_str::format_compact!("{prefix}schedule_uuid"),
58 ),
59 (
60 "server_activities.event",
61 compact_str::format_compact!("{prefix}event"),
62 ),
63 (
64 "server_activities.ip",
65 compact_str::format_compact!("{prefix}ip"),
66 ),
67 (
68 "server_activities.data",
69 compact_str::format_compact!("{prefix}data"),
70 ),
71 (
72 "server_activities.created",
73 compact_str::format_compact!("{prefix}created"),
74 ),
75 ]);
76
77 columns.extend(super::user::User::base_columns(Some("user_")));
78
79 columns
80 }
81
82 #[inline]
83 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
84 let prefix = prefix.unwrap_or_default();
85
86 Ok(Self {
87 user: if row
88 .try_get::<uuid::Uuid, _>("user_uuid".to_string().as_str())
89 .is_ok()
90 {
91 Some(super::user::User::map(Some("user_"), row)?)
92 } else {
93 None
94 },
95 impersonator: super::user::User::get_fetchable_from_row(
96 row,
97 compact_str::format_compact!("{prefix}impersonator_uuid"),
98 ),
99 api_key: super::user_api_key::UserApiKey::get_fetchable_from_row(
100 row,
101 compact_str::format_compact!("{prefix}api_key_uuid"),
102 ),
103 schedule: super::server_schedule::ServerSchedule::get_fetchable_from_row(
104 row,
105 compact_str::format_compact!("{prefix}schedule_uuid"),
106 ),
107 event: row.try_get(compact_str::format_compact!("{prefix}event").as_str())?,
108 ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
109 data: row.try_get(compact_str::format_compact!("{prefix}data").as_str())?,
110 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
111 extension_data: Self::map_extensions(prefix, row)?,
112 })
113 }
114}
115
116impl ServerActivity {
117 pub async fn by_server_uuid_with_pagination(
118 database: &crate::database::Database,
119 server_uuid: uuid::Uuid,
120 page: i64,
121 per_page: i64,
122 search: Option<&str>,
123 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
124 let offset = (page - 1) * per_page;
125
126 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
127 r#"
128 SELECT {}, COUNT(*) OVER() AS total_count
129 FROM server_activities
130 LEFT JOIN users ON users.uuid = server_activities.user_uuid
131 LEFT JOIN roles ON roles.uuid = users.role_uuid
132 WHERE server_activities.server_uuid = $1 AND ($2 IS NULL OR server_activities.event ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%')
133 ORDER BY server_activities.created DESC
134 LIMIT $3 OFFSET $4
135 "#,
136 Self::columns_sql(None)
137 )))
138 .bind(server_uuid)
139 .bind(search)
140 .bind(per_page)
141 .bind(offset)
142 .fetch_all(database.read())
143 .await?;
144
145 Ok(super::Pagination {
146 total: rows
147 .first()
148 .map_or(Ok(0), |row| row.try_get("total_count"))?,
149 per_page,
150 page,
151 data: rows
152 .into_iter()
153 .map(|row| Self::map(None, &row))
154 .try_collect_vec()?,
155 })
156 }
157
158 pub async fn by_server_uuid_user_uuid_with_pagination(
159 database: &crate::database::Database,
160 server_uuid: uuid::Uuid,
161 user_uuid: uuid::Uuid,
162 page: i64,
163 per_page: i64,
164 search: Option<&str>,
165 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
166 let offset = (page - 1) * per_page;
167
168 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
169 r#"
170 SELECT {}, COUNT(*) OVER() AS total_count
171 FROM server_activities
172 LEFT JOIN users ON users.uuid = server_activities.user_uuid
173 LEFT JOIN roles ON roles.uuid = users.role_uuid
174 WHERE server_activities.server_uuid = $1 AND server_activities.user_uuid = $2
175 AND ($3 IS NULL OR server_activities.event ILIKE '%' || $3 || '%')
176 ORDER BY server_activities.created DESC
177 LIMIT $4 OFFSET $5
178 "#,
179 Self::columns_sql(None)
180 )))
181 .bind(server_uuid)
182 .bind(user_uuid)
183 .bind(search)
184 .bind(per_page)
185 .bind(offset)
186 .fetch_all(database.read())
187 .await?;
188
189 Ok(super::Pagination {
190 total: rows
191 .first()
192 .map_or(Ok(0), |row| row.try_get("total_count"))?,
193 per_page,
194 page,
195 data: rows
196 .into_iter()
197 .map(|row| Self::map(None, &row))
198 .try_collect_vec()?,
199 })
200 }
201
202 pub async fn delete_older_than(
203 database: &crate::database::Database,
204 cutoff: chrono::DateTime<chrono::Utc>,
205 ) -> Result<u64, crate::database::DatabaseError> {
206 let result = sqlx::query(
207 r#"
208 DELETE FROM server_activities
209 WHERE server_activities.created < $1
210 "#,
211 )
212 .bind(cutoff.naive_utc())
213 .execute(database.write())
214 .await?;
215
216 Ok(result.rows_affected())
217 }
218
219 pub async fn retain_latest_logs_per_server(
220 database: &crate::database::Database,
221 keep_count: i64,
222 ) -> Result<u64, crate::database::DatabaseError> {
223 let result = sqlx::query(
224 r#"
225 DELETE FROM server_activities
226 WHERE ctid IN (
227 SELECT ctid
228 FROM (
229 SELECT
230 ctid,
231 ROW_NUMBER() OVER (PARTITION BY server_activities.server_uuid ORDER BY server_activities.created DESC) rn
232 FROM server_activities
233 ) sub
234 WHERE sub.rn > $1
235 )
236 "#,
237 )
238 .bind(keep_count)
239 .execute(database.write())
240 .await?;
241
242 Ok(result.rows_affected())
243 }
244}
245
246#[async_trait::async_trait]
247impl IntoApiObject for ServerActivity {
248 type ApiObject = ApiServerActivity;
249 type ExtraArgs<'a> = (&'a crate::storage::StorageUrlRetriever<'a>, bool);
250
251 async fn into_api_object<'a>(
252 self,
253 state: &crate::State,
254 (storage_url_retriever, show_ip): Self::ExtraArgs<'a>,
255 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
256 let api_object = ApiServerActivity::init_hooks(&self, state).await?;
257
258 let user = if let Some(user) = self.user {
259 Some(user.into_api_object(state, storage_url_retriever).await?)
260 } else {
261 None
262 };
263
264 let impersonator = if let Some(impersonator) = self.impersonator {
265 Some(
266 impersonator
267 .fetch_cached(&state.database)
268 .await?
269 .into_api_object(state, storage_url_retriever)
270 .await?,
271 )
272 } else {
273 None
274 };
275
276 let api_object = finish_extendible!(
277 ApiServerActivity {
278 user,
279 impersonator,
280 event: self.event,
281 ip: if show_ip {
282 self.ip.map(|ip| ip.ip().to_compact_string())
283 } else {
284 None
285 },
286 data: self.data,
287 is_api: self.api_key.is_some(),
288 is_schedule: self.schedule.is_some(),
289 created: self.created.and_utc(),
290 },
291 api_object,
292 state
293 )?;
294
295 Ok(api_object)
296 }
297}
298
299#[derive(ToSchema, Deserialize, Validate)]
300pub struct CreateServerActivityOptions {
301 #[garde(skip)]
302 pub server_uuid: uuid::Uuid,
303 #[garde(skip)]
304 pub user_uuid: Option<uuid::Uuid>,
305 #[garde(skip)]
306 pub impersonator_uuid: Option<uuid::Uuid>,
307 #[garde(skip)]
308 pub api_key_uuid: Option<uuid::Uuid>,
309 #[garde(skip)]
310 pub schedule_uuid: Option<uuid::Uuid>,
311 #[garde(length(chars, min = 1, max = 255))]
312 #[schema(min_length = 1, max_length = 255)]
313 pub event: compact_str::CompactString,
314 #[garde(skip)]
315 #[schema(value_type = Option<String>)]
316 pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
317 #[garde(skip)]
318 pub data: serde_json::Value,
319
320 #[garde(skip)]
321 pub created: Option<chrono::NaiveDateTime>,
322}
323
324#[async_trait::async_trait]
325impl CreatableModel for ServerActivity {
326 type CreateOptions<'a> = CreateServerActivityOptions;
327 type CreateResult = ();
328
329 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
330 static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerActivity>> =
331 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
332
333 &CREATE_LISTENERS
334 }
335
336 async fn create_with_transaction(
337 state: &crate::State,
338 mut options: Self::CreateOptions<'_>,
339 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
340 ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
341 options.validate()?;
342
343 let mut query_builder = InsertQueryBuilder::new("server_activities");
344
345 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
346
347 query_builder
348 .set("server_uuid", options.server_uuid)
349 .set("user_uuid", options.user_uuid)
350 .set("impersonator_uuid", options.impersonator_uuid)
351 .set("api_key_uuid", options.api_key_uuid)
352 .set("schedule_uuid", options.schedule_uuid)
353 .set("event", &options.event)
354 .set("ip", options.ip)
355 .set("data", &options.data);
356
357 if let Some(created) = options.created {
358 query_builder.set("created", created);
359 }
360
361 query_builder.execute(&mut **transaction).await?;
362
363 let mut result = ();
364
365 Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
366
367 Ok(result)
368 }
369}
370
371#[schema_extension_derive::extendible]
372#[init_args(ServerActivity, crate::State)]
373#[hook_args(crate::State)]
374#[derive(ToSchema, Serialize)]
375#[schema(title = "ServerActivity")]
376pub struct ApiServerActivity {
377 pub user: Option<super::user::ApiUser>,
378 pub impersonator: Option<super::user::ApiUser>,
379
380 pub event: compact_str::CompactString,
381 pub ip: Option<compact_str::CompactString>,
382 pub data: serde_json::Value,
383
384 pub is_api: bool,
385 pub is_schedule: bool,
386
387 pub created: chrono::DateTime<chrono::Utc>,
388}