2025-06-08 14:15:42 -04:00
use oiseau ::cache ::Cache ;
2025-06-15 16:09:02 -04:00
use crate ::{
database ::posts ::FullPost ,
model ::{
auth ::User ,
permissions ::FinePermission ,
stacks ::{ StackMode , StackPrivacy , StackSort , UserStack } ,
Error , Result ,
} ,
2025-05-08 22:18:04 -04:00
} ;
2025-06-08 14:15:42 -04:00
use crate ::{ auto_method , DataManager } ;
2025-06-24 16:34:55 -04:00
use oiseau ::{ PostgresRow , execute , get , query_rows , params } ;
2025-05-08 22:18:04 -04:00
impl DataManager {
/// Get a [`UserStack`] from an SQL row.
2025-06-15 12:19:58 -04:00
pub ( crate ) fn get_stack_from_row ( x : & PostgresRow ) -> UserStack {
2025-05-08 22:18:04 -04:00
UserStack {
id : get ! ( x ->0 ( i64 ) ) as usize ,
created : get ! ( x ->1 ( i64 ) ) as usize ,
owner : get ! ( x ->2 ( i64 ) ) as usize ,
name : get ! ( x ->3 ( String ) ) ,
users : serde_json ::from_str ( & get! ( x ->4 ( String ) ) ) . unwrap ( ) ,
privacy : serde_json ::from_str ( & get! ( x ->5 ( String ) ) ) . unwrap ( ) ,
2025-05-09 15:56:19 -04:00
mode : serde_json ::from_str ( & get! ( x ->6 ( String ) ) ) . unwrap ( ) ,
sort : serde_json ::from_str ( & get! ( x ->7 ( String ) ) ) . unwrap ( ) ,
2025-05-08 22:18:04 -04:00
}
}
auto_method! ( get_stack_by_id ( usize as i64 ) @ get_stack_from_row -> " SELECT * FROM stacks WHERE id = $1 " - - name = " stack " - - returns = UserStack - - cache - key - tmpl = " atto.stack:{} " ) ;
2025-05-09 15:56:19 -04:00
pub async fn get_stack_posts (
& self ,
as_user_id : usize ,
id : usize ,
batch : usize ,
page : usize ,
ignore_users : & Vec < usize > ,
2025-05-15 23:59:26 -04:00
user : & Option < User > ,
2025-06-15 16:09:02 -04:00
) -> Result < Vec < FullPost > > {
2025-05-09 15:56:19 -04:00
let stack = self . get_stack_by_id ( id ) . await ? ;
Ok ( match stack . mode {
StackMode ::Include = > {
self . fill_posts_with_community (
self . get_posts_from_stack ( id , batch , page , stack . sort )
. await ? ,
as_user_id ,
ignore_users ,
2025-05-15 23:59:26 -04:00
user ,
2025-05-09 15:56:19 -04:00
)
. await ?
}
StackMode ::Exclude = > {
let ignore_users = [ ignore_users . to_owned ( ) , stack . users ] . concat ( ) ;
match stack . sort {
StackSort ::Created = > {
self . fill_posts_with_community (
2025-07-04 17:41:58 -04:00
self . get_latest_posts ( batch , page , & user , 0 ) . await ? ,
2025-05-09 15:56:19 -04:00
as_user_id ,
& ignore_users ,
2025-05-15 23:59:26 -04:00
user ,
2025-05-09 15:56:19 -04:00
)
. await ?
}
StackSort ::Likes = > {
self . fill_posts_with_community (
self . get_popular_posts ( batch , page , 604_800_000 ) . await ? ,
as_user_id ,
& ignore_users ,
2025-05-15 23:59:26 -04:00
user ,
2025-05-09 15:56:19 -04:00
)
. await ?
}
}
}
2025-06-15 11:52:44 -04:00
StackMode ::BlockList = > {
return Err ( Error ::MiscError (
" You should use `get_stack_users` for this type " . to_string ( ) ,
) ) ;
}
2025-06-15 16:09:02 -04:00
StackMode ::Circle = > {
if ! stack . users . contains ( & as_user_id ) & & as_user_id ! = stack . owner {
return Err ( Error ::NotAllowed ) ;
}
self . fill_posts_with_community (
self . get_posts_by_stack ( stack . id , batch , page ) . await ? ,
as_user_id ,
& ignore_users ,
user ,
)
. await ?
}
2025-05-09 15:56:19 -04:00
} )
}
2025-06-15 11:52:44 -04:00
pub async fn get_stack_users ( & self , id : usize , batch : usize , page : usize ) -> Result < Vec < User > > {
let stack = self . get_stack_by_id ( id ) . await ? ;
if stack . mode ! = StackMode ::BlockList {
return Err ( Error ::MiscError (
" You should use `get_stack_posts` for this type " . to_string ( ) ,
) ) ;
}
// build list
let mut out = Vec ::new ( ) ;
let mut i = 0 ;
for user in stack . users . iter ( ) . skip ( batch * page ) {
if i = = batch {
break ;
}
out . push ( self . get_user_by_id ( user . to_owned ( ) ) . await ? ) ;
i + = 1 ;
}
Ok ( out )
}
2025-05-08 22:18:04 -04:00
/// Get all stacks by user.
///
2025-06-15 16:09:02 -04:00
/// Also pulls stacks that are of "Circle" type AND the user is added to the `users` list.
///
2025-05-08 22:18:04 -04:00
/// # Arguments
/// * `id` - the ID of the user to fetch stacks for
2025-06-15 16:09:02 -04:00
pub async fn get_stacks_by_user ( & self , id : usize ) -> Result < Vec < UserStack > > {
2025-06-08 14:15:42 -04:00
let conn = match self . 0. connect ( ) . await {
2025-05-08 22:18:04 -04:00
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_rows! (
& conn ,
2025-06-15 16:09:02 -04:00
" SELECT * FROM stacks WHERE owner = $1 OR (mode = ' \" Circle \" ' AND users LIKE $2) ORDER BY name ASC " ,
& [ & ( id as i64 ) , & format! ( " % {id} % " ) ] ,
2025-05-08 22:18:04 -04:00
| x | { Self ::get_stack_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " stack " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-05-17 20:02:55 -04:00
const MAXIMUM_FREE_STACKS : usize = 5 ;
2025-06-15 16:09:02 -04:00
pub const MAXIMUM_FREE_STACK_USERS : usize = 50 ;
2025-05-17 20:02:55 -04:00
2025-05-08 22:18:04 -04:00
/// Create a new stack in the database.
///
/// # Arguments
/// * `data` - a mock [`UserStack`] object to insert
pub async fn create_stack ( & self , data : UserStack ) -> Result < UserStack > {
2025-05-08 22:35:05 -04:00
// check values
2025-08-04 23:29:24 -04:00
if data . name . trim ( ) . len ( ) < 2 {
2025-05-08 22:35:05 -04:00
return Err ( Error ::DataTooShort ( " title " . to_string ( ) ) ) ;
} else if data . name . len ( ) > 32 {
return Err ( Error ::DataTooLong ( " title " . to_string ( ) ) ) ;
}
2025-05-08 22:18:04 -04:00
// check number of stacks
let owner = self . get_user_by_id ( data . owner ) . await ? ;
if ! owner . permissions . check ( FinePermission ::SUPPORTER ) {
2025-07-14 22:05:59 -04:00
let stacks = self
. get_table_row_count_where ( " stacks " , & format! ( " owner = {} " , owner . id ) )
. await ? as usize ;
2025-05-08 22:18:04 -04:00
2025-07-14 22:05:59 -04:00
if stacks > = Self ::MAXIMUM_FREE_STACKS {
2025-05-08 22:18:04 -04:00
return Err ( Error ::MiscError (
" You already have the maximum number of stacks you can have " . to_string ( ) ,
) ) ;
}
}
// ...
2025-06-08 14:15:42 -04:00
let conn = match self . 0. connect ( ) . await {
2025-05-08 22:18:04 -04:00
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
2025-05-09 15:56:19 -04:00
" INSERT INTO stacks VALUES ($1, $2, $3, $4, $5, $6, $7, $8) " ,
2025-05-08 22:18:04 -04:00
params! [
& ( data . id as i64 ) ,
& ( data . created as i64 ) ,
& ( data . owner as i64 ) ,
& data . name ,
& serde_json ::to_string ( & data . users ) . unwrap ( ) ,
& serde_json ::to_string ( & data . privacy ) . unwrap ( ) ,
2025-05-09 15:56:19 -04:00
& serde_json ::to_string ( & data . mode ) . unwrap ( ) ,
& serde_json ::to_string ( & data . sort ) . unwrap ( ) ,
2025-05-08 22:18:04 -04:00
]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
Ok ( data )
}
pub async fn delete_stack ( & self , id : usize , user : & User ) -> Result < ( ) > {
let stack = self . get_stack_by_id ( id ) . await ? ;
// check user permission
2025-05-09 22:36:16 -04:00
if user . id ! = stack . owner & & ! user . permissions . check ( FinePermission ::MANAGE_STACKS ) {
return Err ( Error ::NotAllowed ) ;
2025-05-08 22:18:04 -04:00
}
// ...
2025-06-08 14:15:42 -04:00
let conn = match self . 0. connect ( ) . await {
2025-05-08 22:18:04 -04:00
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! ( & conn , " DELETE FROM stacks WHERE id = $1 " , & [ & ( id as i64 ) ] ) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
2025-06-15 16:09:02 -04:00
// delete stackblocks
let res = execute! (
& conn ,
" DELETE FROM stackblocks WHERE stack = $1 " ,
& [ & ( id as i64 ) ]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
// delete posts
let res = execute! ( & conn , " DELETE FROM posts WHERE stack = $1 " , & [ & ( id as i64 ) ] ) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
// ...
2025-06-08 14:15:42 -04:00
self . 0. 1. remove ( format! ( " atto.stack: {} " , id ) ) . await ;
2025-05-08 22:18:04 -04:00
Ok ( ( ) )
}
2025-07-02 23:10:58 -04:00
auto_method! ( update_stack_name ( & str ) @ get_stack_by_id :FinePermission ::MANAGE_STACKS ; -> " UPDATE stacks SET name = $1 WHERE id = $2 " - - cache - key - tmpl = " atto.stack:{} " ) ;
auto_method! ( update_stack_users ( Vec < usize > ) @ get_stack_by_id :FinePermission ::MANAGE_STACKS ; -> " UPDATE stacks SET users = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.stack:{} " ) ;
2025-05-09 15:56:19 -04:00
2025-07-02 23:10:58 -04:00
auto_method! ( update_stack_privacy ( StackPrivacy ) @ get_stack_by_id :FinePermission ::MANAGE_STACKS ; -> " UPDATE stacks SET privacy = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.stack:{} " ) ;
auto_method! ( update_stack_mode ( StackMode ) @ get_stack_by_id :FinePermission ::MANAGE_STACKS ; -> " UPDATE stacks SET mode = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.stack:{} " ) ;
auto_method! ( update_stack_sort ( StackSort ) @ get_stack_by_id :FinePermission ::MANAGE_STACKS ; -> " UPDATE stacks SET sort = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.stack:{} " ) ;
2025-05-08 22:18:04 -04:00
}