tetratto_core/database/
invite_codes.rs

1use oiseau::{cache::Cache, query_row, query_rows};
2use tetratto_shared::unix_epoch_timestamp;
3use crate::model::{
4    Error, Result,
5    auth::{User, InviteCode},
6    permissions::FinePermission,
7};
8use crate::{auto_method, DataManager};
9use oiseau::{PostgresRow, execute, get, params};
10
11impl DataManager {
12    /// Get a [`InviteCode`] from an SQL row.
13    pub(crate) fn get_invite_code_from_row(x: &PostgresRow) -> InviteCode {
14        InviteCode {
15            id: get!(x->0(i64)) as usize,
16            created: get!(x->1(i64)) as usize,
17            owner: get!(x->2(i64)) as usize,
18            code: get!(x->3(String)),
19            is_used: get!(x->4(i32)) as i8 == 1,
20        }
21    }
22
23    auto_method!(get_invite_code_by_id()@get_invite_code_from_row -> "SELECT * FROM invite_codes WHERE id = $1" --name="invite code" --returns=InviteCode --cache-key-tmpl="atto.invite_code:{}");
24    auto_method!(get_invite_code_by_code(&str)@get_invite_code_from_row -> "SELECT * FROM invite_codes WHERE code = $1" --name="invite code" --returns=InviteCode);
25
26    /// Get invite_codes by `owner`.
27    pub async fn get_invite_codes_by_owner(
28        &self,
29        owner: usize,
30        batch: usize,
31        page: usize,
32    ) -> Result<Vec<InviteCode>> {
33        let conn = match self.0.connect().await {
34            Ok(c) => c,
35            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
36        };
37
38        let res = query_rows!(
39            &conn,
40            "SELECT * FROM invite_codes WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
41            &[&(owner as i64), &(batch as i64), &((page * batch) as i64)],
42            |x| { Self::get_invite_code_from_row(x) }
43        );
44
45        if res.is_err() {
46            return Err(Error::GeneralNotFound("invite_code".to_string()));
47        }
48
49        Ok(res.unwrap())
50    }
51
52    /// Get invite_codes by `owner`.
53    pub async fn get_invite_codes_by_owner_count(&self, owner: usize) -> Result<i32> {
54        let conn = match self.0.connect().await {
55            Ok(c) => c,
56            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
57        };
58
59        let res = query_row!(
60            &conn,
61            "SELECT COUNT(*)::int FROM invite_codes WHERE owner = $1",
62            &[&(owner as i64)],
63            |x| Ok(x.get::<usize, i32>(0))
64        );
65
66        if res.is_err() {
67            return Err(Error::GeneralNotFound("invite_code".to_string()));
68        }
69
70        Ok(res.unwrap())
71    }
72
73    /// Fill a vector of invite codes with the user that used them.
74    pub async fn fill_invite_codes(
75        &self,
76        codes: Vec<InviteCode>,
77    ) -> Result<Vec<(Option<User>, InviteCode)>> {
78        let mut out = Vec::new();
79
80        for code in codes {
81            if code.is_used {
82                out.push((
83                    match self.get_user_by_invite_code(code.id as i64).await {
84                        Ok(u) => Some(u),
85                        Err(_) => None,
86                    },
87                    code,
88                ))
89            } else {
90                out.push((None, code))
91            }
92        }
93
94        Ok(out)
95    }
96
97    const MAXIMUM_FREE_INVITE_CODES: usize = 4;
98    const MAXIMUM_SUPPORTER_INVITE_CODES: usize = 48;
99    const MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES: usize = 2_629_800_000; // 1mo
100
101    /// Create a new invite_code in the database.
102    ///
103    /// # Arguments
104    /// * `data` - a mock [`InviteCode`] object to insert
105    pub async fn create_invite_code(&self, data: InviteCode, user: &User) -> Result<InviteCode> {
106        // check account creation date (if we aren't a supporter OR this is a purchased account)
107        if !user.permissions.check(FinePermission::SUPPORTER) | user.was_purchased {
108            if unix_epoch_timestamp() - user.created < Self::MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES {
109                return Err(Error::MiscError(
110                    "Your account is too young to do this".to_string(),
111                ));
112            }
113        }
114
115        // ...
116        if !user.permissions.check(FinePermission::SUPPORTER) {
117            // our account is old enough, but we need to make sure we don't already have
118            // 2 invite codes
119            if (self.get_invite_codes_by_owner_count(user.id).await? as usize)
120                >= Self::MAXIMUM_FREE_INVITE_CODES
121            {
122                return Err(Error::MiscError(
123                    "You already have the maximum number of invite codes you can create"
124                        .to_string(),
125                ));
126            }
127        } else if !user.permissions.check(FinePermission::MANAGE_USERS) {
128            // check count since we're also not a moderator with MANAGE_USERS
129            if (self.get_invite_codes_by_owner_count(user.id).await? as usize)
130                >= Self::MAXIMUM_SUPPORTER_INVITE_CODES
131            {
132                return Err(Error::MiscError(
133                    "You already have the maximum number of invite codes you can create"
134                        .to_string(),
135                ));
136            }
137        }
138
139        let conn = match self.0.connect().await {
140            Ok(c) => c,
141            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
142        };
143
144        let res = execute!(
145            &conn,
146            "INSERT INTO invite_codes VALUES ($1, $2, $3, $4, $5)",
147            params![
148                &(data.id as i64),
149                &(data.created as i64),
150                &(data.owner as i64),
151                &data.code,
152                &{ if data.is_used { 1 } else { 0 } }
153            ]
154        );
155
156        if let Err(e) = res {
157            return Err(Error::DatabaseError(e.to_string()));
158        }
159
160        Ok(data)
161    }
162
163    pub async fn delete_invite_code(&self, id: usize, user: &User) -> Result<()> {
164        if !user.permissions.check(FinePermission::MANAGE_USERS) {
165            return Err(Error::NotAllowed);
166        }
167
168        let conn = match self.0.connect().await {
169            Ok(c) => c,
170            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
171        };
172
173        let res = execute!(
174            &conn,
175            "DELETE FROM invite_codes WHERE id = $1",
176            &[&(id as i64)]
177        );
178
179        if let Err(e) = res {
180            return Err(Error::DatabaseError(e.to_string()));
181        }
182
183        self.0.1.remove(format!("atto.invite_code:{}", id)).await;
184
185        Ok(())
186    }
187
188    pub async fn update_invite_code_is_used(&self, id: usize, new_is_used: bool) -> Result<()> {
189        let conn = match self.0.connect().await {
190            Ok(c) => c,
191            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
192        };
193
194        let res = execute!(
195            &conn,
196            "UPDATE invite_codes SET is_used = $1 WHERE id = $2",
197            params![&{ if new_is_used { 1 } else { 0 } }, &(id as i64)]
198        );
199
200        if let Err(e) = res {
201            return Err(Error::DatabaseError(e.to_string()));
202        }
203
204        self.0.1.remove(format!("atto.invite_code:{}", id)).await;
205        Ok(())
206    }
207}