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.
909 lines
22 KiB
C
909 lines
22 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 const char* get_account_path( unsigned int id, char* filename, int size )
|
|
{
|
|
struct stat s;
|
|
|
|
// Check old-style path
|
|
snprintf( filename, size, "data/accounts/%d", id );
|
|
if( 0 == stat( filename, &s ) ) {
|
|
//printf( "Using path %s for account %d\n", filename, id );
|
|
return filename;
|
|
}
|
|
|
|
// Check new-style path
|
|
int millions = id / 1000000;
|
|
int thousands = ( id % 1000000 ) / 1000;
|
|
int ones = id % 1000;
|
|
|
|
mkdir( format(filename,size, "data/accounts/%03dm", millions ), 0755 );
|
|
mkdir( format(filename,size, "data/accounts/%03dm/%03dk", millions, thousands ), 0755 );
|
|
|
|
snprintf( filename,size, "data/accounts/%03dm/%03dk/%03d", millions, thousands, ones );
|
|
//printf( "Checking %s\n", filename );
|
|
if( 0 == stat(filename,&s) ) {
|
|
//printf( "Using path %s for account %d\n", filename, id );
|
|
return filename;
|
|
}
|
|
|
|
printf( "No account path found for %d\n", id );
|
|
return NULL;
|
|
}
|
|
const char* account_get_path( unsigned int id, char* buffer, int size )
|
|
{
|
|
return get_account_path( id, buffer, size );
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Get account path
|
|
char path[512];
|
|
if( !get_account_path( id, path, sizeof(path) ) ) {
|
|
printf( "No account path for %d\n", id );
|
|
return NULL;
|
|
}
|
|
|
|
struct account* a = account_new();
|
|
a->id = id;
|
|
|
|
char filename[512];
|
|
snprintf( filename, 512, "%s/data.json", path );
|
|
if( !json_read_object_layout_from_file( filename, account_layout, a ) ) {
|
|
printf( "Could not load data file from %s\n", filename );
|
|
account_free(a);
|
|
a = account_new();
|
|
a->id = id;
|
|
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 );
|
|
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;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
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 ) {
|
|
if( 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 path[512];
|
|
get_account_path( a->id, path,sizeof(path) );
|
|
|
|
char filename[512];
|
|
FILE* f = fopen( format( filename, sizeof(filename), "%s/%s.pem", path, 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 filename[512];
|
|
char b[512];
|
|
|
|
//*
|
|
// Check new-style path
|
|
int millions = account_id / 1000000;
|
|
int thousands = ( account_id % 1000000 ) / 1000;
|
|
int ones = account_id % 1000;
|
|
|
|
mkdir( format(filename,512, "data/accounts/%03dm", millions ), 0755 );
|
|
mkdir( format(filename,512, "data/accounts/%03dm/%03dk", millions, thousands ), 0755 );
|
|
mkdir( format(filename,512, "data/accounts/%03dm/%03dk/%03d", millions, thousands, ones ), 0755 );
|
|
/*/
|
|
// Use old-style path
|
|
snprintf( filename,512, "data/accounts/%d", account_id );
|
|
//*/
|
|
|
|
// Make sure the account directory exists
|
|
mkdir( format( b, 512, "%s/timeline", filename), 0755 );
|
|
mkdir( format( b, 512, "%s/timeline/pinned", filename ), 0755 );
|
|
|
|
//fs_list_set( format( b, 512, format( b, 512, "%s//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 path[512];
|
|
get_account_path(account_id,path,512);
|
|
char filename[512];
|
|
snprintf( filename,sizeof(filename), "%s/ap.json", path );
|
|
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 path[512];
|
|
get_account_path( a->id, path, 512 );
|
|
create_account_skeleton(a->id);
|
|
|
|
char filename[512];
|
|
snprintf( filename, 512, "%s/data.json", path );
|
|
|
|
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 );
|
|
|
|
if( a->account_id ) {
|
|
ffdb_trie_set( "data/accounts/by-uri", a->account_id, id_str );
|
|
}
|
|
}
|
|
|
|
// 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 )
|
|
{
|
|
char path[512];
|
|
|
|
// Insert an entry for this follower (only does something if not already set)
|
|
char filename[512];
|
|
char key[32];
|
|
get_account_path(a->id,path,512);
|
|
ffdb_trie_set(
|
|
format( filename, sizeof(filename), "%s/followers", path ),
|
|
format(key,sizeof(key),"%d", follower->id),
|
|
"T"
|
|
);
|
|
a->followers_count = ffdb_trie_count(filename);
|
|
account_save(a);
|
|
|
|
get_account_path(follower->id,path,512);
|
|
ffdb_trie_set(
|
|
format( filename, sizeof(filename), "%s/following", path ),
|
|
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 path[512];
|
|
get_account_path(a->id,path,512);
|
|
char filename[512];
|
|
char key[32];
|
|
char* value = ffdb_trie_get(
|
|
format( filename, sizeof(filename), "%s/followers", path ),
|
|
format( key, sizeof(key), "%d", possible_follower->id )
|
|
);
|
|
if( !value ) {
|
|
return false;
|
|
}
|
|
bool res = ( 0 == strcmp( "T", value ) );
|
|
free(value);
|
|
return res;
|
|
}
|
|
struct timeline* account_get_timeline( struct account* a )
|
|
{
|
|
char path[512];
|
|
if( !get_account_path( a->id, path, sizeof(path) ) )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
char timeline_path[512];
|
|
snprintf( timeline_path,512, "%s/timeline", path );
|
|
printf( "Using timeline path %s\n", timeline_path );
|
|
|
|
struct timeline* tl = timeline_from_path(timeline_path);
|
|
if( !tl ) { printf( "! Failed to create timeline object!\n" ); }
|
|
return tl;
|
|
}
|
|
void account_remove_follower( struct account* a, struct account* follower )
|
|
{
|
|
if( !account_is_follower( a, follower ) ) {
|
|
return;
|
|
}
|
|
|
|
// Remove the follow
|
|
char path[512];
|
|
char filename[512];
|
|
char key[32];
|
|
get_account_path(a->id,path,512);
|
|
ffdb_trie_remove(
|
|
format( filename, sizeof(filename), "%s/followers", path ),
|
|
format( key, sizeof(key), "%d", follower->id )
|
|
);
|
|
a->followers_count = ffdb_trie_count(filename);
|
|
|
|
get_account_path(follower->id,path,512);
|
|
ffdb_trie_remove(
|
|
format( filename, sizeof(filename), "%s/following", path ),
|
|
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 path[512];
|
|
get_account_path(a->id,path,512);
|
|
|
|
char filename[512];
|
|
account_list( format( filename, sizeof(filename), "%s/followers", path ), offset, limit, id_array );
|
|
}
|
|
void account_list_following( struct account* a, int offset, int limit, void* id_array )
|
|
{
|
|
char path[512];
|
|
get_account_path(a->id,path,512);
|
|
|
|
char filename[512];
|
|
account_list( format( filename, sizeof(filename), "%s/following", path ), 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];
|
|
|
|
char path[512];
|
|
account_get_path( a->id, path, 512 );
|
|
|
|
// Make sure the account to unfollow has previously been followed
|
|
char* value = ffdb_trie_get(
|
|
format(index,512,"%s/following", path),
|
|
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 path[512];
|
|
account_get_path( a->id, path, 512 );
|
|
|
|
char buffer[512];
|
|
snprintf( buffer,sizeof(buffer), "%s/timeline/pinned", path );
|
|
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 path[512];
|
|
account_get_path( a->id, path, 512 );
|
|
|
|
char buffer[512];
|
|
snprintf( buffer,sizeof(buffer), "%s/timeline/pinned", path );
|
|
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);
|
|
}
|
|
|