1use crate::{models::InsertQueryBuilder, prelude::*, storage::StorageUrlRetriever};
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
26impl BaseModel for ServerActivity {
27 const NAME: &'static str = "server_activity";
28
29 #[inline]
30 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
31 let prefix = prefix.unwrap_or_default();
32
33 let mut columns = BTreeMap::from([
34 (
35 "server_activities.impersonator_uuid",
36 compact_str::format_compact!("{prefix}impersonator_uuid"),
37 ),
38 (
39 "server_activities.api_key_uuid",
40 compact_str::format_compact!("{prefix}api_key_uuid"),
41 ),
42 (
43 "server_activities.schedule_uuid",
44 compact_str::format_compact!("{prefix}schedule_uuid"),
45 ),
46 (
47 "server_activities.event",
48 compact_str::format_compact!("{prefix}event"),
49 ),
50 (
51 "server_activities.ip",
52 compact_str::format_compact!("{prefix}ip"),
53 ),
54 (
55 "server_activities.data",
56 compact_str::format_compact!("{prefix}data"),
57 ),
58 (
59 "server_activities.created",
60 compact_str::format_compact!("{prefix}created"),
61 ),
62 ]);
63
64 columns.extend(super::user::User::columns(Some("user_")));
65
66 columns
67 }
68
69 #[inline]
70 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
71 let prefix = prefix.unwrap_or_default();
72
73 Ok(Self {
74 user: if row
75 .try_get::<uuid::Uuid, _>("user_uuid".to_string().as_str())
76 .is_ok()
77 {
78 Some(super::user::User::map(Some("user_"), row)?)
79 } else {
80 None
81 },
82 impersonator: super::user::User::get_fetchable_from_row(
83 row,
84 compact_str::format_compact!("{prefix}impersonator_uuid"),
85 ),
86 api_key: super::user_api_key::UserApiKey::get_fetchable_from_row(
87 row,
88 compact_str::format_compact!("{prefix}api_key_uuid"),
89 ),
90 schedule: super::server_schedule::ServerSchedule::get_fetchable_from_row(
91 row,
92 compact_str::format_compact!("{prefix}schedule_uuid"),
93 ),
94 event: row.try_get(compact_str::format_compact!("{prefix}event").as_str())?,
95 ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
96 data: row.try_get(compact_str::format_compact!("{prefix}data").as_str())?,
97 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
98 })
99 }
100}
101
102impl ServerActivity {
103 pub async fn by_server_uuid_with_pagination(
104 database: &crate::database::Database,
105 server_uuid: uuid::Uuid,
106 page: i64,
107 per_page: i64,
108 search: Option<&str>,
109 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
110 let offset = (page - 1) * per_page;
111
112 let rows = sqlx::query(&format!(
113 r#"
114 SELECT {}, COUNT(*) OVER() AS total_count
115 FROM server_activities
116 LEFT JOIN users ON users.uuid = server_activities.user_uuid
117 LEFT JOIN roles ON roles.uuid = users.role_uuid
118 WHERE server_activities.server_uuid = $1 AND ($2 IS NULL OR server_activities.event ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%')
119 ORDER BY server_activities.created DESC
120 LIMIT $3 OFFSET $4
121 "#,
122 Self::columns_sql(None)
123 ))
124 .bind(server_uuid)
125 .bind(search)
126 .bind(per_page)
127 .bind(offset)
128 .fetch_all(database.read())
129 .await?;
130
131 Ok(super::Pagination {
132 total: rows
133 .first()
134 .map_or(Ok(0), |row| row.try_get("total_count"))?,
135 per_page,
136 page,
137 data: rows
138 .into_iter()
139 .map(|row| Self::map(None, &row))
140 .try_collect_vec()?,
141 })
142 }
143
144 pub async fn delete_older_than(
145 database: &crate::database::Database,
146 cutoff: chrono::DateTime<chrono::Utc>,
147 ) -> Result<u64, crate::database::DatabaseError> {
148 let result = sqlx::query(
149 r#"
150 DELETE FROM server_activities
151 WHERE created < $1
152 "#,
153 )
154 .bind(cutoff.naive_utc())
155 .execute(database.write())
156 .await?;
157
158 Ok(result.rows_affected())
159 }
160
161 #[inline]
162 pub async fn into_api_object(
163 self,
164 database: &crate::database::Database,
165 storage_url_retriever: &StorageUrlRetriever<'_>,
166 ) -> Result<ApiServerActivity, anyhow::Error> {
167 Ok(ApiServerActivity {
168 user: self
169 .user
170 .map(|user| user.into_api_object(storage_url_retriever)),
171 impersonator: if let Some(impersonator) = self.impersonator {
172 Some(
173 impersonator
174 .fetch_cached(database)
175 .await?
176 .into_api_object(storage_url_retriever),
177 )
178 } else {
179 None
180 },
181 event: self.event,
182 ip: self.ip.map(|ip| ip.ip().to_compact_string()),
183 data: self.data,
184 is_api: self.api_key.is_some(),
185 is_schedule: self.schedule.is_some(),
186 created: self.created.and_utc(),
187 })
188 }
189}
190
191#[derive(ToSchema, Deserialize, Validate)]
192pub struct CreateServerActivityOptions {
193 #[garde(skip)]
194 pub server_uuid: uuid::Uuid,
195 #[garde(skip)]
196 pub user_uuid: Option<uuid::Uuid>,
197 #[garde(skip)]
198 pub impersonator_uuid: Option<uuid::Uuid>,
199 #[garde(skip)]
200 pub api_key_uuid: Option<uuid::Uuid>,
201 #[garde(skip)]
202 pub schedule_uuid: Option<uuid::Uuid>,
203 #[garde(length(chars, min = 1, max = 255))]
204 #[schema(min_length = 1, max_length = 255)]
205 pub event: compact_str::CompactString,
206 #[garde(skip)]
207 #[schema(value_type = Option<String>)]
208 pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
209 #[garde(skip)]
210 pub data: serde_json::Value,
211
212 #[garde(skip)]
213 pub created: Option<chrono::NaiveDateTime>,
214}
215
216#[async_trait::async_trait]
217impl CreatableModel for ServerActivity {
218 type CreateOptions<'a> = CreateServerActivityOptions;
219 type CreateResult = ();
220
221 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
222 static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerActivity>> =
223 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
224
225 &CREATE_LISTENERS
226 }
227
228 async fn create(
229 state: &crate::State,
230 mut options: Self::CreateOptions<'_>,
231 ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
232 options.validate()?;
233
234 let mut transaction = state.database.write().begin().await?;
235
236 let mut query_builder = InsertQueryBuilder::new("server_activities");
237
238 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
239 .await?;
240
241 query_builder
242 .set("server_uuid", options.server_uuid)
243 .set("user_uuid", options.user_uuid)
244 .set("impersonator_uuid", options.impersonator_uuid)
245 .set("api_key_uuid", options.api_key_uuid)
246 .set("schedule_uuid", options.schedule_uuid)
247 .set("event", &options.event)
248 .set("ip", options.ip)
249 .set("data", options.data);
250
251 if let Some(created) = options.created {
252 query_builder.set("created", created);
253 }
254
255 query_builder.execute(&mut *transaction).await?;
256
257 transaction.commit().await?;
258
259 Ok(())
260 }
261}
262
263#[derive(ToSchema, Serialize)]
264#[schema(title = "ServerActivity")]
265pub struct ApiServerActivity {
266 pub user: Option<super::user::ApiUser>,
267 pub impersonator: Option<super::user::ApiUser>,
268
269 pub event: compact_str::CompactString,
270 pub ip: Option<compact_str::CompactString>,
271 pub data: serde_json::Value,
272
273 pub is_api: bool,
274 pub is_schedule: bool,
275
276 pub created: chrono::DateTime<chrono::Utc>,
277}