tetratto_core/database/
stacks.rs

1use oiseau::cache::Cache;
2use crate::{
3    database::posts::FullPost,
4    model::{
5        auth::User,
6        permissions::FinePermission,
7        stacks::{StackMode, StackPrivacy, StackSort, UserStack},
8        Error, Result,
9    },
10};
11use crate::{auto_method, DataManager};
12use oiseau::{PostgresRow, execute, get, query_rows, params};
13
14impl DataManager {
15    /// Get a [`UserStack`] from an SQL row.
16    pub(crate) fn get_stack_from_row(x: &PostgresRow) -> UserStack {
17        UserStack {
18            id: get!(x->0(i64)) as usize,
19            created: get!(x->1(i64)) as usize,
20            owner: get!(x->2(i64)) as usize,
21            name: get!(x->3(String)),
22            users: serde_json::from_str(&get!(x->4(String))).unwrap(),
23            privacy: serde_json::from_str(&get!(x->5(String))).unwrap(),
24            mode: serde_json::from_str(&get!(x->6(String))).unwrap(),
25            sort: serde_json::from_str(&get!(x->7(String))).unwrap(),
26        }
27    }
28
29    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:{}");
30
31    pub async fn get_stack_posts(
32        &self,
33        as_user_id: usize,
34        id: usize,
35        batch: usize,
36        page: usize,
37        ignore_users: &Vec<usize>,
38        user: &Option<User>,
39    ) -> Result<Vec<FullPost>> {
40        let stack = self.get_stack_by_id(id).await?;
41
42        Ok(match stack.mode {
43            StackMode::Include => {
44                self.fill_posts_with_community(
45                    self.get_posts_from_stack(id, batch, page, stack.sort)
46                        .await?,
47                    as_user_id,
48                    ignore_users,
49                    user,
50                )
51                .await?
52            }
53            StackMode::Exclude => {
54                let ignore_users = [ignore_users.to_owned(), stack.users].concat();
55
56                match stack.sort {
57                    StackSort::Created => {
58                        self.fill_posts_with_community(
59                            self.get_latest_posts(batch, page, &user, 0).await?,
60                            as_user_id,
61                            &ignore_users,
62                            user,
63                        )
64                        .await?
65                    }
66                    StackSort::Likes => {
67                        self.fill_posts_with_community(
68                            self.get_popular_posts(batch, page, 604_800_000).await?,
69                            as_user_id,
70                            &ignore_users,
71                            user,
72                        )
73                        .await?
74                    }
75                }
76            }
77            StackMode::BlockList => {
78                return Err(Error::MiscError(
79                    "You should use `get_stack_users` for this type".to_string(),
80                ));
81            }
82            StackMode::Circle => {
83                if !stack.users.contains(&as_user_id) && as_user_id != stack.owner {
84                    return Err(Error::NotAllowed);
85                }
86
87                self.fill_posts_with_community(
88                    self.get_posts_by_stack(stack.id, batch, page).await?,
89                    as_user_id,
90                    &ignore_users,
91                    user,
92                )
93                .await?
94            }
95        })
96    }
97
98    pub async fn get_stack_users(&self, id: usize, batch: usize, page: usize) -> Result<Vec<User>> {
99        let stack = self.get_stack_by_id(id).await?;
100
101        if stack.mode != StackMode::BlockList {
102            return Err(Error::MiscError(
103                "You should use `get_stack_posts` for this type".to_string(),
104            ));
105        }
106
107        // build list
108        let mut out = Vec::new();
109        let mut i = 0;
110
111        for user in stack.users.iter().skip(batch * page) {
112            if i == batch {
113                break;
114            }
115
116            out.push(self.get_user_by_id(user.to_owned()).await?);
117            i += 1;
118        }
119
120        Ok(out)
121    }
122
123    /// Get all stacks by user.
124    ///
125    /// Also pulls stacks that are of "Circle" type AND the user is added to the `users` list.
126    ///
127    /// # Arguments
128    /// * `id` - the ID of the user to fetch stacks for
129    pub async fn get_stacks_by_user(&self, id: usize) -> Result<Vec<UserStack>> {
130        let conn = match self.0.connect().await {
131            Ok(c) => c,
132            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
133        };
134
135        let res = query_rows!(
136            &conn,
137            "SELECT * FROM stacks WHERE owner = $1 OR (mode = '\"Circle\"' AND users LIKE $2) ORDER BY name ASC",
138            &[&(id as i64), &format!("%{id}%")],
139            |x| { Self::get_stack_from_row(x) }
140        );
141
142        if res.is_err() {
143            return Err(Error::GeneralNotFound("stack".to_string()));
144        }
145
146        Ok(res.unwrap())
147    }
148
149    const MAXIMUM_FREE_STACKS: usize = 5;
150    pub const MAXIMUM_FREE_STACK_USERS: usize = 50;
151
152    /// Create a new stack in the database.
153    ///
154    /// # Arguments
155    /// * `data` - a mock [`UserStack`] object to insert
156    pub async fn create_stack(&self, data: UserStack) -> Result<UserStack> {
157        // check values
158        if data.name.trim().len() < 2 {
159            return Err(Error::DataTooShort("title".to_string()));
160        } else if data.name.len() > 32 {
161            return Err(Error::DataTooLong("title".to_string()));
162        }
163
164        // check number of stacks
165        let owner = self.get_user_by_id(data.owner).await?;
166
167        if !owner.permissions.check(FinePermission::SUPPORTER) {
168            let stacks = self
169                .get_table_row_count_where("stacks", &format!("owner = {}", owner.id))
170                .await? as usize;
171
172            if stacks >= Self::MAXIMUM_FREE_STACKS {
173                return Err(Error::MiscError(
174                    "You already have the maximum number of stacks you can have".to_string(),
175                ));
176            }
177        }
178
179        // ...
180        let conn = match self.0.connect().await {
181            Ok(c) => c,
182            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
183        };
184
185        let res = execute!(
186            &conn,
187            "INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
188            params![
189                &(data.id as i64),
190                &(data.created as i64),
191                &(data.owner as i64),
192                &data.name,
193                &serde_json::to_string(&data.users).unwrap(),
194                &serde_json::to_string(&data.privacy).unwrap(),
195                &serde_json::to_string(&data.mode).unwrap(),
196                &serde_json::to_string(&data.sort).unwrap(),
197            ]
198        );
199
200        if let Err(e) = res {
201            return Err(Error::DatabaseError(e.to_string()));
202        }
203
204        Ok(data)
205    }
206
207    pub async fn delete_stack(&self, id: usize, user: &User) -> Result<()> {
208        let stack = self.get_stack_by_id(id).await?;
209
210        // check user permission
211        if user.id != stack.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) {
212            return Err(Error::NotAllowed);
213        }
214
215        // ...
216        let conn = match self.0.connect().await {
217            Ok(c) => c,
218            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
219        };
220
221        let res = execute!(&conn, "DELETE FROM stacks WHERE id = $1", &[&(id as i64)]);
222
223        if let Err(e) = res {
224            return Err(Error::DatabaseError(e.to_string()));
225        }
226
227        // delete stackblocks
228        let res = execute!(
229            &conn,
230            "DELETE FROM stackblocks WHERE stack = $1",
231            &[&(id as i64)]
232        );
233
234        if let Err(e) = res {
235            return Err(Error::DatabaseError(e.to_string()));
236        }
237
238        // delete posts
239        let res = execute!(&conn, "DELETE FROM posts WHERE stack = $1", &[&(id as i64)]);
240
241        if let Err(e) = res {
242            return Err(Error::DatabaseError(e.to_string()));
243        }
244
245        // ...
246        self.0.1.remove(format!("atto.stack:{}", id)).await;
247        Ok(())
248    }
249
250    /// Clone the given stack.
251    pub async fn clone_stack(&self, owner: usize, stack: usize) -> Result<UserStack> {
252        let stack = self.get_stack_by_id(stack).await?;
253        self.create_stack(UserStack::new(stack.name, owner, stack.users))
254            .await
255    }
256
257    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:{}");
258    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:{}");
259
260    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:{}");
261    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:{}");
262    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:{}");
263}