2025-03-24 19:55:08 -04:00
use super ::* ;
use crate ::cache ::Cache ;
2025-03-24 20:19:12 -04:00
use crate ::model ::{
2025-03-27 18:10:47 -04:00
Error , Result ,
auth ::User ,
2025-03-29 00:26:56 -04:00
communities ::{ Community , CommunityWriteAccess , Post , PostContext } ,
2025-03-24 20:19:12 -04:00
permissions ::FinePermission ,
} ;
2025-03-26 21:46:21 -04:00
use crate ::{ auto_method , execute , get , query_row , query_rows } ;
2025-03-24 19:55:08 -04:00
#[ cfg(feature = " sqlite " ) ]
use rusqlite ::Row ;
#[ cfg(feature = " postgres " ) ]
use tokio_postgres ::Row ;
impl DataManager {
2025-03-27 18:10:47 -04:00
/// Get a [`Post`] from an SQL row.
2025-03-25 21:19:55 -04:00
pub ( crate ) fn get_post_from_row (
2025-03-24 19:55:08 -04:00
#[ cfg(feature = " sqlite " ) ] x : & Row < '_ > ,
#[ cfg(feature = " postgres " ) ] x : & Row ,
2025-03-27 18:10:47 -04:00
) -> Post {
Post {
2025-03-29 00:26:56 -04:00
id : get ! ( x ->0 ( isize ) ) as usize ,
created : get ! ( x ->1 ( isize ) ) as usize ,
2025-03-24 19:55:08 -04:00
content : get ! ( x ->2 ( String ) ) ,
2025-03-29 00:26:56 -04:00
owner : get ! ( x ->3 ( isize ) ) as usize ,
community : get ! ( x ->4 ( isize ) ) as usize ,
2025-03-24 20:23:52 -04:00
context : serde_json ::from_str ( & get! ( x ->5 ( String ) ) ) . unwrap ( ) ,
2025-03-25 22:52:47 -04:00
replying_to : if let Some ( id ) = get! ( x ->6 ( Option < i64 > ) ) {
Some ( id as usize )
} else {
None
} ,
2025-03-24 22:42:33 -04:00
// likes
2025-03-29 00:26:56 -04:00
likes : get ! ( x ->7 ( isize ) ) as isize ,
dislikes : get ! ( x ->8 ( isize ) ) as isize ,
2025-03-25 22:52:47 -04:00
// other counts
2025-03-29 00:26:56 -04:00
comment_count : get ! ( x ->9 ( isize ) ) as usize ,
2025-03-24 19:55:08 -04:00
}
}
2025-03-27 18:10:47 -04:00
auto_method! ( get_post_by_id ( ) @ get_post_from_row -> " SELECT * FROM posts WHERE id = $1 " - - name = " post " - - returns = Post - - cache - key - tmpl = " atto.post:{} " ) ;
2025-03-25 22:52:47 -04:00
/// Get all posts which are comments on the given post by ID.
///
/// # Arguments
/// * `id` - the ID of the post the requested posts are commenting on
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
2025-03-29 00:26:56 -04:00
pub async fn get_post_comments (
& self ,
id : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < Post > > {
2025-03-25 22:52:47 -04:00
let conn = match self . connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
2025-03-29 00:26:56 -04:00
let res = query_rows! (
2025-03-25 22:52:47 -04:00
& conn ,
2025-03-26 21:46:21 -04:00
" SELECT * FROM posts WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
2025-03-25 22:52:47 -04:00
& [ & ( id as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
2025-03-29 00:26:56 -04:00
| x | { Self ::get_post_from_row ( x ) }
2025-03-25 22:52:47 -04:00
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " post " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-03-24 19:55:08 -04:00
2025-03-29 00:26:56 -04:00
/// Complete a vector of just posts with their owner as well.
pub async fn fill_posts ( & self , posts : Vec < Post > ) -> Result < Vec < ( Post , User ) > > {
let mut out : Vec < ( Post , User ) > = Vec ::new ( ) ;
for post in posts {
let owner = post . owner . clone ( ) ;
out . push ( ( post , self . get_user_by_id ( owner ) . await ? ) ) ;
}
Ok ( out )
}
/// Complete a vector of just posts with their owner and community as well.
pub async fn fill_posts_with_community (
& self ,
posts : Vec < Post > ,
) -> Result < Vec < ( Post , User , Community ) > > {
let mut out : Vec < ( Post , User , Community ) > = Vec ::new ( ) ;
for post in posts {
let owner = post . owner . clone ( ) ;
let community = post . community . clone ( ) ;
out . push ( (
post ,
self . get_user_by_id ( owner ) . await ? ,
self . get_community_by_id ( community ) . await ? ,
) ) ;
}
Ok ( out )
}
2025-03-26 21:46:21 -04:00
/// Get all posts from the given user (from most recent).
///
/// # Arguments
/// * `id` - the ID of the user the requested posts belong to
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_posts_by_user (
& self ,
id : usize ,
batch : usize ,
page : usize ,
2025-03-27 18:10:47 -04:00
) -> Result < Vec < Post > > {
2025-03-26 21:46:21 -04:00
let conn = match self . connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_rows! (
& conn ,
" SELECT * FROM posts WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [ & ( id as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
| x | { Self ::get_post_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " post " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-03-29 00:26:56 -04:00
/// Get all posts from the given community (from most recent).
///
/// # Arguments
/// * `id` - the ID of the community the requested posts belong to
/// * `batch` - the limit of posts in each page
/// * `page` - the page number
pub async fn get_posts_by_community (
& self ,
id : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < Post > > {
let conn = match self . connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = query_rows! (
& conn ,
" SELECT * FROM posts WHERE community = $1 AND replying_to IS NULL ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [ & ( id as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
| x | { Self ::get_post_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " post " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-03-24 19:55:08 -04:00
/// Create a new journal entry in the database.
///
/// # Arguments
/// * `data` - a mock [`JournalEntry`] object to insert
2025-03-29 00:26:56 -04:00
pub async fn create_post ( & self , data : Post ) -> Result < usize > {
2025-03-24 19:55:08 -04:00
// check values
if data . content . len ( ) < 2 {
return Err ( Error ::DataTooShort ( " content " . to_string ( ) ) ) ;
} else if data . content . len ( ) > 4096 {
return Err ( Error ::DataTooLong ( " username " . to_string ( ) ) ) ;
}
2025-03-29 00:26:56 -04:00
// check permission in community
let community = match self . get_community_by_id ( data . community ) . await {
2025-03-24 20:19:12 -04:00
Ok ( p ) = > p ,
Err ( e ) = > return Err ( e ) ,
} ;
2025-03-29 00:26:56 -04:00
match community . write_access {
2025-03-27 18:10:47 -04:00
CommunityWriteAccess ::Owner = > {
2025-03-29 00:26:56 -04:00
if data . owner ! = community . owner {
2025-03-24 20:19:12 -04:00
return Err ( Error ::NotAllowed ) ;
}
}
2025-03-27 18:10:47 -04:00
CommunityWriteAccess ::Joined = > {
2025-03-24 20:19:12 -04:00
if let Err ( _ ) = self
2025-03-29 00:26:56 -04:00
. get_membership_by_owner_community ( data . owner , community . id )
2025-03-24 20:19:12 -04:00
. await
{
return Err ( Error ::NotAllowed ) ;
}
}
_ = > ( ) ,
} ;
2025-03-29 00:26:56 -04:00
// check if we're blocked
if let Some ( replying_to ) = data . replying_to {
if let Ok ( _ ) = self
. get_userblock_by_initiator_receiver ( replying_to , data . owner )
. await
{
return Err ( Error ::NotAllowed ) ;
}
}
2025-03-24 19:55:08 -04:00
// ...
let conn = match self . connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
2025-03-29 00:26:56 -04:00
" INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) " ,
2025-03-24 19:55:08 -04:00
& [
2025-03-25 22:52:47 -04:00
& Some ( data . id . to_string ( ) ) ,
& Some ( data . created . to_string ( ) ) ,
& Some ( data . content ) ,
& Some ( data . owner . to_string ( ) ) ,
2025-03-27 18:10:47 -04:00
& Some ( data . community . to_string ( ) ) ,
2025-03-25 22:52:47 -04:00
& Some ( serde_json ::to_string ( & data . context ) . unwrap ( ) ) ,
& if let Some ( id ) = data . replying_to {
Some ( id . to_string ( ) )
} else {
None
} ,
& Some ( 0. to_string ( ) ) ,
& Some ( 0. to_string ( ) ) ,
& Some ( 0. to_string ( ) )
2025-03-24 19:55:08 -04:00
]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
2025-03-25 22:52:47 -04:00
// incr comment count
if let Some ( id ) = data . replying_to {
self . incr_post_comments ( id ) . await . unwrap ( ) ;
}
2025-03-29 00:26:56 -04:00
// return
Ok ( data . id )
}
pub async fn delete_post ( & self , id : usize , user : User ) -> Result < ( ) > {
let y = self . get_post_by_id ( id ) . await ? ;
if user . id ! = y . owner {
if ! user . permissions . check ( FinePermission ::MANAGE_POSTS ) {
return Err ( Error ::NotAllowed ) ;
}
}
let conn = match self . connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! ( & conn , " DELETE FROM posts WHERE id = $1 " , & [ & id . to_string ( ) ] ) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
self . 2. remove ( format! ( " atto.post: {} " , id ) ) . await ;
// decr parent comment count
if let Some ( replying_to ) = y . replying_to {
self . decr_post_comments ( replying_to ) . await . unwrap ( ) ;
}
2025-03-25 22:52:47 -04:00
// return
2025-03-24 19:55:08 -04:00
Ok ( ( ) )
}
2025-03-29 00:26:56 -04:00
auto_method! ( update_post_content ( String ) @ get_post_by_id :MANAGE_POSTS -> " UPDATE posts SET content = $1 WHERE id = $2 " - - cache - key - tmpl = " atto.post:{} " ) ;
auto_method! ( update_post_context ( PostContext ) @ get_post_by_id :MANAGE_POSTS -> " UPDATE posts SET context = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.post:{} " ) ;
2025-03-25 22:52:47 -04:00
auto_method! ( incr_post_likes ( ) -> " UPDATE posts SET likes = likes + 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - incr ) ;
auto_method! ( incr_post_dislikes ( ) -> " UPDATE posts SET likes = dislikes + 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - incr ) ;
auto_method! ( decr_post_likes ( ) -> " UPDATE posts SET likes = likes - 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - decr ) ;
auto_method! ( decr_post_dislikes ( ) -> " UPDATE posts SET likes = dislikes - 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - decr ) ;
2025-03-24 22:42:33 -04:00
2025-03-25 22:52:47 -04:00
auto_method! ( incr_post_comments ( ) -> " UPDATE posts SET comment_count = comment_count + 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - incr ) ;
auto_method! ( decr_post_comments ( ) -> " UPDATE posts SET comment_count = comment_count - 1 WHERE id = $1 " - - cache - key - tmpl = " atto.post:{} " - - decr ) ;
2025-03-24 19:55:08 -04:00
}