tetratto-core 16.0.1

The core behind Tetratto
Documentation
use oiseau::cache::Cache;
use crate::{
    database::posts::FullPost,
    model::{
        auth::User,
        permissions::FinePermission,
        stacks::{StackMode, StackPrivacy, StackSort, UserStack},
        Error, Result,
    },
};
use crate::{auto_method, DataManager};
use oiseau::{PostgresRow, execute, get, query_rows, params};

impl DataManager {
    /// Get a [`UserStack`] from an SQL row.
    pub(crate) fn get_stack_from_row(x: &PostgresRow) -> UserStack {
        UserStack {
            id: get!(x->0(i64)) as usize,
            created: get!(x->1(i64)) as usize,
            owner: get!(x->2(i64)) as usize,
            name: get!(x->3(String)),
            users: serde_json::from_str(&get!(x->4(String))).unwrap(),
            privacy: serde_json::from_str(&get!(x->5(String))).unwrap(),
            mode: serde_json::from_str(&get!(x->6(String))).unwrap(),
            sort: serde_json::from_str(&get!(x->7(String))).unwrap(),
            is_locked: get!(x->8(i32)) == 1,
        }
    }

    auto_method!(get_stack_by_id(usize as i64)@get_stack_from_row -> "SELECT * FROM stacks WHERE id = $1" --name="stack" --returns=UserStack --cache-key-tmpl="atto.stack:{}");

    pub async fn get_stack_posts(
        &self,
        as_user_id: usize,
        id: usize,
        batch: usize,
        page: usize,
        ignore_users: &Vec<usize>,
        user: &Option<User>,
    ) -> Result<Vec<FullPost>> {
        let stack = self.get_stack_by_id(id).await?;

        Ok(match stack.mode {
            StackMode::Include => {
                self.fill_posts_with_community(
                    self.get_posts_from_stack(id, batch, page, stack.sort)
                        .await?,
                    as_user_id,
                    ignore_users,
                    user,
                )
                .await?
            }
            StackMode::Exclude => {
                let ignore_users = [ignore_users.to_owned(), stack.users].concat();

                match stack.sort {
                    StackSort::Created => {
                        self.fill_posts_with_community(
                            self.get_latest_posts(batch, &user, 0).await?,
                            as_user_id,
                            &ignore_users,
                            user,
                        )
                        .await?
                    }
                    StackSort::Likes => {
                        self.fill_posts_with_community(
                            self.get_popular_posts(batch, page, 604_800_000).await?,
                            as_user_id,
                            &ignore_users,
                            user,
                        )
                        .await?
                    }
                }
            }
            StackMode::BlockList => {
                return Err(Error::MiscError(
                    "You should use `get_stack_users` for this type".to_string(),
                ));
            }
            StackMode::Circle => {
                if !stack.users.contains(&as_user_id) && as_user_id != stack.owner {
                    return Err(Error::NotAllowed);
                }

                self.fill_posts_with_community(
                    self.get_posts_by_stack(stack.id, batch, page).await?,
                    as_user_id,
                    &ignore_users,
                    user,
                )
                .await?
            }
        })
    }

    pub async fn get_stack_users(&self, id: usize, batch: usize, page: usize) -> Result<Vec<User>> {
        let stack = self.get_stack_by_id(id).await?;

        if stack.mode != StackMode::BlockList {
            return Err(Error::MiscError(
                "You should use `get_stack_posts` for this type".to_string(),
            ));
        }

        // build list
        let mut out = Vec::new();
        let mut i = 0;

        for user in stack.users.iter().skip(batch * page) {
            if i == batch {
                break;
            }

            out.push(self.get_user_by_id(user.to_owned()).await?);
            i += 1;
        }

        Ok(out)
    }

    /// Get all stacks by user.
    ///
    /// Also pulls stacks that are of "Circle" type AND the user is added to the `users` list.
    ///
    /// # Arguments
    /// * `id` - the ID of the user to fetch stacks for
    pub async fn get_stacks_by_user(&self, id: usize) -> Result<Vec<UserStack>> {
        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 stacks WHERE owner = $1 OR (mode = '\"Circle\"' AND users LIKE $2) ORDER BY name ASC",
            &[&(id as i64), &format!("%{id}%")],
            |x| { Self::get_stack_from_row(x) }
        );

        if res.is_err() {
            return Err(Error::GeneralNotFound("stack".to_string()));
        }

        Ok(res.unwrap())
    }

    const MAXIMUM_FREE_STACKS: usize = 5;
    pub const MAXIMUM_FREE_STACK_USERS: usize = 50;

    /// Create a new stack in the database.
    ///
    /// # Arguments
    /// * `data` - a mock [`UserStack`] object to insert
    pub async fn create_stack(&self, data: UserStack) -> Result<UserStack> {
        // check values
        if data.name.trim().len() < 2 {
            return Err(Error::DataTooShort("title".to_string()));
        } else if data.name.len() > 32 {
            return Err(Error::DataTooLong("title".to_string()));
        }

        // check number of stacks
        let owner = self.get_user_by_id(data.owner).await?;

        if !owner.permissions.check(FinePermission::SUPPORTER) {
            let stacks = self
                .get_table_row_count_where("stacks", &format!("owner = {}", owner.id))
                .await? as usize;

            if stacks >= Self::MAXIMUM_FREE_STACKS {
                return Err(Error::MiscError(
                    "You already have the maximum number of stacks you can have".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 stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
            params![
                &(data.id as i64),
                &(data.created as i64),
                &(data.owner as i64),
                &data.name,
                &serde_json::to_string(&data.users).unwrap(),
                &serde_json::to_string(&data.privacy).unwrap(),
                &serde_json::to_string(&data.mode).unwrap(),
                &serde_json::to_string(&data.sort).unwrap(),
                &if data.is_locked { 1 } else { 0 },
            ]
        );

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

        Ok(data)
    }

    pub async fn delete_stack(&self, id: usize, user: &User) -> Result<()> {
        let stack = self.get_stack_by_id(id).await?;

        if stack.is_locked {
            return Err(Error::NotAllowed);
        }

        // check user permission
        if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) {
            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 stacks WHERE id = $1", &[&(id as i64)]);

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

        // delete stackblocks
        let res = execute!(
            &conn,
            "DELETE FROM stackblocks WHERE stack = $1",
            &[&(id as i64)]
        );

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

        // delete posts
        let res = execute!(&conn, "DELETE FROM posts WHERE stack = $1", &[&(id as i64)]);

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

        // ...
        self.0.1.remove(format!("atto.stack:{}", id)).await;
        Ok(())
    }

    /// Clone the given stack.
    pub async fn clone_stack(&self, owner: usize, stack: usize) -> Result<UserStack> {
        let stack = self.get_stack_by_id(stack).await?;
        self.create_stack(UserStack::new(stack.name, owner, stack.users))
            .await
    }

    auto_method!(update_stack_name(&str)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.stack:{}");
    auto_method!(update_stack_users(Vec<usize>)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET users = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}");

    auto_method!(update_stack_privacy(StackPrivacy)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}");
    auto_method!(update_stack_mode(StackMode)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET mode = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}");
    auto_method!(update_stack_sort(StackSort)@get_stack_by_id:FinePermission::MANAGE_STACKS; -> "UPDATE stacks SET sort = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.stack:{}");
}