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.

495 lines
12 KiB
C

#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <sys/stat.h>
#include <time.h>
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;
}