Skip to main content

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                    (self.get_user_by_invite_code(code.id as i64).await).ok(),
84                    code,
85                ))
86            } else {
87                out.push((None, code))
88            }
89        }
90
91        Ok(out)
92    }
93
94    const MAXIMUM_FREE_INVITE_CODES: usize = 4;
95    const MAXIMUM_SUPPORTER_INVITE_CODES: usize = 48;
96    const MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES: usize = 2_629_800_000; // 1mo
97
98    /// Create a new invite_code in the database.
99    ///
100    /// # Arguments
101    /// * `data` - a mock [`InviteCode`] object to insert
102    pub async fn create_invite_code(&self, data: InviteCode, user: &User) -> Result<InviteCode> {
103        // check account creation date (if we aren't a supporter OR this is a purchased account)
104        if !user.permissions.check(FinePermission::SUPPORTER) | user.was_purchased
105            && unix_epoch_timestamp() - user.created < Self::MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES {
106                return Err(Error::MiscError(
107                    "Your account is too young to do this".to_string(),
108                ));
109            }
110
111        // ...
112        if !user.permissions.check(FinePermission::SUPPORTER) {
113            // our account is old enough, but we need to make sure we don't already have
114            // 2 invite codes
115            if (self.get_invite_codes_by_owner_count(user.id).await? as usize)
116                >= Self::MAXIMUM_FREE_INVITE_CODES
117            {
118                return Err(Error::MiscError(
119                    "You already have the maximum number of invite codes you can create"
120                        .to_string(),
121                ));
122            }
123        } else if !user.permissions.check(FinePermission::MANAGE_USERS) {
124            // check count since we're also not a moderator with MANAGE_USERS
125            if (self.get_invite_codes_by_owner_count(user.id).await? as usize)
126                >= Self::MAXIMUM_SUPPORTER_INVITE_CODES
127            {
128                return Err(Error::MiscError(
129                    "You already have the maximum number of invite codes you can create"
130                        .to_string(),
131                ));
132            }
133        }
134
135        let conn = match self.0.connect().await {
136            Ok(c) => c,
137            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
138        };
139
140        let res = execute!(
141            &conn,
142            "INSERT INTO invite_codes VALUES ($1, $2, $3, $4, $5)",
143            params![
144                &(data.id as i64),
145                &(data.created as i64),
146                &(data.owner as i64),
147                &data.code,
148                &{ if data.is_used { 1 } else { 0 } }
149            ]
150        );
151
152        if let Err(e) = res {
153            return Err(Error::DatabaseError(e.to_string()));
154        }
155
156        Ok(data)
157    }
158
159    pub async fn delete_invite_code(&self, id: usize, user: &User) -> Result<()> {
160        if !user.permissions.check(FinePermission::MANAGE_USERS) {
161            return Err(Error::NotAllowed);
162        }
163
164        let conn = match self.0.connect().await {
165            Ok(c) => c,
166            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
167        };
168
169        let res = execute!(
170            &conn,
171            "DELETE FROM invite_codes WHERE id = $1",
172            &[&(id as i64)]
173        );
174
175        if let Err(e) = res {
176            return Err(Error::DatabaseError(e.to_string()));
177        }
178
179        self.0.1.remove(format!("atto.invite_code:{}", id)).await;
180
181        Ok(())
182    }
183
184    pub async fn update_invite_code_is_used(&self, id: usize, new_is_used: bool) -> Result<()> {
185        let conn = match self.0.connect().await {
186            Ok(c) => c,
187            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
188        };
189
190        let res = execute!(
191            &conn,
192            "UPDATE invite_codes SET is_used = $1 WHERE id = $2",
193            params![&{ if new_is_used { 1 } else { 0 } }, &(id as i64)]
194        );
195
196        if let Err(e) = res {
197            return Err(Error::DatabaseError(e.to_string()));
198        }
199
200        self.0.1.remove(format!("atto.invite_code:{}", id)).await;
201        Ok(())
202    }
203}