tetratto_core/database/
invite_codes.rs1use 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 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 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 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 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; pub async fn create_invite_code(&self, data: InviteCode, user: &User) -> Result<InviteCode> {
106 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 if !user.permissions.check(FinePermission::SUPPORTER) {
117 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 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}