2025-06-23 13:48:16 -04:00
use oiseau ::{ cache ::Cache , query_row , query_rows } ;
2025-06-22 13:50:12 -04:00
use tetratto_shared ::unix_epoch_timestamp ;
2025-06-22 13:03:02 -04:00
use crate ::model ::{
Error , Result ,
auth ::{ User , InviteCode } ,
permissions ::FinePermission ,
} ;
use crate ::{ auto_method , DataManager } ;
use oiseau ::{ PostgresRow , execute , get , params } ;
impl DataManager {
/// Get a [`InviteCode`] from an SQL row.
pub ( crate ) fn get_invite_code_from_row ( x : & PostgresRow ) -> InviteCode {
InviteCode {
id : get ! ( x ->0 ( i64 ) ) as usize ,
created : get ! ( x ->1 ( i64 ) ) as usize ,
owner : get ! ( x ->2 ( i64 ) ) as usize ,
code : get ! ( x ->3 ( String ) ) ,
is_used : get ! ( x ->4 ( i32 ) ) as i8 = = 1 ,
}
}
2025-07-03 21:56:21 -04:00
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:{} " ) ;
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 ) ;
2025-06-22 13:03:02 -04:00
/// Get invite_codes by `owner`.
2025-06-23 13:48:16 -04:00
pub async fn get_invite_codes_by_owner (
& self ,
owner : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < InviteCode > > {
2025-06-22 13:03:02 -04:00
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_rows! (
& conn ,
2025-06-23 13:48:16 -04:00
" SELECT * FROM invite_codes WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [ & ( owner as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
2025-06-22 13:03:02 -04:00
| x | { Self ::get_invite_code_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " invite_code " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-06-23 13:48:16 -04:00
/// Get invite_codes by `owner`.
pub async fn get_invite_codes_by_owner_count ( & self , owner : usize ) -> Result < i32 > {
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_row! (
& conn ,
" SELECT COUNT(*)::int FROM invite_codes WHERE owner = $1 " ,
& [ & ( owner as i64 ) ] ,
| x | Ok ( x . get ::< usize , i32 > ( 0 ) )
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " invite_code " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-06-22 13:03:02 -04:00
/// Fill a vector of invite codes with the user that used them.
pub async fn fill_invite_codes (
& self ,
codes : Vec < InviteCode > ,
) -> Result < Vec < ( Option < User > , InviteCode ) > > {
let mut out = Vec ::new ( ) ;
for code in codes {
if code . is_used {
out . push ( (
match self . get_user_by_invite_code ( code . id as i64 ) . await {
Ok ( u ) = > Some ( u ) ,
Err ( _ ) = > None ,
} ,
code ,
) )
} else {
out . push ( ( None , code ) )
}
}
Ok ( out )
}
2025-06-22 15:06:21 -04:00
const MAXIMUM_FREE_INVITE_CODES : usize = 4 ;
const MAXIMUM_SUPPORTER_INVITE_CODES : usize = 48 ;
2025-07-03 21:56:21 -04:00
const MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES : usize = 2_629_800_000 ; // 1mo
2025-06-22 13:03:02 -04:00
/// Create a new invite_code in the database.
///
/// # Arguments
/// * `data` - a mock [`InviteCode`] object to insert
pub async fn create_invite_code ( & self , data : InviteCode , user : & User ) -> Result < InviteCode > {
2025-07-05 11:58:51 -04:00
// check account creation date (if we aren't a supporter OR this is a purchased account)
if ! user . permissions . check ( FinePermission ::SUPPORTER ) | user . was_purchased {
if unix_epoch_timestamp ( ) - user . created < Self ::MINIMUM_ACCOUNT_AGE_FOR_INVITE_CODES {
return Err ( Error ::MiscError (
" Your account is too young to do this " . to_string ( ) ,
) ) ;
}
2025-07-03 21:56:21 -04:00
}
2025-06-22 13:50:12 -04:00
2025-07-03 21:56:21 -04:00
// ...
if ! user . permissions . check ( FinePermission ::SUPPORTER ) {
2025-06-22 13:50:12 -04:00
// our account is old enough, but we need to make sure we don't already have
// 2 invite codes
2025-06-23 13:48:16 -04:00
if ( self . get_invite_codes_by_owner_count ( user . id ) . await ? as usize )
2025-06-22 13:50:12 -04:00
> = Self ::MAXIMUM_FREE_INVITE_CODES
{
return Err ( Error ::MiscError (
" You already have the maximum number of invite codes you can create "
. to_string ( ) ,
) ) ;
}
2025-06-22 15:15:39 -04:00
} else if ! user . permissions . check ( FinePermission ::MANAGE_USERS ) {
// check count since we're also not a moderator with MANAGE_USERS
2025-06-23 13:48:16 -04:00
if ( self . get_invite_codes_by_owner_count ( user . id ) . await ? as usize )
2025-06-22 13:03:02 -04:00
> = Self ::MAXIMUM_SUPPORTER_INVITE_CODES
{
return Err ( Error ::MiscError (
" You already have the maximum number of invite codes you can create "
. to_string ( ) ,
) ) ;
}
}
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
" INSERT INTO invite_codes VALUES ($1, $2, $3, $4, $5) " ,
params! [
& ( data . id as i64 ) ,
& ( data . created as i64 ) ,
& ( data . owner as i64 ) ,
& data . code ,
& { if data . is_used { 1 } else { 0 } }
]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
Ok ( data )
}
pub async fn delete_invite_code ( & self , id : usize , user : & User ) -> Result < ( ) > {
2025-06-22 15:15:39 -04:00
if ! user . permissions . check ( FinePermission ::MANAGE_USERS ) {
2025-06-22 13:03:02 -04:00
return Err ( Error ::NotAllowed ) ;
}
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
" DELETE FROM invite_codes WHERE id = $1 " ,
& [ & ( id as i64 ) ]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
self . 0. 1. remove ( format! ( " atto.invite_code: {} " , id ) ) . await ;
Ok ( ( ) )
}
pub async fn update_invite_code_is_used ( & self , id : usize , new_is_used : bool ) -> Result < ( ) > {
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
" UPDATE invite_codes SET is_used = $1 WHERE id = $2 " ,
params! [ & { if new_is_used { 1 } else { 0 } } , & ( id as i64 ) ]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
self . 0. 1. remove ( format! ( " atto.invite_code: {} " , id ) ) . await ;
Ok ( ( ) )
}
}