#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 #include #include #include #include #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 %s", 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); }