# define _GNU_SOURCE
# include "status.h"
# include "status/react.h"
# include "model/server.h"
# include "model/account.h"
# include "model/ap/activity.h"
# include "model/notification.h"
# include "model/timeline.h"
// Submodules
# include "json/json.h"
# include "json/layout.h"
# include "ffdb/hash_index.h"
# include "sha256/sha256.h"
# include "collections/array.h"
# include "http/client/client.h"
# include "format.h"
# include <stdio.h>
# include <string.h>
# include <stdlib.h>
# include <stddef.h>
# include <sys/stat.h>
# define OBJ_TYPE struct status
static struct json_object_field status_layout [ ] = {
JSON_FIELD_INTEGER ( account_id , true ) ,
JSON_FIELD_STRING ( url , false ) ,
JSON_FIELD_BOOL ( stub , false ) ,
JSON_FIELD_BOOL ( remote , false ) ,
// DO NOT INCLUDE field content
JSON_FIELD_STRING ( source , false ) ,
JSON_FIELD_BOOL ( pinned , false ) ,
JSON_FIELD_DATETIME ( published , false ) ,
JSON_FIELD_INTEGER ( in_reply_to , false ) ,
JSON_FIELD_INTEGER ( repost_id , false ) ,
JSON_FIELD_INTEGER ( root_status_id , false ) ,
JSON_FIELD_INTEGER ( reposted_status_id , false ) ,
JSON_FIELD_ARRAY_OF_STRINGS ( media , false ) ,
JSON_FIELD_ARRAY_OF_TYPE ( reacts , false , status_react_type ) ,
JSON_FIELD_ARRAY_OF_INTS ( likes , false ) ,
JSON_FIELD_ARRAY_OF_INTS ( replies , false ) ,
JSON_FIELD_ARRAY_OF_INTS ( reposts , false ) ,
JSON_FIELD_ARRAY_OF_INTS ( mentions , false ) ,
JSON_FIELD_BOOL ( sensitive , true ) ,
JSON_FIELD_END
} ;
# undef OBJ_TYPE
void * allocate ( size_t s )
{
void * ptr = malloc ( s ) ;
memset ( ptr , 0 , s ) ;
return ptr ;
}
static const char * get_status_data_filename ( unsigned int id , char * filename , int size )
{
int millions = id / 1000000 ;
int thousands = ( id % 1000000 ) / 1000 ;
int ones = id % 1000 ;
mkdir ( format ( filename , size , " data/statuses/%03dm " , millions ) , 0755 ) ;
mkdir ( format ( filename , size , " data/statuses/%03dm/%03dk " , millions , thousands ) , 0755 ) ;
return format ( filename , size , " data/statuses/%03dm/%03dk/%03d.json " , millions , thousands , ones ) ;
}
static FILE * open_status_data_file ( unsigned int id , const char * mode )
{
char filename [ 512 ] ;
FILE * f = NULL ;
f = fopen ( get_status_data_filename ( id , filename , sizeof ( filename ) ) , mode ) ;
if ( f ) {
printf ( " Using new location at %s \n " , filename ) ;
return f ;
}
f = fopen ( format ( filename , sizeof ( filename ) , " data/statuses/%d.json " , id ) , mode ) ;
if ( f ) {
printf ( " Using existing location at %s \n " , filename ) ;
return f ;
}
}
struct status * status_from_id ( unsigned int id )
{
struct status * s = NULL ;
FILE * f = open_status_data_file ( id , " r " ) ;
if ( ! f ) { return NULL ; }
s = allocate ( sizeof ( struct status ) ) ;
s - > id = id ;
if ( ! json_read_object_layout_from_FILE ( f , status_layout , s ) ) {
printf ( " Failed to load status %d \n " , id ) ;
status_free ( s ) ;
return NULL ;
}
if ( ! s - > source ) {
s - > source = strdup ( " " ) ;
}
if ( ! s - > remote & & ! s - > url ) {
s - > url = aformat ( " https://%s/note/%d " , g_server_name , s - > id ) ;
}
if ( s - > account_id = = owner_account_id ) {
s - > remote = false ;
}
return s ;
}
struct status * status_new_repost ( struct status * s , struct account * a )
{
struct status * repost ;
repost = malloc ( sizeof ( * repost ) ) ;
memset ( repost , 0 , sizeof ( * repost ) ) ;
repost - > account_id = a - > id ;
repost - > repost_id = s - > id ;
repost - > published = time ( NULL ) ;
status_save_new ( repost ) ;
return repost ;
}
static struct status * status_from_local_uri ( const char * uri )
{
if ( 0 ! = strncmp ( " https:// " , uri , 8 ) ) { return NULL ; }
uri + = 8 ;
int server_name_length = strlen ( g_server_name ) ;
if ( 0 ! = strncmp ( g_server_name , uri , server_name_length ) ) { return NULL ; }
uri + = server_name_length ;
if ( 0 ! = strncmp ( " /note/ " , uri , 6 ) ) { return NULL ; }
uri + = 6 ;
// Note: zero is never a valid status id
int id = atoi ( uri ) ;
if ( id = = 0 ) { return NULL ; }
return status_from_id ( id ) ;
}
struct status * status_from_uri ( const char * uri )
{
// Check for local
struct status * s = status_from_local_uri ( uri ) ;
if ( s ) { return s ; }
int id = - 1 ;
if ( ! hash_index_get ( " data/statuses/uri " , uri , & id ) ) { return NULL ; }
return status_from_id ( id ) ;
}
bool status_sync_from_activity_pub ( struct status * s , struct ap_activity * act )
{
printf ( " Syncing status from activity %s \n " , act - > id ) ;
bool result = false ;
struct account * a = account_from_uri_or_fetch ( act - > actor ) ;
if ( ! a ) {
printf ( " ! Unable to get account for %s \n " , act - > actor ) ;
goto failed ;
}
s - > account_id = a - > id ;
s - > published = act - > published ;
s - > remote = true ;
s - > stub = false ;
s - > source = safe_strdup ( act - > source . content ) ;
s - > url = strdup ( act - > id ) ;
// Erase existing media
for ( int i = 0 ; i < s - > media . count ; + + i ) {
free ( s - > media . items [ i ] ) ;
}
free ( s - > media . items ) ;
memset ( & s - > media , 0 , sizeof ( s - > media ) ) ;
// Recreate the media field
for ( int i = 0 ; i < act - > attachments . count ; + + i ) {
struct ap_attachement * att = act - > attachments . items [ i ] ;
if ( att & & att - > url ) {
char * media = strdup ( att - > url ) ;
array_append ( & s - > media , sizeof ( char * ) , & media ) ;
}
}
status_save ( s ) ;
result = true ;
cleanup :
account_free ( a ) ;
return result ;
failed :
result = false ;
goto cleanup ;
} ;
bool status_sync_from_uri ( struct status * s , const char * uri )
{
struct ap_activity * act = NULL ;
FILE * f = NULL ;
// Fetch the object from the remote server
char filename [ 512 ] ;
snprintf ( filename , sizeof ( filename ) , " data/statuses/ap/%d.json " , s - > id ) ;
f = fopen ( filename , " r " ) ;
if ( ! f ) {
printf ( " * Fetching %s \n " , uri ) ;
char tmp_filename [ 512 ] ;
FILE * f = fopen ( format ( tmp_filename , 512 , " %s.tmp " , filename ) , " w " ) ;
long status_code = - 1 ;
const void * request [ ] = {
HTTP_REQ_URL , uri ,
HTTP_REQ_HEADER , " Accept: application/ld+json " ,
HTTP_REQ_OUTFILE , f ,
HTTP_REQ_RESULT_STATUS , & status_code ,
NULL ,
} ;
if ( ! http_client_do ( request ) ) {
printf ( " ! Unable to fetch %s \n " , uri ) ;
return NULL ;
}
printf ( " status_code = %d \n " , status_code ) ;
if ( status_code ! = 200 ) {
return NULL ;
}
fclose ( f ) ;
rename ( tmp_filename , filename ) ;
}
f = fopen ( filename , " r " ) ;
if ( ! f ) { return NULL ; }
// Load the activity and sync status
act = ap_activity_from_FILE ( f ) ; f = NULL ;
if ( ! act ) { goto failed ; }
if ( ! status_sync_from_activity_pub ( s , act ) ) { goto failed ; }
cleanup :
if ( f ) { fclose ( f ) ; }
ap_activity_free ( act ) ;
return s ;
failed :
if ( s ) {
printf ( " Creating stub status for later sync \n " ) ;
status_flag_for_async_fetch ( s ) ;
goto cleanup ;
}
status_free ( s ) ;
s = NULL ;
goto cleanup ;
}
bool status_sync ( struct status * s )
{
return status_sync_from_uri ( s , s - > url ) ;
}
struct status * status_from_uri_or_fetch ( const char * uri )
{
return status_fetch_from_uri ( uri ) ;
}
struct status * status_fetch_from_uri ( const char * uri )
{
struct status * s = status_from_uri ( uri ) ;
if ( ! s ) {
s = malloc ( sizeof ( * s ) ) ;
memset ( s , 0 , sizeof ( * s ) ) ;
s - > remote = true ;
s - > account_id = - 1 ;
s - > stub = true ;
s - > published = time ( NULL ) ;
s - > url = strdup ( uri ) ;
status_save_new ( s ) ;
hash_index_set ( " data/statuses/uri " , uri , s - > id ) ;
}
if ( ! status_sync_from_uri ( s , uri ) ) {
status_free ( s ) ;
return NULL ;
}
return s ;
}
struct status * status_new_system_unfollow ( int account_id )
{
struct account * a = account_from_id ( account_id ) ;
if ( ! a ) { return NULL ; }
struct status * s ;
s = malloc ( sizeof ( * s ) ) ;
memset ( s , 0 , sizeof ( * s ) ) ;
s - > id = - 1 ;
s - > account_id = - 1 ;
asprintf ( & s - > content , " %s unfollowed you \n " , a - > display_name ) ;
s - > sensitive = true ;
account_free ( a ) ;
return s ;
}
struct status * status_new_system_block ( int account_id )
{
struct account * a = account_from_id ( account_id ) ;
if ( ! a ) { return NULL ; }
struct status * s ;
s = malloc ( sizeof ( * s ) ) ;
memset ( s , 0 , sizeof ( * s ) ) ;
s - > id = - 1 ;
s - > account_id = - 1 ;
asprintf ( & s - > content , " %s blocked you \n " , a - > display_name ) ;
s - > sensitive = true ;
account_free ( a ) ;
return s ;
}
struct status * status_new_system_stub ( struct status * stub )
{
struct status * s ;
s = malloc ( sizeof ( * s ) ) ;
memset ( s , 0 , sizeof ( * s ) ) ;
s - > id = - stub - > id - 50 ;
s - > account_id = - 1 ;
s - > published = time ( NULL ) ;
asprintf ( & s - > content , " Unable to load status #%d. View on original server at <a href='%s'>%s</a> " ,
stub - > id ,
stub - > url , stub - > url
) ;
s - > sensitive = true ;
return s ;
}
struct status * status_from_activity ( struct ap_activity * act )
{
struct status * s ;
s = malloc ( sizeof ( * s ) ) ;
memset ( s , 0 , sizeof ( * s ) ) ;
if ( ! status_sync_from_activity_pub ( s , act ) ) {
status_free ( s ) ;
return NULL ;
}
return s ;
}
bool status_save_new ( struct status * s )
{
int head = - 1 ;
FILE * f = fopen ( " data/statuses/HEAD " , " r " ) ;
fscanf ( f , " %d " , & head ) ;
if ( head = = - 1 ) { return false ; }
fclose ( f ) ;
s - > id = head + 1 ;
s - > root_status_id = s - > id ;
f = fopen ( " data/statuses/HEAD.tmp " , " w " ) ;
fprintf ( f , " %d " , s - > id ) ;
fclose ( f ) ;
rename ( " data/statuses/HEAD.tmp " , " data/statuses/HEAD " ) ;
status_save ( s ) ;
return true ;
}
void status_save ( struct status * s )
{
char filename [ 512 ] ;
json_write_object_layout_to_file ( get_status_data_filename ( s - > id , filename , sizeof ( filename ) ) , " \t " , status_layout , s ) ;
// Index the status
if ( s - > url ) {
hash_index_set ( " data/statuses/uri " , s - > url , s - > id ) ;
}
}
void status_write_to_FILE ( struct status * s , FILE * f )
{
json_write_object_layout_to_FILE ( f , " \t " , status_layout , s ) ;
}
void status_free ( struct status * s )
{
if ( ! s ) { return ; }
free ( s - > url ) ;
free ( s - > content ) ;
free ( s - > source ) ;
// Free media
for ( int i = 0 ; i < s - > media . count ; + + i ) {
free ( s - > media . items [ i ] ) ;
}
free ( s - > media . items ) ;
// Free reactions
for ( int i = 0 ; i < s - > reacts . count ; + + i ) {
status_react_free ( s - > reacts . items [ i ] ) ;
}
free ( s - > reacts . items ) ;
free ( s - > likes . items ) ;
free ( s - > reposts . items ) ;
free ( s - > mentions . items ) ;
free ( s ) ;
}
struct async_status_fetch
{
struct {
int * items ;
int count ;
} ids ;
} ;
# define OBJ_TYPE struct async_status_fetch
static struct json_object_field async_fetch_layout [ ] = {
JSON_FIELD_ARRAY_OF_INTS ( ids , true ) ,
JSON_FIELD_END
} ;
# undef OBJ_TYPE
void status_flag_for_async_fetch ( struct status * s )
{
struct async_status_fetch fetch ;
memset ( & fetch , 0 , sizeof ( fetch ) ) ;
json_read_object_layout_from_file ( " data/statuses/async_fetch.json " , async_fetch_layout , & fetch ) ;
// Don't add if already flagged for async fetch
for ( int i = 0 ; i < fetch . ids . count ; + + i ) {
if ( fetch . ids . items [ i ] = = s - > id ) {
goto already_present ;
}
}
array_append ( & fetch . ids , sizeof ( s - > id ) , & s - > id ) ;
json_write_object_layout_to_file ( " data/statuses/async_fetch.json " , " \t " , async_fetch_layout , & fetch ) ;
already_present :
free ( fetch . ids . items ) ;
}
void status_add_to_timeline ( struct status * s , int timeline_id )
{
struct timeline * tl = timeline_from_id ( timeline_id ) ;
timeline_add_post ( tl , s ) ;
timeline_free ( tl ) ;
}
void status_make_reply_to ( struct status * s , int in_reply_to_id )
{
// Add this status to the other
struct status * in_reply_to = status_from_id ( in_reply_to_id ) ;
if ( ! in_reply_to ) {
s - > in_reply_to = 0 ;
} else {
s - > in_reply_to = in_reply_to_id ;
s - > root_status_id = in_reply_to - > root_status_id ;
array_append ( & in_reply_to - > replies , sizeof ( s - > id ) , & s - > id ) ;
// TODO: full mention handling
int id = in_reply_to - > account_id ;
array_append ( & s - > mentions , sizeof ( id ) , & id ) ;
status_save ( in_reply_to ) ;
}
}
void status_get_context ( struct status * s , void * ancestors_ptr , void * replies_ptr )
{
struct array_of_status {
struct status * * items ;
int count ;
} ;
struct array_of_status * ancestors = ancestors_ptr ;
struct array_of_status * replies = replies_ptr ;
memset ( ancestors , 0 , sizeof ( * ancestors ) ) ;
memset ( replies , 0 , sizeof ( * replies ) ) ;
struct status * parent = NULL ;
for ( int i = s - > in_reply_to ; i ! = 0 ; i = parent - > in_reply_to ) {
parent = status_from_id ( i ) ;
array_append ( ancestors , sizeof ( parent ) , & parent ) ;
} ;
// reverse ancestors
for ( int i = 0 ; i < ancestors - > count / 2 ; + + i ) {
struct status * a = ancestors - > items [ i ] ;
struct status * b = ancestors - > items [ ancestors - > count - i - 1 ] ;
ancestors - > items [ i ] = b ;
ancestors - > items [ ancestors - > count - i - 1 ] = a ;
}
for ( int i = 0 ; i < s - > replies . count ; + + i ) {
struct status * reply = status_from_id ( s - > replies . items [ i ] ) ;
array_append ( replies , sizeof ( reply ) , & reply ) ;
}
}
void status_add_react ( struct status * s , const char * react , struct account * a )
{
// generate outbox element
if ( a - > id = = owner_account_id ) {
if ( s - > account_id = = owner_account_id ) {
// De
printf ( " TODO: generate outbox activity for adding reaction '%s' to status #%d by account #%d, deliver to followers \n " , react , s - > id ) ;
} else {
// Deliver react to post owner
//printf( "TODO: generate outbox activity for adding reaction '%s' to status #%d by account #%d, deliver to account #%d\n", react, s->id, a->id, s->account_id );
ap_activity_react ( s , react ) ;
}
}
if ( ! s - > remote & & a - > id ! = owner_account_id ) { //&& s->account_id == owner_account_id && a->id != owner_account_id ) {
if ( s - > id ! = 0 ) {
// Create notification for liking the owner's post
struct notification * note = notification_new ( ) ;
note - > debug = 1 ;
note - > type = nt_react ;
note - > status_id = s - > id ;
note - > account_id = a - > id ;
note - > react = strdup ( react ) ;
note - > created_at = time ( NULL ) ;
notification_save ( note ) ;
notification_free ( note ) ;
}
}
struct status_react * re = NULL ;
for ( int i = 0 ; i < s - > reacts . count ; + + i ) {
if ( 0 = = strcmp ( s - > reacts . items [ i ] - > code , react ) ) {
re = s - > reacts . items [ i ] ;
goto update_entry ;
}
}
re = malloc ( sizeof ( * re ) ) ;
memset ( re , 0 , sizeof ( * re ) ) ;
re - > code = strdup ( react ) ;
array_append ( & s - > reacts , sizeof ( re ) , & re ) ;
update_entry :
for ( int i = 0 ; i < re - > accounts . count ; + + i ) {
if ( re - > accounts . items [ i ] = = a - > id ) {
// Already present
goto done ;
}
}
array_append ( & re - > accounts , sizeof ( a - > id ) , & a - > id ) ;
done :
status_save ( s ) ;
}
void status_remove_react ( struct status * s , const char * react , struct account * a )
{
// TODO: generate outbox element
printf ( " TODO: generate outbox activity for removing reaction '%s' to status #%d by account #%d \n " , react , s - > id , a - > id ) ;
/// Find react activity we need to Undo
/// Generate Undo Activity
// TODO: generate notification
/*
if ( s - > account_id = = owner_account_id & & a - > id ! = owner_account_id ) {
// Create notification for liking the owner's post
struct notification * note = notification_new ( ) ;
note - > debug = 2 ;
note - > type = nt_react ;
note - > account_id = a - > id ;
note - > react = strdup ( react ) ;
note - > created_at = time ( NULL ) ;
notification_save ( note ) ;
notification_free ( note ) ;
} */
struct status_react * re = NULL ;
for ( int i = 0 ; i < s - > reacts . count ; + + i ) {
if ( 0 = = strcmp ( s - > reacts . items [ i ] - > code , react ) ) {
re = s - > reacts . items [ i ] ;
goto update_entry ;
}
}
return ;
update_entry :
array_delete ( & re - > accounts , sizeof ( a - > id ) , & a - > id ) ;
if ( re - > accounts . count = = 0 ) {
array_delete ( & s - > reacts , sizeof ( re ) , & re ) ;
status_react_free ( re ) ;
}
status_save ( s ) ;
}
void status_add_like ( struct status * s , struct account * a )
{
for ( int i = 0 ; i < s - > likes . count ; + + i ) {
if ( s - > likes . items [ i ] = = a - > id ) {
return ;
}
}
if ( ! s - > stub ) {
if ( s - > account_id = = owner_account_id ) {
if ( a - > id ! = owner_account_id ) {
// Create notification for liking the owner's post
struct notification * note = notification_new ( ) ;
note - > debug = 3 ;
note - > type = nt_like ;
note - > account_id = a - > id ;
note - > status_id = s - > id ;
note - > created_at = time ( NULL ) ;
notification_save ( note ) ;
notification_free ( note ) ;
}
} else {
ap_activity_like ( s ) ;
}
}
status_save ( s ) ;
}