use std::collections::HashMap; use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result}; use crate::{auto_method, DataManager}; use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; impl DataManager { /// Get a [`Letter`] from an SQL row. pub(crate) fn get_letter_from_row(x: &PostgresRow) -> Letter { Letter { id: get!(x->0(i64)) as usize, created: get!(x->1(i64)) as usize, owner: get!(x->2(i64)) as usize, receivers: serde_json::from_str(&get!(x->3(String))).unwrap(), subject: get!(x->4(String)), content: get!(x->5(String)), read_by: serde_json::from_str(&get!(x->6(String))).unwrap(), replying_to: get!(x->7(i64)) as usize, } } auto_method!(get_letter_by_id(usize as i64)@get_letter_from_row -> "SELECT * FROM letters WHERE id = $1" --name="letter" --returns=Letter --cache-key-tmpl="atto.letter:{}"); /// Get all letters by user. /// /// # Arguments /// * `id` - the ID of the user to fetch letters for /// * `batch` - the limit of items in each page /// * `page` - the page number pub async fn get_letters_by_user( &self, id: usize, batch: usize, page: usize, ) -> Result> { let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; let res = query_rows!( &conn, "SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", &[&(id as i64), &(batch as i64), &((page * batch) as i64)], |x| { Self::get_letter_from_row(x) } ); if res.is_err() { return Err(Error::GeneralNotFound("letter".to_string())); } Ok(res.unwrap()) } /// Get all letters by user (where user is a receiver). /// /// # Arguments /// * `id` - the ID of the user to fetch letters for /// * `batch` - the limit of items in each page /// * `page` - the page number pub async fn get_received_letters_by_user( &self, id: usize, batch: usize, page: usize, ) -> Result> { let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; let res = query_rows!( &conn, "SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3", &[ &format!("%{id}%"), &(batch as i64), &((page * batch) as i64) ], |x| { Self::get_letter_from_row(x) } ); if res.is_err() { return Err(Error::GeneralNotFound("letter".to_string())); } Ok(res.unwrap()) } /// Get all letters which are replying to the given letter. /// /// # Arguments /// * `id` - the ID of the letter to fetch letters for /// * `batch` - the limit of items in each page /// * `page` - the page number pub async fn get_letters_by_replying_to( &self, id: usize, batch: usize, page: usize, ) -> Result> { let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; let res = query_rows!( &conn, "SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3", &[&(id as i64), &(batch as i64), &((page * batch) as i64)], |x| { Self::get_letter_from_row(x) } ); if res.is_err() { return Err(Error::GeneralNotFound("letter".to_string())); } Ok(res.unwrap()) } /// Fill a list of letters with their owner. pub async fn fill_letters(&self, letters: Vec) -> Result> { let mut seen_users: HashMap = HashMap::new(); let mut out = Vec::new(); for letter in letters { out.push(if let Some(ua) = seen_users.get(&letter.owner) { (ua.to_owned(), letter) } else { let user = self.get_user_by_id(letter.owner).await?; seen_users.insert(letter.owner, user.clone()); (user, letter) }) } Ok(out) } /// Create a new letter in the database. /// /// # Arguments /// * `data` - a mock [`Letter`] object to insert pub async fn create_letter(&self, data: Letter) -> Result { // check values if data.subject.len() < 2 { return Err(Error::DataTooShort("subject".to_string())); } else if data.subject.len() > 256 { return Err(Error::DataTooLong("subject".to_string())); } if data.content.len() < 2 { return Err(Error::DataTooShort("content".to_string())); } else if data.content.len() > 16384 { return Err(Error::DataTooLong("content".to_string())); } if data.receivers.len() < 1 { return Err(Error::DataTooShort("receivers".to_string())); } else if data.receivers.len() > 10 { return Err(Error::DataTooLong("receivers".to_string())); } // ... let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; let res = execute!( &conn, "INSERT INTO letters VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", params![ &(data.id as i64), &(data.created as i64), &(data.owner as i64), &serde_json::to_string(&data.receivers).unwrap(), &data.subject, &data.content, &serde_json::to_string(&data.read_by).unwrap(), &(data.replying_to as i64) ] ); if let Err(e) = res { return Err(Error::DatabaseError(e.to_string())); } Ok(data) } pub async fn delete_letter(&self, id: usize, user: &User) -> Result<()> { let letter = self.get_letter_by_id(id).await?; // check user permission if user.id != letter.owner && !user .secondary_permissions .check(SecondaryPermission::MANAGE_LETTERS) { return Err(Error::NotAllowed); } // ... let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; let res = execute!(&conn, "DELETE FROM letters WHERE id = $1", &[&(id as i64)]); if let Err(e) = res { return Err(Error::DatabaseError(e.to_string())); } // ... self.0.1.remove(format!("atto.letter:{}", id)).await; Ok(()) } auto_method!(update_letter_read_by(Vec) -> "UPDATE letters SET read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.letter:{}"); }