tetratto-core 18.0.1

The core behind Tetratto
Documentation
use std::collections::HashMap;

use crate::model::auth::Notification;
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,
            likes: get!(x->8(i32)) as isize,
            dislikes: get!(x->9(i32)) as isize,
        }
    }

    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<Vec<Letter>> {
        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<Vec<Letter>> {
        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<Vec<Letter>> {
        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<Letter>) -> Result<Vec<(User, Letter)>> {
        let mut seen_users: HashMap<usize, User> = 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<Letter> {
        // 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.is_empty() {
            return Err(Error::DataTooShort("receivers".to_string()));
        } else if data.receivers.len() > 10 {
            return Err(Error::DataTooLong("receivers".to_string()));
        }

        // get sender
        let sender = self.get_user_by_id(data.owner).await?;

        // ...
        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, $9, $10)",
            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),
                &(data.likes as i32),
                &(data.dislikes as i32),
            ]
        );

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        // send notifications
        for x in &data.receivers {
            self.create_notification(Notification::new(
                "You've got mail!".to_string(),
                format!(
                    "[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}).",
                    sender.username, sender.id, data.id
                ),
                *x,
            ))
            .await?;
        }

        // ...
        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<usize>) -> "UPDATE letters SET read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.letter:{}");

    auto_method!(incr_letter_likes() -> "UPDATE letters SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --incr);
    auto_method!(incr_letter_dislikes() -> "UPDATE letters SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --incr);
    auto_method!(decr_letter_likes()@get_letter_by_id -> "UPDATE letters SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --decr=likes);
    auto_method!(decr_letter_dislikes()@get_letter_by_id -> "UPDATE letters SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --decr=dislikes);
}