2025-08-02 16:04:50 -04:00
use std ::collections ::HashMap ;
2025-08-08 02:17:06 -04:00
use crate ::model ::auth ::Notification ;
2025-07-26 22:18:32 -04:00
use crate ::model ::{ auth ::User , mail ::Letter , permissions ::SecondaryPermission , Error , Result } ;
use crate ::{ auto_method , DataManager } ;
use oiseau ::{ cache ::Cache , execute , get , params , query_rows , PostgresRow } ;
impl DataManager {
/// Get a [`Letter`] from an SQL row.
pub ( crate ) fn get_letter_from_row ( x : & PostgresRow ) -> Letter {
Letter {
id : get ! ( x ->0 ( i64 ) ) as usize ,
created : get ! ( x ->1 ( i64 ) ) as usize ,
owner : get ! ( x ->2 ( i64 ) ) as usize ,
receivers : serde_json ::from_str ( & get! ( x ->3 ( String ) ) ) . unwrap ( ) ,
subject : get ! ( x ->4 ( String ) ) ,
content : get ! ( x ->5 ( String ) ) ,
read_by : serde_json ::from_str ( & get! ( x ->6 ( String ) ) ) . unwrap ( ) ,
2025-08-02 16:04:50 -04:00
replying_to : get ! ( x ->7 ( i64 ) ) as usize ,
2025-07-26 22:18:32 -04:00
}
}
auto_method! ( get_letter_by_id ( usize as i64 ) @ get_letter_from_row -> " SELECT * FROM letters WHERE id = $1 " - - name = " letter " - - returns = Letter - - cache - key - tmpl = " atto.letter:{} " ) ;
/// Get all letters by user.
///
/// # Arguments
/// * `id` - the ID of the user to fetch letters for
2025-08-02 16:04:50 -04:00
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_letters_by_user (
& self ,
id : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < Letter > > {
2025-07-26 22:18:32 -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-08-02 16:04:50 -04:00
" SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [ & ( id as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
2025-07-26 22:18:32 -04:00
| x | { Self ::get_letter_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " letter " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
/// Get all letters by user (where user is a receiver).
///
/// # Arguments
/// * `id` - the ID of the user to fetch letters for
2025-08-02 16:04:50 -04:00
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_received_letters_by_user (
& self ,
id : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < Letter > > {
2025-07-26 22:18:32 -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-08-02 16:04:50 -04:00
" SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [
& format! ( " % {id} % " ) ,
& ( batch as i64 ) ,
& ( ( page * batch ) as i64 )
] ,
2025-07-26 22:18:32 -04:00
| x | { Self ::get_letter_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " letter " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-07-26 22:28:45 -04:00
/// Get all letters which are replying to the given letter.
///
/// # Arguments
/// * `id` - the ID of the letter to fetch letters for
2025-08-02 16:04:50 -04:00
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_letters_by_replying_to (
& self ,
id : usize ,
batch : usize ,
page : usize ,
) -> Result < Vec < Letter > > {
2025-07-26 22:28:45 -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-08-02 16:04:50 -04:00
" SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3 " ,
& [ & ( id as i64 ) , & ( batch as i64 ) , & ( ( page * batch ) as i64 ) ] ,
2025-07-26 22:28:45 -04:00
| x | { Self ::get_letter_from_row ( x ) }
) ;
if res . is_err ( ) {
return Err ( Error ::GeneralNotFound ( " letter " . to_string ( ) ) ) ;
}
Ok ( res . unwrap ( ) )
}
2025-08-02 16:04:50 -04:00
/// Fill a list of letters with their owner.
pub async fn fill_letters ( & self , letters : Vec < Letter > ) -> Result < Vec < ( User , Letter ) > > {
let mut seen_users : HashMap < usize , User > = HashMap ::new ( ) ;
let mut out = Vec ::new ( ) ;
for letter in letters {
out . push ( if let Some ( ua ) = seen_users . get ( & letter . owner ) {
( ua . to_owned ( ) , letter )
} else {
let user = self . get_user_by_id ( letter . owner ) . await ? ;
seen_users . insert ( letter . owner , user . clone ( ) ) ;
( user , letter )
} )
}
Ok ( out )
}
2025-07-26 22:18:32 -04:00
/// Create a new letter in the database.
///
/// # Arguments
/// * `data` - a mock [`Letter`] object to insert
pub async fn create_letter ( & self , data : Letter ) -> Result < Letter > {
// check values
if data . subject . len ( ) < 2 {
return Err ( Error ::DataTooShort ( " subject " . to_string ( ) ) ) ;
} else if data . subject . len ( ) > 256 {
return Err ( Error ::DataTooLong ( " subject " . to_string ( ) ) ) ;
}
if data . content . len ( ) < 2 {
return Err ( Error ::DataTooShort ( " content " . to_string ( ) ) ) ;
} else if data . content . len ( ) > 16384 {
return Err ( Error ::DataTooLong ( " content " . to_string ( ) ) ) ;
}
2025-08-02 16:04:50 -04:00
if data . receivers . len ( ) < 1 {
return Err ( Error ::DataTooShort ( " receivers " . to_string ( ) ) ) ;
} else if data . receivers . len ( ) > 10 {
return Err ( Error ::DataTooLong ( " receivers " . to_string ( ) ) ) ;
}
2025-08-08 02:17:06 -04:00
// get sender
let sender = self . get_user_by_id ( data . owner ) . await ? ;
2025-07-26 22:18:32 -04:00
// ...
let conn = match self . 0. connect ( ) . await {
Ok ( c ) = > c ,
Err ( e ) = > return Err ( Error ::DatabaseConnection ( e . to_string ( ) ) ) ,
} ;
let res = execute! (
& conn ,
2025-07-26 22:28:45 -04:00
" INSERT INTO letters VALUES ($1, $2, $3, $4, $5, $6, $7, $8) " ,
2025-07-26 22:18:32 -04:00
params! [
& ( data . id as i64 ) ,
& ( data . created as i64 ) ,
& ( data . owner as i64 ) ,
& serde_json ::to_string ( & data . receivers ) . unwrap ( ) ,
& data . subject ,
& data . content ,
& serde_json ::to_string ( & data . read_by ) . unwrap ( ) ,
2025-07-26 22:28:45 -04:00
& ( data . replying_to as i64 )
2025-07-26 22:18:32 -04:00
]
) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
2025-08-08 02:17:06 -04:00
// send notifications
for x in & data . receivers {
self . create_notification ( Notification ::new (
" You've got mail! " . to_string ( ) ,
format! (
" [@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}). " ,
sender . username , sender . id , data . id
) ,
* x ,
) )
. await ? ;
}
// ...
2025-07-26 22:18:32 -04:00
Ok ( data )
}
pub async fn delete_letter ( & self , id : usize , user : & User ) -> Result < ( ) > {
let letter = self . get_letter_by_id ( id ) . await ? ;
// check user permission
if user . id ! = letter . owner
& & ! user
. secondary_permissions
. check ( SecondaryPermission ::MANAGE_LETTERS )
{
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 letters WHERE id = $1 " , & [ & ( id as i64 ) ] ) ;
if let Err ( e ) = res {
return Err ( Error ::DatabaseError ( e . to_string ( ) ) ) ;
}
// ...
self . 0. 1. remove ( format! ( " atto.letter: {} " , id ) ) . await ;
Ok ( ( ) )
}
auto_method! ( update_letter_read_by ( Vec < usize > ) -> " UPDATE letters SET read_by = $1 WHERE id = $2 " - - serde - - cache - key - tmpl = " atto.letter:{} " ) ;
}