use std::ops::DerefMut; use super::{ role::Role, user::{ManageUserUser, User}, }; use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; #[derive(FromRow, Debug, Serialize, Deserialize, Clone)] pub struct Activity { pub id: i64, pub created_at: NaiveDateTime, pub text: String, pub relevant_for: String, pub keep_until: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct ActivityWithDetails { #[serde(flatten)] pub(crate) activity: Activity, keep_until_days: Option, } impl From for ActivityWithDetails { fn from(activity: Activity) -> Self { let keep_until_days = activity.keep_until.map(|keep_until| { let now = Utc::now().naive_utc(); let duration = keep_until.signed_duration_since(now); duration.num_days() }); Self { keep_until_days, activity, } } } // TODO: add `reason` as additional db field, to be able to query and show this to the users pub enum Reason<'a> { Auth(ReasonAuth<'a>), // `User` changed the data of `User`, explanation in `String` UserDataChange(&'a ManageUserUser, &'a User, String), // New Note for User NewUserNote(&'a ManageUserUser, &'a User, String), } impl From> for ActivityBuilder { fn from(value: Reason<'_>) -> Self { match value { Reason::Auth(auth) => auth.into(), Reason::UserDataChange(changed_by, changed_user, explanation) => Self::new(&format!( "{changed_by} hat die Daten von {changed_user} aktualisiert: {explanation}" )) .relevant_for_user(changed_user), Reason::NewUserNote(changed_by, user, explanation) => { Self::new(&format!("({changed_by}) {explanation}")).relevant_for_user(user) } } } } pub enum ReasonAuth<'a> { // `User` tried to login with `String` as UserAgent SuccLogin(&'a User, String), // `User` tried to login which was already deleted DeletedUserLogin(&'a User), // `User` tried to login, supplied wrong PW WrongPw(&'a User), } impl<'a> From> for Reason<'a> { fn from(auth_reason: ReasonAuth<'a>) -> Self { Reason::Auth(auth_reason) } } impl From> for ActivityBuilder { fn from(value: ReasonAuth<'_>) -> Self { match value { ReasonAuth::SuccLogin(user, agent) => { Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})")) .relevant_for_user(user) .keep_until_days(7) } ReasonAuth::DeletedUserLogin(user) => Self::new(&format!( "User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." )) .relevant_for_user(user) .keep_until_days(30), ReasonAuth::WrongPw(user) => Self::new(&format!( "User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben." )) .relevant_for_user(user) .keep_until_days(7), } } } pub struct ActivityBuilder { text: String, relevant_for: String, keep_until: Option, } impl ActivityBuilder { /// TODO: maybe make this private, and only allow specific acitivites defined in `Reason` #[must_use] pub fn new(text: &str) -> Self { Self { text: text.into(), relevant_for: String::new(), keep_until: None, } } #[must_use] pub fn relevant_for_user(self, user: &User) -> Self { Self { relevant_for: format!("{}user-{};", self.relevant_for, user.id), ..self } } #[must_use] pub fn relevant_for_role(self, role: &Role) -> Self { Self { relevant_for: format!("{}role-{};", self.relevant_for, role.id), ..self } } #[must_use] pub fn keep_until_days(self, days: i64) -> Self { let now = Utc::now().naive_utc(); Self { keep_until: Some(now + Duration::days(days)), ..self } } pub async fn save(self, db: &SqlitePool) { Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await; } pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) { Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await; } } impl Activity { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { sqlx::query_as!( Self, "SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?", id ) .fetch_one(db) .await .ok() } pub(super) async fn create_with_tx( db: &mut Transaction<'_, Sqlite>, text: &str, relevant_for: &str, keep_until: Option, ) { sqlx::query!( "INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)", text, relevant_for, keep_until ) .execute(db.deref_mut()) .await .unwrap(); } pub(super) async fn create( db: &SqlitePool, text: &str, relevant_for: &str, keep_until: Option, ) { let mut tx = db.begin().await.unwrap(); Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await; tx.commit().await.unwrap(); } pub async fn for_user(db: &SqlitePool, user: &User) -> Vec { let user_str = format!("user-{};", user.id); sqlx::query_as!( Self, " SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE relevant_for like CONCAT('%', ?, '%') ORDER BY created_at DESC; ", user_str ) .fetch_all(db) .await .unwrap() } async fn last(db: &SqlitePool) -> Vec { sqlx::query_as!( Self, " SELECT id, created_at, text, relevant_for, keep_until FROM activity ORDER BY id DESC LIMIT 1000 " ) .fetch_all(db) .await .unwrap() } pub async fn show(db: &SqlitePool) -> String { let mut ret = String::new(); for log in Self::last(db).await { let utc_time: DateTime = Utc::from_utc_datetime(&Utc, &log.created_at); let local_time = utc_time.with_timezone(&Local); ret.push_str(&format!("- {local_time}: {}\n", log.text)); } ret } }