Skip to main content

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