#define _GNU_SOURCE #include "account.h" // Submodules #include "json/json.h" #include "json/layout.h" #include "http/client/client.h" #include "ffdb/fs_list.h" #include "ffdb/hash_index.h" #include "ffdb/trie.h" #include "format.h" #include "collections/array.h" // Model #include "model/server.h" #include "model/status.h" #include "model/ap/account.h" #include "model/ap/activity.h" #include "model/ap/outbox_envelope.h" #include "model/crypto/keys.h" #include "model/notification.h" #include "view/api/Relationship.h" // Stdlib #include #include #include #include #include #include static const char* safe( const char* value, const char* other ) { if( !value ) { return other; } return value; } static const char* b(bool value) { return value ? "true" : "false"; } static struct json_enum account_types_enum[] = { { "owner", at_owner }, { "bot", at_bot }, { "activity_pub", at_remote_activity_pub }, { "rss", at_rss_feed }, { NULL }, }; #define OBJ_TYPE struct account static struct json_object_field account_layout[] = { JSON_FIELD_STRING( handle, true ), JSON_FIELD_STRING( server, true ), JSON_FIELD_STRING( display_name, true ), JSON_FIELD_STRING( avatar, false ), { .key = "avatar", .offset = offsetof( struct account, avatar.url ), .required = false, .type = &json_field_string }, { .key = "avatar_static", .offset = offsetof( struct account, avatar.static_url ), .required = false, .type = &json_field_string }, JSON_FIELD_ARRAY_OF_STRINGS( aliases, false ), JSON_FIELD_ENUM( account_type, account_types_enum, true ), JSON_FIELD_STRING( inbox, false ), JSON_FIELD_STRING( shared_inbox, false ), JSON_FIELD_STRING( note, false ), { .key = "followers", .offset = offsetof( struct account, followers_count ), .required = false, .type = &json_field_integer }, { .key = "following", .offset = offsetof( struct account, following_count ), .required = false, .type = &json_field_integer }, JSON_FIELD_INTEGER( follow_activity, false ), JSON_FIELD_STRING( account_url, true ), JSON_FIELD_END, }; static struct account* new_system_account() { struct account* a; a = malloc(sizeof(*a)); memset(a,0,sizeof(*a)); a->id = -1; a->handle = strdup("system"); a->display_name = strdup("Apogee System"); a->account_type = at_bot, a->bot = true; asprintf( &a->avatar.url, "https://%s/system-account.png", g_server_name ); a->avatar.static_url = strdup(a->avatar.url); return a; } static bool index_uri_to_account_id( const char* uri, int account_id ); void account_reindex() { int max_account_id = fs_list_get( "data/accounts/HEAD" ); for( int i = 0; i < max_account_id+1; ++i ) { struct account* a = account_from_id(i); if( !a ) { continue; } if( a->account_url ) { index_uri_to_account_id( a->account_url, a->id ); } account_index_webfinger( a ); account_free(a); } } struct account* account_from_id( int id ) { switch( id ) { case -1: // System account return new_system_account(); } char filename[512]; snprintf( filename, 512, "data/accounts/%d.json", id ); struct account* a = malloc(sizeof(struct account)); memset( a, 0, sizeof(struct account) ); a->id = id; if( !json_read_object_layout_from_file( filename, account_layout, a ) ) { account_free(a); return NULL; } return a; } static bool index_uri_to_account_id( const char* uri, int account_id ) { return hash_index_set( "data/accounts/uri_index/", uri, account_id ); } static int lookup_account_id_from_uri( const char* uri ) { int result = 0; if( !hash_index_get( "data/accounts/uri_index/", uri, &result ) ) { return -1; } return result; } struct account* account_from_uri( const char* uri ) { struct account* result = NULL; // Handle owner as special case char buffer[512]; snprintf( buffer, 512, "https://%s/owner/actor", g_server_name ); if( 0 == strcmp(buffer,uri) ) { return account_from_id(0); } // TODO: handle bots int account_id = lookup_account_id_from_uri( uri ); if( account_id == -1 ) { printf( "Failed to lookup local account id for %s\n", uri ); return NULL; } return account_from_id( account_id ); } struct account* account_from_uri_or_fetch( const char* uri ) { struct account* res = account_from_uri(uri); if( res ) { return res; } return account_fetch_from_uri( uri ); } void account_index_webfinger( struct account* a ) { char webfinger_name[512]; snprintf( webfinger_name, 512, "%s@%s", a->handle, a->server ); hash_index_set( "data/accounts/webfinger", webfinger_name, a->id ); } struct account* account_from_webfinger( const char* handle ) { //printf( "account_from_webfinger( %s )\n", handle ); int id; if( !hash_index_get( "data/accounts/webfinger", handle, &id ) ) { printf( "! No account id for %s\n", handle ); printf( "! TODO: implement webfinger lookup\n" ); return NULL; } return account_from_id(id); } struct ap_account* account_activity_pub_data( struct account* a ) { char filename[512]; return ap_account_from_file( format( filename, 512, "data/accounts/%d/ap.json", a->id ) ); } bool account_sync_from_activity( struct account* a, struct ap_activity* act ) { for( int i = 0; i < a->aliases.count; ++i ) { free( a->aliases.items[i] ); } a->aliases.count = 0; for( int i = 0; i < act->also_known_as.count; ++i ) { char* str = strdup(act->also_known_as.items[i]); array_append( &a->aliases, sizeof(str), &str ); } return true; } bool account_sync_from_activity_pub( unsigned int account_id ) { char filename[512]; struct ap_account* ap = ap_account_from_file( format( filename, 512, "data/accounts/%d/ap.json", account_id ) ); if( !ap ) { printf( "! Failed to sync account %d from %s\n", account_id, filename ); return false; } printf( "ap = " ); ap_account_debug_dump(ap); struct account* a = malloc(sizeof(struct account)); memset(a,0,sizeof(*a)); a->id = account_id; a->handle = strdup(ap->preferredUsername); if( ap->name ) { a->display_name = strdup(ap->name); } else { a->display_name = strdup(ap->preferredUsername); } if( ap->avatar ) { a->avatar.url = strdup(ap->avatar); a->avatar.static_url = strdup(ap->avatar); } a->bot = ( ap->type != apacct_Person ); a->account_type = at_remote_activity_pub; a->account_url = strdup(ap->url); a->inbox = strdup(ap->inbox); if( ap->shared_inbox ) { a->shared_inbox = strdup(ap->shared_inbox); } if( 0 == strncmp( ap->id, "https://", 8 ) ) { char* server_name = strdup(&ap->id[8]); char* discard; strtok_r(server_name,"/",&discard); a->server = server_name; } // Extract out the public key char* id = strdup(ap->public_key.id); char* key_id = NULL; strtok_r( id, "#", &key_id ); FILE* key_pem = fopen( format(filename,sizeof(filename),"data/accounts/%d/%s.pem", a->id, key_id), "w" ); if( !key_pem ) { printf( "Unable to save public key to %s\n", filename ); } else { printf( "Writing public key to %s\n", filename ); fprintf( key_pem, "%s", ap->public_key.pem ); fclose(key_pem); } account_save(a); account_index_webfinger(a); ap_account_free(ap); account_free(a); return true; } struct crypto_keys* account_get_public_key( struct account* a, const char* key_name ) { char filename[512]; FILE* f = fopen( format( filename, sizeof(filename), "data/accounts/%d/%s.pem", a->id, key_name ), "r" ); if( !f ) { printf( "Failed to open file %s\n", filename ); return NULL; } fclose(f); struct crypto_keys* keys = crypto_keys_new(); if( crypto_keys_load_public( keys, filename ) ) { return keys; } printf( "Failed to load public key from %s\n", filename ); crypto_keys_free(keys); return NULL; } struct crypto_keys* account_get_private_key( struct account* a ) { return NULL; } static void create_account_skeleton( int account_id ) { char b[512]; // Make sure the account directory exists mkdir( format( b, 512, "data/accounts/%d", account_id ), 0755 ); mkdir( format( b, 512, "data/accounts/%d/timeline", account_id ), 0755 ); fs_list_set( format( b, 512, "data/accounts/%d/timeline/HEAD", account_id ), 0 ); } struct account* account_fetch_from_uri( const char* uri ) { int account_id = lookup_account_id_from_uri( uri ); if( -1 == account_id ) { account_id = fs_list_get( "data/accounts/HEAD" ) + 1; fs_list_set( "data/accounts/HEAD", account_id ); index_uri_to_account_id( uri, account_id ); } create_account_skeleton(account_id); // Fetch the ActivityPub actor data if we don't already have it char filename[512]; FILE* f = fopen( format( filename, 512, "data/accounts/%d/ap.json", account_id ), "r" ); if( !f ) { char tmp_filename[512]; snprintf( tmp_filename, 512, "%s.tmp", filename ); printf( "tmp_filename = %s\n", tmp_filename ); FILE* tmp = fopen(tmp_filename,"w"); if( !tmp ) { printf( "! Unable to open %s\n", tmp_filename ); return NULL; } long status_code = -1; const void* request[] = { HTTP_REQ_URL, uri, HTTP_REQ_HEADER, "Accept: application/ld+json", HTTP_REQ_OUTFILE, tmp, HTTP_REQ_RESULT_STATUS, &status_code, NULL, }; if( !http_client_do( request ) ) { printf( "! Unable to fetch %s\n", uri ); return NULL; } fflush(tmp); printf( "status_code = %d\n", status_code ); rename(tmp_filename,filename); } // Fail if we can't sync if( !account_sync_from_activity_pub( account_id ) ) { return NULL; } return account_from_id(account_id); } void account_free( struct account* a ) { if( !a ) { return; } free(a->handle); free(a->server); free(a->inbox); free(a->shared_inbox); free(a->display_name); free(a->account_url); free(a->avatar.url); free(a->avatar.static_url); for( int i = 0; i < a->aliases.count; ++i ) { free( a->aliases.items[i] ); } free(a->aliases.items); free(a->note); free(a); } void account_save( struct account* a ) { char filename[512]; snprintf( filename, 512, "data/accounts/%d.json", a->id ); json_write_object_layout_to_file( filename, "\t", account_layout, a ); } void account_add_follower( struct account* a, struct account* follower ) { // Insert an entry for this follower (only does something if not already set) char filename[512]; char key[32]; ffdb_trie_set( format( filename, sizeof(filename), "data/accounts/%d/followers", a->id ), format(key,sizeof(key),"%d", follower->id), "T" ); a->followers_count = ffdb_trie_count(filename); account_save(a); ffdb_trie_set( format( filename, sizeof(filename), "data/accounts/%d/following", follower->id ), format(key,sizeof(key),"%d", a->id), "T" ); follower->following_count = ffdb_trie_count(filename); account_save(follower); // Create notification for follow struct notification* note = notification_new(); note->debug = 4; note->type = nt_follow; note->account_id = follower->id; note->created_at = time(NULL); notification_save( note ); notification_free( note ); } void account_remove_follower( struct account* a, struct account* follower ) { // Remove the follow char filename[512]; char key[32]; ffdb_trie_remove( format( filename, sizeof(filename), "data/accounts/%d/followers", a->id ), format( key, sizeof(key), "%d", follower->id ) ); ffdb_trie_remove( format( filename, sizeof(filename), "data/accounts/%d/following", follower->id ), format( key, sizeof(key), "%d", a->id ) ); // Update follower count a->followers_count = ffdb_trie_count(filename); // create a notification for unfollow struct notification* note = notification_new(); note->debug = 5; note->type = nt_unfollow; note->account_id = -1; note->ref_account_id = follower->id; note->created_at = time(NULL); notification_save( note ); notification_free( note ); } void account_follow( struct account* a, struct account* to_follow ) { char index[512]; char key[32]; char value[32]; // Federate Follow Activity int act_id = ap_activity_follow( a, to_follow ); // Update following list ffdb_trie_set( format(index,512,"data/accounts/%d/following", a->id), format(key,32,"%d", to_follow->id ), format(value,32,"%d", act_id ) ); a->following_count = ffdb_trie_count(index); // Save account data account_save(a); } void account_announce( struct account* a, struct status* s ) { struct ap_activity* act = ap_activity_new(); int id = fs_list_get("data/activities/HEAD") + 1; act->id = aformat( "https://%s/activity/%d", g_server_name, id ); act->local_id = id; act->published = time(NULL); act->type = apat_announce; act->actor = strdup(a->account_url); act->object.tag = apaot_ref; act->object.ref = strdup(s->url); // Create To: list char* str = strdup("https://www.w3.org/ns/activitystreams#Public"); array_append( &act->to, sizeof(str), &str ); str = aformat("https://%s/owner/followers", g_server_name ); array_append( &act->to, sizeof(str), &str ); struct account* origin_post_account = account_from_id(s->account_id); str = strdup(origin_post_account->account_url); array_append( &act->to, sizeof(str), &str ); struct outbox_envelope_list oel; memset(&oel,0,sizeof(oel)); account_deliver_activity_to_followers( a, act, &oel ); account_deliver_activity( origin_post_account, act, &oel ); printf( "Delivering to %s inboxes\n", oel.count ); outbox_envelope_list_save(&oel); outbox_envelope_list_free_composite(&oel); ap_activity_save(act); ap_activity_write_to_FILE( act, stdout ); cleanup: account_free(origin_post_account); ap_activity_free(act); } void account_deliver_activity( struct account* a, struct ap_activity* act, struct outbox_envelope_list* oel ) { printf( "Delivering activity %s to account %s\n", act->id, a->account_url ); if( a->shared_inbox ) { // Check if the sh for( int i = 0; i < oel->count; ++i ) { struct outbox_envelope* e = oel->items[i]; if( e->shared_inbox && a->shared_inbox && 0 == strcmp(e->shared_inbox,a->shared_inbox) ) { printf( "\tUsing shared inbox %s already in delivery list\n", a->shared_inbox ); // This account will get the message delivered thru the shared inbox return; } } } struct outbox_envelope* env = outbox_envelope_new(); env->activity_id = act->local_id; if( a->shared_inbox ) { env->shared_inbox = strdup(a->shared_inbox); } env->account_id = a->id; array_append( oel, sizeof(env), &env ); } void account_deliver_activity_to_followers( struct account* a, struct ap_activity* act, struct outbox_envelope_list* oel ) { printf( "Delivering activity %s to followers of account %s\n", act->id, a->account_url ); struct { char** items; int count; } keys; memset( &keys, 0, sizeof(keys) ); char filename[512]; snprintf( filename, sizeof(filename), "data/accounts/%d/followers", a->id ); // TODO: handle shared inbox delivery int pages = (a->followers_count+31) / 32; printf( "\tpages = %d, followers = %d\n", pages, a->followers_count ); for( int i = 0; i < pages; ++i ) { keys.count = 0; ffdb_trie_list( filename, i * 32, 32, &keys, NULL ); printf( "keys.count = %d\n", keys.count ); for( int j = 0; j < keys.count; ++j ) { int account_id = atoi(keys.items[j]); printf( "account_id = %d\n", account_id ); struct account* follower_account = account_from_id(account_id); account_deliver_activity( follower_account, act, oel ); account_free(follower_account); } } free(keys.items); } void account_unfollow( struct account* a, struct account* to_unfollow ) { char index[512]; char key[32]; // Make sure the account to unfollow has previously been followed char* res = ffdb_trie_get( format(index,512,"data/accounts/%d/following", a->id), format(key,32,"%d", to_unfollow->id) ); if( !res ) { printf( "%s is not following %s\n", a->account_url, to_unfollow->account_url ); return; } // Lookup the Activity used to federate following this account struct ap_activity* act = ap_activity_from_local_id( atoi(res) ); free(res); // Federate Undo Activity ap_activity_undo(act,to_unfollow->id); ap_activity_free(act); // Update following list ffdb_trie_remove( index, key ); a->following_count = ffdb_trie_count(index); // Save account data account_save(a); } struct int_array { int* items; int count; }; static void account_list( const char* filename, int offset, int limit, struct int_array* array ) { struct { char** items; int count; } keys; memset( &keys, 0, sizeof(keys) ); ffdb_trie_list( filename, offset, limit, &keys, NULL ); array->items = malloc( sizeof(int) * keys.count ); for( int i = 0; i < keys.count; ++i ) { char* str = keys.items[i]; array->items[i] = atoi( str ); free( str ); } free(keys.items); array->count = keys.count; } void account_list_followers( struct account* a, int offset, int limit, void* id_array ) { char filename[512]; account_list( format( filename, sizeof(filename), "data/accounts/%d/followers", a->id ), offset, limit, id_array ); } void account_list_following( struct account* a, int offset, int limit, void* id_array ) { char filename[512]; account_list( format( filename, sizeof(filename), "data/accounts/%d/following", a->id ), offset, limit, id_array ); } void account_move( struct account* a, const char* new_uri ) { printf( "TODO: implement account move\n" ); } bool account_does_follow( struct account* a, int account_id ) { char index[512]; char key[32]; // Make sure the account to unfollow has previously been followed char* value = ffdb_trie_get( format(index,512,"data/accounts/%d/following", a->id), format(key,32,"%d", account_id) ); //printf( "account_does_follow( %d, %d ) => %s (%c)\n", a->id, account_id, value, !!value ? 'T' : 'F' ); bool result = !!value; free(value); return result; }