You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

827 lines
21 KiB
C

#define _GNU_SOURCE
#include "account.h"
// Model
#include "model/server.h"
#include "model/status.h"
#include "model/crypto/keys.h"
#include "model/notification.h"
#include "model/fetch.h"
#include "model/webfinger.h"
#include "model/timeline.h"
#include "model/activity.h"
// View
#include "view/api/Relationship.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 "collections/array.h"
#include "ap/object.h"
#include "util/format.h"
#include "util/time.h"
// Stdlib
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
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_INTEGER( replaced_by, false ),
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_DATETIME( next_update, 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,
},
{
.key = "emoji",
.offset = offsetof(OBJ_TYPE, emoji),
.required = false,
.type = &json_field_array_of,
.array_item_type = &string_pair_type,
},
{
.key = "pinned",
.offset = offsetof(OBJ_TYPE,pinned_posts),
.type = &json_field_array_of,
.array_item_type = &json_field_integer,
},
JSON_FIELD_INTEGER( follow_activity, false ),
JSON_FIELD_STRING( account_url, true ),
JSON_FIELD_STRING( account_id, false ),
JSON_FIELD_END,
};
struct account* account_new()
{
struct account* a;
a = malloc(sizeof(*a));
memset(a,0,sizeof(*a));
return a;
}
static struct account* new_system_account()
{
struct account* a = account_new();
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);
}
}
static struct account* account_load_from_id( int id, int recurse_limit )
{
if( recurse_limit <= 0 ) {
printf( "Recursion safetly limit hit while loading account %d\n", id );
return NULL;
}
switch( id ) {
case -1:
case system_account_id:
// System account
return new_system_account();
}
char filename[512];
struct account* a = account_new();
a->id = id;
snprintf( filename, 512, "data/accounts/%d/data.json", id );
if( !json_read_object_layout_from_file( filename, account_layout, a ) ) {
account_free(a);
a = account_new();
a->id = id;
snprintf( filename, 512, "data/accounts/%d.json", id );
if( !json_read_object_layout_from_file( filename, account_layout, a ) ) {
account_free(a);
return NULL;
}
}
if( a->replaced_by && a->replaced_by != a->id ) {
int new_id = a->replaced_by;
account_free(a);
return account_load_from_id( new_id, recurse_limit - 1 );
}
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;
}
struct account* account_from_id( int id )
{
return account_load_from_id( id, 10 );
}
static bool index_uri_to_account_id( const char* uri, int account_id )
{
char id_str[32];
snprintf( id_str,32, "%d", account_id );
char* old_id = ffdb_trie_get( "data/accounts/by-uri", uri );
if( old_id ) {
if( atoi(old_id) < account_id ) {
printf( "Existing account at id=%s\n", old_id );
free( old_id );
return true;
}
free( old_id );
}
ffdb_trie_set( "data/accounts/by-uri", uri, id_str );
hash_index_remove( "data/accounts/uri_index/", uri );
//return hash_index_set( "data/accounts/uri_index/", uri, account_id );
return true;
}
/*
static int account_search_for_id_from_uri( const char* uri )
{
printf( "Searching for account id for %s\n", uri );
int max_account_id = fs_list_get( "data/accounts/HEAD" );
int result = -1;
for( int i = 0; (result == -1) && (i < max_account_id); ++i ) {
struct account* a = account_from_id(i);
if( !a ) { continue; }
if( !a->account_id ) {
// Force syncing
account_sync_from_activity_pub( i, true );
}
if( a->account_url && (0 == strcmp(a->account_url,uri)) ) {
result = a->id;
account_save(a);
} else if( a->account_id && (0 == strcmp(a->account_id,uri)) ) {
result = a->id;
account_save(a);
}
account_free(a);
}
printf( "result is %d\n", result );
return result;
}
//*/
int account_id_from_uri( const char* uri )
{
char* id_str = ffdb_trie_get( "data/accounts/by-uri", uri );
if( id_str ) {
int id = atoi(id_str);
free(id_str);
return id;
}
int result = 0;
if( !hash_index_get( "data/accounts/uri_index/", uri, &result ) ) {
//return account_search_for_id_from_uri( uri );
return -1;
}
// Migrate to trie
{
char id_str[32];
snprintf( id_str,32, "%d", result );
ffdb_trie_set( "data/accounts/by-uri", uri, id_str );
hash_index_remove( "data/accounts/uri_index/", uri );
}
return result;
}
static int lookup_account_id_from_uri( const char* uri )
{
return account_id_from_uri(uri);
}
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 ) {
printf( "This account is a stub\n" );
account_free(a);
return NULL;
}
index_uri_to_account_id( a->account_url, a->id );
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 );
}
struct account* account_from_webfinger( const char* handle, const char* default_server )
{
int id;
if( hash_index_get( "data/accounts/webfinger", handle, &id ) ) {
return account_from_id(id);
}
if( default_server && !strchr( handle, '@' ) ) {
char buffer[512];
snprintf( buffer,sizeof(buffer), "%s@%s", handle, default_server );
return account_from_webfinger( buffer, default_server );
}
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 );
mkdir( format( b, 512, "data/accounts/%d/timeline/pinned", 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_inc( "data/accounts/HEAD" );
index_uri_to_account_id( uri, account_id );
}
create_account_skeleton(account_id);
struct account* a = NULL;
a = account_from_id(account_id);
if( a ) {
a->id = account_id;
a->account_url = strdup(uri);
account_save(a);
} else {
// Mark as stub
if( !a ) { a = account_new(); }
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);
}
// Fetch the ActivityPub actor data if we don't already have it
char filename[512];
snprintf( filename,sizeof(filename), "data/accounts/%d/ap.json", account_id );
pull_remote_file_if_older( filename, uri, 60*60*24*3 );
FILE* f = fopen( filename, "r" );
if( !f ) {
return NULL;
} else {
printf( "! AP data is present at %s\n", filename );
fclose(f);
}
account_free(a);
// Fail if we can't sync
if( !account_sync_from_activity_pub( account_id, false ) ) {
printf( "Failed to sync from activity pub data (account_id=%d)\n", 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->account_id);
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 );
for( int i = 0; i < a->emoji.count; ++i ) {
free( a->emoji.items[i].key );
free( a->emoji.items[i].value );
}
free( a->emoji.items );
free( a->pinned_posts.items );
free(a->note);
free(a);
}
void account_save( struct account* a )
{
char filename[512];
snprintf( filename, 512, "data/accounts/%d/data.json", a->id );
printf( "Saving to filename %s\n", filename );
json_write_object_layout_to_file( filename, "\t", account_layout, a );
snprintf( filename, 512, "data/accounts/%d.json", a->id );
unlink( filename );
// Index by uri
if( a->account_url ) {
char id_str[32];
snprintf( id_str,32, "%d", a->id );
ffdb_trie_set( "data/accounts/by-uri", a->account_url, id_str );
hash_index_remove( "data/accounts/uri_index/", a->account_url );
if( a->account_id ) {
ffdb_trie_set( "data/accounts/by-uri", a->account_id, id_str );
hash_index_remove( "data/accounts/uri_index/", a->account_id );
}
}
// Index by next_update
{
char timestamp[128];
rfc3339_time_string( a->next_update, timestamp, sizeof(timestamp) );
char key[512];
snprintf( key,512, "%s-%d", timestamp, a->id );
char id_str[32];
snprintf( id_str,32, "%d", a->id );
ffdb_trie_set( "data/accounts/updates/", key, id_str );
}
}
struct account* account_load_next_update()
{
int count = ffdb_trie_count( "data/accounts/updates/" );
if( count == 0 ) {
printf( "No accounts to update\n" );
return NULL;
}
char* key = NULL;
char* value = NULL;
count -= 1;
while( !ffdb_trie_get_index( "data/accounts/updates/", count, &key, &value ) ) {
if( !count ) {
return NULL;
}
printf( "Unable to load index entry #%d\n", count );
free(key); key = NULL;
free(value); value = NULL;
count -= 1;
}
struct account* result = account_from_id( atoi(value) );
if( !result ) {
printf( "Unable to load account with id=%s\n", value );
goto reindex;
}
// Check to make sure the index entry is valid
char timestamp[512];
rfc3339_time_string( result->next_update, timestamp, 512 );
char buffer[512];
snprintf( buffer,sizeof(buffer), "%s-%s", timestamp, value );
if( 0 != strcmp( buffer, key ) ) {
printf( "Mismatch between index and account data (%s != %s). Fixing\n", key, buffer );
goto reindex;
}
free(key);
free(value);
printf( "Next account to update: %s\n", result->account_url );
return result;
reindex:
// It isn't, fix the index and retry
ffdb_trie_set( "data/accounts/updates/", key, NULL );
account_free(result);
free(key);
free(value);
return NULL;
}
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( ( a->id == owner_account_id ) && ( 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 );
}
}
bool account_is_follower( struct account* a, struct account* possible_follower )
{
char filename[512];
char key[32];
char* value = ffdb_trie_get(
format( filename, sizeof(filename), "data/accounts/%d/followers", a->id ),
format( key, sizeof(key), "%d", possible_follower->id )
);
if( !value ) {
return false;
}
bool res = ( 0 == strcmp( "T", value ) );
free(value);
return res;
}
void account_remove_follower( struct account* a, struct account* follower )
{
if( !account_is_follower( a, follower ) ) {
return;
}
// 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* old_account, const char* new_uri )
{
struct account* new_account = account_from_uri_or_fetch(new_uri);
if( !new_account ) {
printf( "! Failed to move account %s (%d) to %s\n", old_account->account_url, old_account->id, new_uri );
return;
}
// Update Followers list
struct {
int* items;
int count;
} existing;
account_list_followers( old_account, 0, INT_MAX, &existing );
for( int i = 0; i < existing.count; ++i ) {
struct account* follower = account_from_id(existing.items[i]);
if( !follower ) { continue; }
account_add_follower( new_account, follower );
account_remove_follower( old_account, follower );
account_free(follower);
}
existing.count = 0;
// Update following list
account_list_following( old_account, 0, INT_MAX, &existing );
for( int i = 0; i < existing.count; ++i ) {
struct account* following = account_from_id(existing.items[i]);
account_add_follower( following, new_account );
account_remove_follower( following, old_account );
account_free(following);
}
free(existing.items);
old_account->replaced_by = new_account->id;
account_save(old_account);
struct notification* note = notification_new();
note->type = nt_move;
note->account_id = old_account->id;
note->ref_account_id = new_account->id;
notification_save(note);
notification_free(note);
}
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;
}
void account_pin_status( struct account* a, struct status* s )
{
char buffer[512];
snprintf( buffer,sizeof(buffer), "data/accounts/%d/timeline/pinned", a->id );
s->pinned = true;
struct timeline* pinned = timeline_from_path( buffer );
timeline_add_post( pinned, s );
timeline_free(pinned);
// TODO: federate an Add(Note) activity
struct ap_object* act = ap_object_new();
activity_allocate_local_id(act);
act->type = ap_Add;
act->object.tag = apaot_ref;
act->object.ref = strdup( s->url );
act->actor = strdup( a->account_url );
act->target = aformat( "https://%s/owner/collections/featured", g_server->domain );
char* str;
str = aformat("https://%s/owner/followers", g_server->domain );
array_append( &act->to, sizeof(str), &str );
ap_object_write_to_FILE( act, stdout );
activity_deliver( act );
ap_object_free(act);
}
void account_unpin_status( struct account* a, struct status* s )
{
char buffer[512];
snprintf( buffer,sizeof(buffer), "data/accounts/%d/timeline/pinned", a->id );
s->pinned = false;
struct timeline* pinned = timeline_from_path( buffer );
timeline_remove_post( pinned, s );
timeline_free(pinned);
// TODO: federate an Add(Note) activity
struct ap_object* act = ap_object_new();
activity_allocate_local_id(act);
act->type = ap_Remove;
act->object.tag = apaot_ref;
act->object.ref = strdup( s->url );
act->actor = strdup( a->account_url );
act->target = aformat( "https://%s/owner/collections/featured", g_server->domain );
char* str;
str = aformat("https://%s/owner/followers", g_server->domain );
array_append( &act->to, sizeof(str), &str );
ap_object_write_to_FILE( act, stdout );
activity_deliver( act );
ap_object_free(act);
}