tetratto_core/database/
letters.rs

1use std::collections::HashMap;
2
3use crate::model::auth::Notification;
4use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result};
5use crate::{auto_method, DataManager};
6use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
7
8impl DataManager {
9    /// Get a [`Letter`] from an SQL row.
10    pub(crate) fn get_letter_from_row(x: &PostgresRow) -> Letter {
11        Letter {
12            id: get!(x->0(i64)) as usize,
13            created: get!(x->1(i64)) as usize,
14            owner: get!(x->2(i64)) as usize,
15            receivers: serde_json::from_str(&get!(x->3(String))).unwrap(),
16            subject: get!(x->4(String)),
17            content: get!(x->5(String)),
18            read_by: serde_json::from_str(&get!(x->6(String))).unwrap(),
19            replying_to: get!(x->7(i64)) as usize,
20            likes: get!(x->8(i32)) as isize,
21            dislikes: get!(x->9(i32)) as isize,
22        }
23    }
24
25    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:{}");
26
27    /// Get all letters by user.
28    ///
29    /// # Arguments
30    /// * `id` - the ID of the user to fetch letters for
31    /// * `batch` - the limit of items in each page
32    /// * `page` - the page number
33    pub async fn get_letters_by_user(
34        &self,
35        id: usize,
36        batch: usize,
37        page: usize,
38    ) -> Result<Vec<Letter>> {
39        let conn = match self.0.connect().await {
40            Ok(c) => c,
41            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
42        };
43
44        let res = query_rows!(
45            &conn,
46            "SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
47            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
48            |x| { Self::get_letter_from_row(x) }
49        );
50
51        if res.is_err() {
52            return Err(Error::GeneralNotFound("letter".to_string()));
53        }
54
55        Ok(res.unwrap())
56    }
57
58    /// Get all letters by user (where user is a receiver).
59    ///
60    /// # Arguments
61    /// * `id` - the ID of the user to fetch letters for
62    /// * `batch` - the limit of items in each page
63    /// * `page` - the page number
64    pub async fn get_received_letters_by_user(
65        &self,
66        id: usize,
67        batch: usize,
68        page: usize,
69    ) -> Result<Vec<Letter>> {
70        let conn = match self.0.connect().await {
71            Ok(c) => c,
72            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
73        };
74
75        let res = query_rows!(
76            &conn,
77            "SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
78            &[
79                &format!("%{id}%"),
80                &(batch as i64),
81                &((page * batch) as i64)
82            ],
83            |x| { Self::get_letter_from_row(x) }
84        );
85
86        if res.is_err() {
87            return Err(Error::GeneralNotFound("letter".to_string()));
88        }
89
90        Ok(res.unwrap())
91    }
92
93    /// Get all letters which are replying to the given letter.
94    ///
95    /// # Arguments
96    /// * `id` - the ID of the letter to fetch letters for
97    /// * `batch` - the limit of items in each page
98    /// * `page` - the page number
99    pub async fn get_letters_by_replying_to(
100        &self,
101        id: usize,
102        batch: usize,
103        page: usize,
104    ) -> Result<Vec<Letter>> {
105        let conn = match self.0.connect().await {
106            Ok(c) => c,
107            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
108        };
109
110        let res = query_rows!(
111            &conn,
112            "SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
113            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
114            |x| { Self::get_letter_from_row(x) }
115        );
116
117        if res.is_err() {
118            return Err(Error::GeneralNotFound("letter".to_string()));
119        }
120
121        Ok(res.unwrap())
122    }
123
124    /// Fill a list of letters with their owner.
125    pub async fn fill_letters(&self, letters: Vec<Letter>) -> Result<Vec<(User, Letter)>> {
126        let mut seen_users: HashMap<usize, User> = HashMap::new();
127        let mut out = Vec::new();
128
129        for letter in letters {
130            out.push(if let Some(ua) = seen_users.get(&letter.owner) {
131                (ua.to_owned(), letter)
132            } else {
133                let user = self.get_user_by_id(letter.owner).await?;
134                seen_users.insert(letter.owner, user.clone());
135                (user, letter)
136            })
137        }
138
139        Ok(out)
140    }
141
142    /// Create a new letter in the database.
143    ///
144    /// # Arguments
145    /// * `data` - a mock [`Letter`] object to insert
146    pub async fn create_letter(&self, data: Letter) -> Result<Letter> {
147        // check values
148        if data.subject.len() < 2 {
149            return Err(Error::DataTooShort("subject".to_string()));
150        } else if data.subject.len() > 256 {
151            return Err(Error::DataTooLong("subject".to_string()));
152        }
153
154        if data.content.len() < 2 {
155            return Err(Error::DataTooShort("content".to_string()));
156        } else if data.content.len() > 16384 {
157            return Err(Error::DataTooLong("content".to_string()));
158        }
159
160        if data.receivers.len() < 1 {
161            return Err(Error::DataTooShort("receivers".to_string()));
162        } else if data.receivers.len() > 10 {
163            return Err(Error::DataTooLong("receivers".to_string()));
164        }
165
166        // get sender
167        let sender = self.get_user_by_id(data.owner).await?;
168
169        // ...
170        let conn = match self.0.connect().await {
171            Ok(c) => c,
172            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
173        };
174
175        let res = execute!(
176            &conn,
177            "INSERT INTO letters VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
178            params![
179                &(data.id as i64),
180                &(data.created as i64),
181                &(data.owner as i64),
182                &serde_json::to_string(&data.receivers).unwrap(),
183                &data.subject,
184                &data.content,
185                &serde_json::to_string(&data.read_by).unwrap(),
186                &(data.replying_to as i64),
187                &(data.likes as i32),
188                &(data.dislikes as i32),
189            ]
190        );
191
192        if let Err(e) = res {
193            return Err(Error::DatabaseError(e.to_string()));
194        }
195
196        // send notifications
197        for x in &data.receivers {
198            self.create_notification(Notification::new(
199                "You've got mail!".to_string(),
200                format!(
201                    "[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}).",
202                    sender.username, sender.id, data.id
203                ),
204                *x,
205            ))
206            .await?;
207        }
208
209        // ...
210        Ok(data)
211    }
212
213    pub async fn delete_letter(&self, id: usize, user: &User) -> Result<()> {
214        let letter = self.get_letter_by_id(id).await?;
215
216        // check user permission
217        if user.id != letter.owner
218            && !user
219                .secondary_permissions
220                .check(SecondaryPermission::MANAGE_LETTERS)
221        {
222            return Err(Error::NotAllowed);
223        }
224
225        // ...
226        let conn = match self.0.connect().await {
227            Ok(c) => c,
228            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
229        };
230
231        let res = execute!(&conn, "DELETE FROM letters WHERE id = $1", &[&(id as i64)]);
232
233        if let Err(e) = res {
234            return Err(Error::DatabaseError(e.to_string()));
235        }
236
237        // ...
238        self.0.1.remove(format!("atto.letter:{}", id)).await;
239        Ok(())
240    }
241
242    auto_method!(update_letter_read_by(Vec<usize>) -> "UPDATE letters SET read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.letter:{}");
243
244    auto_method!(incr_letter_likes() -> "UPDATE letters SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --incr);
245    auto_method!(incr_letter_dislikes() -> "UPDATE letters SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl="atto.letter:{}" --incr);
246    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);
247    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);
248}