#define _GNU_SOURCE #include "account.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 "util/format.h" #include "collections/array.h" #include "ap/object.h" // Model #include "model/server.h" #include "model/status.h" #include "model/crypto/keys.h" #include "model/notification.h" // View #include "view/api/Relationship.h" // Stdlib #include #include #include #include #include #include bool pull_remote_file( const char* filename, const char* uri ); 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 string_pair static struct json_object_field string_pair_layout[] = { JSON_FIELD_STRING( key, true ), JSON_FIELD_STRING( value, true ), JSON_FIELD_END, }; #undef OBJ_TYPE static struct json_field_type string_pair_type = { .reader = json_field_object_composite_reader, .writer = json_field_object_composite_writer, .layout = string_pair_layout, .size = sizeof(struct string_pair), }; #define OBJ_TYPE struct account static struct json_object_field account_layout[] = { JSON_FIELD_STRING( handle, true ), JSON_FIELD_BOOL( local, false ), JSON_FIELD_BOOL( defunct, false ), JSON_FIELD_STRING( server, true ), JSON_FIELD_STRING( display_name, true ), JSON_FIELD_BOOL( stub, false ), JSON_FIELD_DATETIME( next_stub_recheck, false ), 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_STRING( banner, false ), 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 }, { .key = "fields", .offset = offsetof(OBJ_TYPE, fields), .required = false, .type = &json_field_array_of, .array_item_type = &string_pair_type, }, 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 = system_account_id; a->handle = strdup("system"); a->display_name = strdup("Apogee System"); a->account_type = at_bot, a->server = strdup(g_server->domain); a->bot = true; asprintf( &a->avatar.url, "https://%s/system-account.png", g_server->domain ); 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: case system_account_id: // 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; } if( !a->banner ) { a->banner = aformat( "https://%s/server/default-banner.blob", g_server->domain ); } if( a->id == owner_account_id ) { a->local = true; } 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 ) { if( !uri ) { return NULL; } // Handle owner as special case char buffer[512]; snprintf( buffer, 512, "https://%s/owner/actor", g_server->domain ); 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; } struct account* a = account_from_id( account_id ); // Only return non-stub accounts here. This will force a refetch attempt if( a && a->stub ) { account_free(a); return NULL; } return a; } struct account* account_from_uri_or_fetch( const char* uri ) { if( !uri ) { return NULL; } 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 ); } char* webfinger_query( const char* handle, const char* rel, const char* type ); struct account* account_from_webfinger( const char* handle ) { int id; if( hash_index_get( "data/accounts/webfinger", handle, &id ) ) { return account_from_id(id); } char* account_uri = webfinger_query( handle, "self", "application/activity+json" ); if( !account_uri ) { return NULL; } struct account* a = account_from_uri_or_fetch( account_uri ); free(account_uri); return a; } 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 ) { if( !uri ) { return NULL; } 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); struct account* a = NULL; a = account_from_id(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 ) { if( a && a->stub && ( time(NULL) < a->next_stub_recheck ) ) { goto next; } if( pull_remote_file( filename, uri ) ) { goto next; } // Mark as stub if( !a ) { a = malloc(sizeof(*a)); memset(a,0,sizeof(*a)); } a->id = account_id; a->account_url = strdup(uri); a->stub = true; a->next_stub_recheck = time(NULL) + 3600; // minimum 1 hour between checks a->display_name = aformat( "stub-%d", a->id ); account_save(a); account_index_webfinger(a); account_free(a); return NULL; next: /* nop */; } else { fclose(f); } // 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->display_name); free(a->account_url); free(a->inbox); free(a->shared_inbox); free(a->avatar.url); free(a->avatar.static_url); free(a->banner); for( int i = 0; i < a->aliases.count; ++i ) { free( a->aliases.items[i] ); } free(a->aliases.items); for( int i = 0; i < a->fields.count; ++i ) { free( a->fields.items[i].key ); free( a->fields.items[i].value ); } free( a->fields.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 if( follower->id != owner_account_id ) { 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 ) ); a->followers_count = ffdb_trie_count(filename); ffdb_trie_remove( format( filename, sizeof(filename), "data/accounts/%d/following", follower->id ), format( key, sizeof(key), "%d", a->id ) ); follower->following_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 ); } 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; }