#define _GNU_SOURCE #include "status.h" #include "status/react.h" // Submodules #include "json/json.h" #include "json/layout.h" #include "ffdb/fs_list.h" #include "ffdb/hash_index.h" #include "ffdb/trie.h" #include "sha256/sha256.h" #include "collections/array.h" #include "http/client/client.h" #include "util/format.h" #include "ap/object.h" // Model #include "model/server.h" #include "model/account.h" #include "model/activity.h" #include "model/notification.h" #include "model/timeline.h" #include "model/emoji.h" #include "model/media.h" // Standard Library #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_INTEGER( activity_id, false ), JSON_FIELD_STRING( url, false ), JSON_FIELD_BOOL( stub, false ), JSON_FIELD_BOOL( remote, false ), JSON_FIELD_STRING( content, false ), JSON_FIELD_STRING( source, false ), JSON_FIELD_BOOL( pinned, false ), JSON_FIELD_BOOL( bookmarked, 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( media2, false, media_type ), 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_ARRAY_OF_TYPE( emoji, false, emoji_type ), 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; } return NULL; } struct status* status_from_id( unsigned int id ) { if( id == 0 ) { return NULL; } 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; } // Convert media to media2 if( s->media.count != 0 ) { for( int i = 0; i < s->media.count; ++i ) { struct media* local = media_from_local_uri( s->media.items[i] ); if( local ) { array_append( &s->media2, sizeof(local), &local ); continue; } struct media* remote = media_new(); remote->preview_url = strdup(s->media.items[i]); remote->remote_url = strdup(s->media.items[i]); remote->content_type = strdup("image/*"); array_append( &s->media2, sizeof(remote), &remote ); free(s->media.items[i] ); } free( s->media.items ); memset( &s->media, 0, sizeof(s->media) ); status_save(s); } if( !s->source ) { s->source = strdup(""); } if( !s->remote && !s->url ) { s->url = aformat( "https://%s/note/%d", g_server->domain, 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->domain); if( 0 != strncmp( g_server->domain, 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); } void status_add_reply( struct status* s, struct status* child ) { if( child->id == 0 ) { return; } for( int i = 0; i < s->replies.count; ++i ) { if( s->replies.items[i] == child->id ) { return; } } int id = child->id; array_append( &s->replies, sizeof(id), &id ); status_save(s); } void status_add_mention( struct status* s, int id ) { for( int i = 0; i < s->mentions.count; ++i ) { if( s->mentions.items[i] == id ) { // Already mentioned, don't add a second time return; } } array_append( &s->mentions, sizeof(id), &id ); } void status_add_repost( struct status* s, struct status* repost ) { // TODO: implement } bool status_sync_from_activity_pub( struct status* s, struct ap_object* act ) { if( !act->actor && act->attributed_to ) { act->actor = strdup( act->attributed_to ); } printf( "Syncing status from activity %s\n", act->id ); ap_object_write_to_FILE( act, stdout ); 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->sensitive = act->sensitive; if( act->in_reply_to ) { struct status* parent = status_from_uri_or_fetch( act->in_reply_to ); if( parent ) { status_make_reply_to( s, parent->id ); // DO NOT SAVE parent! This is done inside status_make_reply_to status_free(parent); } } for( int i = 0; i < s->emoji.count; ++i ) { emoji_free( s->emoji.items[i] ); } s->emoji.count = 0; s->mentions.count = 0; for( int i = 0; i < act->tags.count; ++i ) { struct ap_activity_tag* tag = act->tags.items[i]; if( tag->type == aptag_mention ) { struct account* a = account_from_uri_or_fetch( tag->href ); if( a ) { int item = a->id; array_append( &s->mentions, sizeof(item), &item ); account_free(a); } } else if( tag->type == aptag_emoji ) { struct emoji* e; e = malloc(sizeof(*e)); memset(e,0,sizeof(*e)); e->url = safe_strdup( tag->icon.url ); e->shortcode = safe_strdup( &tag->name[1] ); e->shortcode[ strlen(e->shortcode) - 1 ] = '\0'; array_append( &s->emoji, sizeof(e), &e ); } } free(s->source); s->source = safe_strdup(act->source.content); free(s->url); s->url = strdup( act->id ); if( !s->source ) { printf( "? No source, using content directly\n" ); free(s->content); s->content = safe_strdup(act->content.content); } // 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)); for( int i = 0; i < s->media2.count; ++i ) { media_free( s->media2.items[i] ); } free(s->media2.items); memset(&s->media2,0,sizeof(s->media2)); // Recreate the media field for( int i = 0; i < act->attachments.count; ++i ) { struct ap_object* att = act->attachments.items[i]; if( att && att->url ) { struct media* media = media_new(); media->remote_url = strdup( att->url ); media->content_type = strdup(att->media_type); array_append( &s->media2, sizeof(char*), &media ); } } status_save(s); status_add_to_timeline( s, a->id ); result = true; cleanup: account_free(a); return result; failed: result = false; goto cleanup; }; bool pull_remote_file( const char* filename, const char* uri ) { 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; profile=\"https://www.w3.org/ns/activitystreams\"", HTTP_REQ_OUTFILE, f, HTTP_REQ_RESULT_STATUS, &status_code, NULL, }; if( !http_client_do( request ) ) { printf( "! Unable to fetch %s\n", uri ); fclose(f); return false; } printf( "status_code = %ld\n", status_code ); if( status_code == 200 ) { // success fclose(f); rename(tmp_filename,filename); return true; } else if( status_code == 401 ) { // Not Authorized // TODO: perform signed fetch fclose(f); return false; } // Failure fclose(f); return false; } bool status_sync_from_uri( struct status* s, const char* uri ) { struct ap_object* act = NULL; FILE* f = NULL; bool result = false; mkdir_p( "data/statuses/ap", 0755 ); // 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 ) { if( !pull_remote_file( filename, uri ) ) { goto failed; } f = fopen(filename,"r"); if( !f ) { goto failed; } } // Load the activity and sync status act = ap_object_from_FILE(f); f = NULL; if( !act ) { goto failed; } if( !status_sync_from_activity_pub(s,act) ) { goto failed; } result = true; cleanup: if( f ) { fclose(f); } ap_object_free(act); return result; failed: if( s ) { printf( "Creating stub status for later sync\n" ); char filename[512]; ffdb_trie_set( "data/statuses/stubs", format(filename,512,"%d",s->id), "T" ); goto cleanup; } status_free(s); result = false; 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 ) { struct status* s = status_from_uri(uri); if( s ) { return s; } 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); free(s->url); 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->published = time(NULL); 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->published = time(NULL); s->account_id = -1; asprintf( &s->content, "%s blocked you. View their account here.", a->display_name, a->account_url ); s->sensitive = false; 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; //-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 = false; return s; } struct status* status_from_activity( struct ap_object* 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; } void status_assign_local_id( struct status* s ) { if( s->id ) { return; } mkdir_p( "data/statuses", 0755 ); int head = fs_list_get( "data/statuses/HEAD" ) + 1; fs_list_set( "data/statuses/HEAD", head ); s->id = head; s->root_status_id = s->id; } bool status_save_new( struct status* s ) { status_assign_local_id( s ); 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 ); } if( s->stub ) { mkdir_p( "data/statuses/stubs", 0755 ); ffdb_trie_set( "data/statuses/stubs", format(filename,512,"%d",s->id), "T" ); } else { ffdb_trie_remove( "data/statuses/stubs", format(filename,512,"%d",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); for( int i = 0; i < s->media2.count; ++i ) { media_free(s->media2.items[i] ); } free(s->media2.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->replies.items); free(s->reposts.items); free(s->mentions.items); for( int i = 0; i < s->emoji.count; ++i ) { emoji_free( s->emoji.items[i] ); } free(s->emoji.items); free(s); } void status_delete( struct status* s ) { int ids[] = { s->account_id, home_timeline_id, public_timeline_id }; for( int i = 0; i < sizeof(ids)/sizeof(ids[0]); ++i ) { struct timeline* tl = timeline_from_id( ids[i] ); if( tl ) { timeline_remove_post( tl, s ); timeline_free(tl); } } char filename[512]; ffdb_trie_remove( "data/statuses/stubs", format(filename,512,"%d",s->id) ); get_status_data_filename(s->id,filename,sizeof(filename)); remove(filename); status_free(s); } 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 { // Setup this status's reply fields s->in_reply_to = in_reply_to_id; s->root_status_id = in_reply_to->root_status_id; // Record in parent as reply status_add_reply( in_reply_to, s ); // Mention the account that made the post being replied to status_add_mention( s, in_reply_to->account_id ); // Mention everytone else in that post for( int i = 0; i < in_reply_to->mentions.count; ++i ) { status_add_mention( s, in_reply_to->mentions.items[i] ); } status_save(in_reply_to); } status_free(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 #%u by account #%u, deliver to followers\n", react, s->id, a->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 ); 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; } } int id = a->id; array_append( &s->likes, sizeof(id), &id ); 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 ); } } if( a->id == owner_account_id ) { activity_like( s ); } } status_save(s); } void status_remove_like( struct status* s, struct account* a ) { for( int i = 0; i < s->likes.count; ++i ) { if( s->likes.items[i] == a->id ) { // Swap with last element s->likes.items[i] = s->likes.items[ s->likes.count-1 ]; // Then discard the last element s->likes.count -= 1; break; } } if( !s->stub ) { if( a->id == owner_account_id ) { // TODO: undo activity // activity_like( s ); } } status_save(s); } void status_set_bookmark( struct status* s ) { s->bookmarked = true; mkdir_p( "data/bookmarks", 0755 ); char key[32]; ffdb_trie_set( "data/owner/bookmarks", format(key,32,"%d",s->id), "T" ); status_save(s); } void status_clear_bookmark( struct status* s ) { s->bookmarked = false; char key[32]; ffdb_trie_set( "data/owner/bookmarks", format(key,32,"%d",s->id), NULL ); status_save(s); } void status_get_bookmarks( int offset, int limit, void* results_ptr ) { struct { char** items; int count; } keys; memset( &keys, 0, sizeof(keys) ); struct { struct status** items; int count; } *results = results_ptr; results->count = 0; ffdb_trie_list( "data/owner/bookmarks", offset, limit, &keys, NULL ); for( int i = 0; i < keys.count; ++i ) { int id; if( 1 == sscanf( keys.items[i], "%d", &id ) ) { struct status* s = status_from_id(id); if( s ) { array_append( results_ptr, sizeof(s), &s ); } } free( keys.items[i] ); } free( keys.items ); }