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.

1418 lines
33 KiB
C

#define _GNU_SOURCE
#include "status.h"
#include "status/react.h"
// Model
#include "model/server.h"
#include "model/account.h"
#include "model/activity.h"
#include "model/notification.h"
#include "model/timeline.h"
#include "model/emoji.h"
#include "model/media.h"
#include "model/fetch.h"
#include "model/status/poll.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 "sha256/sha256.h"
#include "collections/array.h"
#include "util/format.h"
#include "ap/object.h"
// Standard Library
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
#define STATUSES_BY_URI "data/statuses/by-uri"
extern struct json_enum visibility_enum[];
#define OBJ_TYPE struct status
static struct json_object_field status_layout[] = {
JSON_FIELD_INTEGER( account_id, true ),
JSON_FIELD_INTEGER( activity_id, false ),
JSON_FIELD_STRING( url, false ),
JSON_FIELD_BOOL( stub, false ),
JSON_FIELD_BOOL( remote, false ),
JSON_FIELD_STRING( content, false ),
JSON_FIELD_STRING( source, false ),
JSON_FIELD_STRING( spoiler_text, false ),
JSON_FIELD_BOOL( pinned, false ),
JSON_FIELD_BOOL( bookmarked, false ),
JSON_FIELD_ENUM( visibility, visibility_enum, false ),
JSON_FIELD_DATETIME( published, false ),
{
.key = "in_reply_to",
.offset = offsetof( OBJ_TYPE, in_reply_to.id ),
.required = false,
.type = &json_field_integer,
},
{
.key = "in_reply_to_url",
.offset = offsetof( OBJ_TYPE, in_reply_to.url ),
.required = false,
.type = &json_field_string,
},
{
.key = "quote_id",
.offset = offsetof( OBJ_TYPE, quote.id ),
.required = false,
.type = &json_field_integer,
},
{
.key = "quote_url",
.offset = offsetof( OBJ_TYPE, quote.url ),
.required = false,
.type = &json_field_string,
},
{
.key = "repost_id",
.offset = offsetof( OBJ_TYPE, repost.id ),
.required = false,
.type = &json_field_integer,
},
{
.key = "repost_url",
.offset = offsetof( OBJ_TYPE, repost.url ),
.required = false,
.type = &json_field_string,
},
{
.key = "root_status_id",
.offset = offsetof( OBJ_TYPE, root_status.id ),
.required = false,
.type = &json_field_integer,
},
{
.key = "root_status_url",
.offset = offsetof( OBJ_TYPE, root_status.url ),
.required = false,
.type = &json_field_string,
},
JSON_FIELD_INTEGER( reposted_status_id, false ),
JSON_FIELD_ARRAY_OF_STRINGS( media, false ),
JSON_FIELD_ARRAY_OF_TYPE( media2, false, media_type ),
JSON_FIELD_ARRAY_OF_TYPE( reacts, false, status_react_type ),
JSON_FIELD_ARRAY_OF_INTS( likes, false ),
JSON_FIELD_ARRAY_OF_INTS( replies, false ),
JSON_FIELD_ARRAY_OF_INTS( reposts, false ),
JSON_FIELD_ARRAY_OF_INTS( quotes, false ),
JSON_FIELD_ARRAY_OF_INTS( mentions, false ),
JSON_FIELD_ARRAY_OF_TYPE( emoji, false, emoji_type ),
JSON_FIELD_ARRAY_OF_STRINGS( tags, false ),
{
.key = "poll",
.offset = offsetof( OBJ_TYPE, poll ),
.type = &status_poll_type,
},
JSON_FIELD_BOOL( sensitive, true ),
JSON_FIELD_END
};
#undef OBJ_TYPE
void* allocate( size_t s )
{
void* ptr = malloc(s);
memset( ptr, 0, s );
return ptr;
}
void status_model_init()
{
mkdir( "data", 0755 );
mkdir( "data/statuses", 0755 );
mkdir( "data/statuses/ap", 0755 );
mkdir( "data/statuses/tags", 0755 );
}
static const char* get_status_data_filename( unsigned int id, char* filename, int size )
{
int millions = id / 1000000;
int thousands = ( id % 1000000 ) / 1000;
int ones = id % 1000;
mkdir( format(filename,size, "data/statuses/%03dm", millions ), 0755 );
mkdir( format(filename,size, "data/statuses/%03dm/%03dk", millions, thousands ), 0755 );
return format(filename,size, "data/statuses/%03dm/%03dk/%03d.json", millions, thousands, ones );
}
static FILE* open_status_data_file( unsigned int id, const char* mode )
{
char filename[512];
FILE* f = NULL;
f = fopen( get_status_data_filename(id,filename,sizeof(filename)), mode );
if( f ) {
//printf( "Opening %s\n", filename );
//printf( "Using new location at %s\n", filename );
return f;
}
f = fopen( format(filename,sizeof(filename), "data/statuses/%d.json", id), mode );
if( f ) {
//printf( "Opening %s\n", filename );
//printf( "Using existing location at %s\n", filename );
return f;
}
return NULL;
}
struct status* status_from_id( unsigned int id )
{
if( id == 0 ) { return NULL; }
struct status* s = NULL;
bool needs_save = false;
FILE* f = open_status_data_file( id, "r" );
if( !f ) { return NULL; }
s = allocate(sizeof(struct status));
s->id = id;
if( !json_read_object_layout_from_FILE( f, status_layout, s ) ) {
//printf( "Failed to load status %d\n", id );
status_free(s);
return NULL;
}
// Force the poll id to be the same as the status id
if( s->poll ) {
s->poll->id = s->id;
}
// Convert media to media2
if( s->media.count != 0 ) {
for( int i = 0; i < s->media.count; ++i ) {
struct media* local = media_from_local_uri( s->media.items[i] );
if( local ) {
array_append( &s->media2, sizeof(local), &local );
continue;
}
struct media* remote = media_new();
remote->preview_url = strdup(s->media.items[i]);
remote->remote_url = strdup(s->media.items[i]);
remote->content_type = strdup("image/*");
array_append( &s->media2, sizeof(remote), &remote );
free(s->media.items[i] );
}
free( s->media.items );
memset( &s->media, 0, sizeof(s->media) );
needs_save = true;
}
// Fix source field
if( !s->source ) {
s->source = strdup("");
}
// Fix remote field
if( s->account_id == owner_account_id ) {
s->remote =false;
}
// Fix url field
if( !s->remote && !s->url ) {
s->url = aformat( "https://%s/note/%d", g_server->domain, s->id );
}
// Fill in reference urls
if( s->in_reply_to.id && !s->in_reply_to.url ) {
struct status* in_reply_to = status_from_id( s->in_reply_to.id );
if( in_reply_to && in_reply_to->url ) {
s->in_reply_to.url = strdup(in_reply_to->url);
needs_save = true;
}
status_free(in_reply_to);
}
if( s->quote.id && !s->quote.url ) {
struct status* quote = status_from_id( s->quote.id );
if( quote && quote->url ) {
s->quote.url = strdup(quote->url);
needs_save = true;
}
status_free(quote);
}
if( s->root_status.id && ( s->root_status.id != s->id ) && !s->root_status.url ) {
struct status* root_status = status_from_id( s->root_status.id );
if( root_status && root_status->url ) {
s->root_status.url = strdup(root_status->url);
needs_save = true;
}
status_free(root_status);
}
// Save changes
if( needs_save ) {
status_save(s);
}
return s;
}
struct status* status_new_repost( struct status* s, struct account* a )
{
struct status* repost;
repost = malloc(sizeof(*repost));
memset(repost,0,sizeof(*repost));
repost->account_id = a->id;
repost->repost.id = s->id;
repost->repost.url = strdup(s->url);
repost->published = time(NULL);
status_add_repost( s, repost );
status_save_new(repost);
return repost;
}
static struct status* status_from_local_uri( const char* uri )
{
if( 0 != strncmp( "https://", uri, 8 ) ) { return NULL; }
uri += 8;
int server_name_length = strlen(g_server->domain);
if( 0 != strncmp( g_server->domain, uri, server_name_length ) ) { return NULL; }
uri += server_name_length;
if( 0 != strncmp( "/note/", uri, 6 ) ) { return NULL; }
uri += 6;
// Note: zero is never a valid status id
int id = atoi(uri);
if( id == 0 ) { return NULL; }
return status_from_id(id);
}
struct status* status_from_uri( const char* uri )
{
// Check for local
struct status* s = status_from_local_uri( uri );
if( s ) { return s; }
char* id_str = ffdb_trie_get( "data/statuses/by-uri", uri );
if( id_str ) {
struct status* s = status_from_id(atoi(id_str));
if( s ) {
return s;
} else {
// Status has been deleted, remove the index entry
ffdb_trie_remove( "data/statuses/by-uri", uri );
}
free(id_str);
}
return NULL;
}
void status_add_reply( struct status* s, struct status* child )
{
if( child->id == 0 ) { return; }
for( int i = 0; i < s->replies.count; ++i ) {
if( s->replies.items[i] == child->id ) {
return;
}
}
int id = child->id;
array_append( &s->replies, sizeof(id), &id );
}
void status_add_mention( struct status* s, int id )
{
for( int i = 0; i < s->mentions.count; ++i ) {
if( s->mentions.items[i] == id ) {
// Already mentioned, don't add a second time
return;
}
}
array_append( &s->mentions, sizeof(id), &id );
}
void status_add_tag( struct status* s, const char* tag )
{
if( !tag ) { return; }
for( int i = 0; i < s->tags.count; ++i ) {
if( 0 == strcasecmp( s->tags.items[i], tag ) ) {
// Tag already present, don't add a second time
return;
}
}
char* value = strdup(tag);
array_append( &s->tags, sizeof(value), &value );
}
void status_add_repost( struct status* s, struct status* repost )
{
if( repost->id == 0 ) { return; }
for( int i = 0; i < s->reposts.count; ++i ) {
if( s->reposts.items[i] == repost->id ) {
return;
}
}
int id = repost->id;
array_append( &s->reposts, sizeof(id), &id );
status_save(s);
}
void status_add_quote( struct status* s, struct status* quote )
{
if( quote->id == 0 ) { return; }
for( int i = 0; i < s->quotes.count; ++i ) {
if( s->quotes.items[i] == quote->id ) {
return;
}
}
int id = quote->id;
array_append( &s->quotes, sizeof(id), &id );
status_save(s);
}
static void sync_poll( struct status* s, void* poll_data_ptr )
{
struct {
struct ap_object_ptr_or_ref* items;
int count;
} *poll_data = poll_data_ptr;
// Pull in options
for( int i = 0; i < poll_data->count; ++i ) {
if( poll_data->items[i].tag == apaot_ref ) {
printf( "TODO: automatically dereference poll option data at %s\n", poll_data->items[i].ref );
} else if( poll_data->items[i].tag != apaot_object ) {
continue;
}
printf( "Processing option[%d]\n", i );
struct ap_object* option = poll_data->items[i].ptr;
// Expand options if there aren't enough already
struct status_poll_option* o;
if( s->poll->options.count <= i ) {
o = malloc(sizeof(*o));
memset(o,0,sizeof(*o));
array_append( &s->poll->options, sizeof(o), &o );
}
o = s->poll->options.items[i];
// Update option title
if( option->name ) {
if( o->title ) {
free( o->title );
}
o->title = strdup(option->name);
printf( "Option[%d] is %s\n", i, o->title );
}
// Pull out vote counts
if( option->replies.tag == apaot_object ) {
printf( "Syncing reply count..\n" );
struct ap_object* replies = option->replies.ptr;
while( s->poll->options.items[i]->votes.count < replies->total_items ) {
int id = poll_vote_unknown_id;
array_append( &o->votes, sizeof(id), &id );
}
}
}
// Rebuild votes_count
status_poll_update_vote_count( s->poll );
}
// TODO: Move to src/model/status/ap_sync.c
bool status_sync_from_activity_pub( struct status* s, struct ap_object* act )
{
if( !act->actor && act->attributed_to ) {
act->actor = strdup( act->attributed_to );
}
//printf( "Syncing status from activity %s\n", act->id );
ap_object_write_to_FILE( act, stdout );
bool result = false;
struct account* a = account_from_uri_or_fetch(act->actor);
if( !a ) {
printf( "! Unable to get account for %s\n", act->actor );
goto failed;
}
// Update account if it should be
if( time(NULL) >= a->next_update ) {
printf( "* Updating account %s\n", a->account_url );
account_sync_from_activity_pub( a->id, true );
}
// Handle post visibility
bool is_public = false;
for( int i = 0; i < act->to.count; ++i ) {
if( 0 == strcmp( act->to.items[i], "https://www.w3.org/ns/activitystreams#Public" ) ) {
is_public = true;
}
}
for( int i = 0; i < act->cc.count; ++i ) {
if( 0 == strcmp( act->cc.items[i], "https://www.w3.org/ns/activitystreams#Public" ) ) {
is_public = true;
}
}
if( is_public ) {
s->visibility = status_visibility_public;
} else {
s->visibility = status_visibility_direct;
}
// Setup initial settings
s->account_id = a->id;
s->published = act->published;
s->remote = true;
s->stub = false;
s->sensitive = act->sensitive;
if( act->summary ) {
s->spoiler_text = strdup(act->summary);
}
// Copy over post URL
free(s->url);
s->url = strdup( act->id );
// Populate source string
free(s->source);
s->source = safe_strdup(act->source.content);
// Populate content
free(s->content);
s->content = safe_strdup( act->content.content );
//// Handle Media
// Erase existing media
for( int i = 0; i < s->media.count; ++i ) {
free( s->media.items[i] );
}
free(s->media.items);
memset(&s->media,0,sizeof(s->media));
for( int i = 0; i < s->media2.count; ++i ) {
media_free( s->media2.items[i] );
}
free(s->media2.items);
memset(&s->media2,0,sizeof(s->media2));
// Recreate the media field
for( int i = 0; i < act->attachments.count; ++i ) {
struct ap_object* att = act->attachments.items[i];
if( att && att->url.count == 1 ) {
struct media* media = media_new();
media->remote_url = strdup( att->url.items[0].ref );
if( att->media_type ) {
media->content_type = strdup(att->media_type);
}
array_append( &s->media2, sizeof(char*), &media );
}
}
// Handle Polls
// TODO: move this out to another function to use same code for any_of and one_of
if( act->one_of.count > 0 ) {
if( !s->poll ) {
struct status_poll* poll;
poll = malloc(sizeof(*poll));
memset(poll,0,sizeof(*poll));
poll->id = s->id;
poll->multiple_choice = false;
s->poll = poll;
}
s->poll->expires_at = act->closed;
sync_poll( s, &act->one_of );
} else if( act->any_of.count > 0 ) {
if( !s->poll ) {
struct status_poll* poll;
poll = malloc(sizeof(*poll));
memset(poll,0,sizeof(*poll));
poll->id = s->id;
poll->multiple_choice = true;
s->poll = poll;
}
s->poll->expires_at = act->closed;
sync_poll( s, &act->any_of );
}
// Translate emoji
for( int i = 0; i < s->emoji.count; ++i ) {
emoji_free( s->emoji.items[i] );
}
s->emoji.count = 0;
s->mentions.count = 0;
for( int i = 0; i < act->tags.count; ++i ) {
struct ap_activity_tag* tag = act->tags.items[i];
if( tag->type == aptag_mention ) {
struct account* a = account_from_uri_or_fetch( tag->href );
if( a ) {
int item = a->id;
array_append( &s->mentions, sizeof(item), &item );
account_free(a);
}
} else if( tag->type == aptag_emoji ) {
struct emoji* e;
e = malloc(sizeof(*e));
memset(e,0,sizeof(*e));
e->url = safe_strdup( tag->icon.url );
e->shortcode = safe_strdup( &tag->name[1] );
e->shortcode[ strlen(e->shortcode) - 1 ] = '\0';
array_append( &s->emoji, sizeof(e), &e );
} else if( tag->type == aptag_hashtag && tag->name ) {
char* tag_name = safe_strdup( &tag->name[1] );
status_add_tag( s, tag_name );
}
}
// Force this status to have an id
status_save_new(s);
status_add_to_timeline( s, a->id );
// Handle InReplyTo and QuoteURL after saving to timeline, which could take a while
if( act->in_reply_to ) {
printf( "Status %d is reply to %s\n", s->id, act->in_reply_to );
int parent_id = 0;
struct status* parent = status_from_uri_or_fetch( act->in_reply_to );
//struct status* parent = status_from_uri_or_stub( act->in_reply_to );
if( parent ) {
parent_id = parent->id;
status_save(parent);
status_free(parent);
}
if( parent_id ) {
status_make_reply_to( s, parent_id );
s->in_reply_to.id = parent_id;
s->in_reply_to.url = strdup(act->in_reply_to);
}
//printf( "Status %d has been marked as a reply to %d (%s)\n", s->id, parent_id, act->in_reply_to );
}
if( act->quote_url ) {
int parent_id = 0;
struct status* parent = status_from_uri_or_fetch( act->quote_url );
if( parent ) {
parent_id = parent->id;
status_save(parent);
status_free(parent);
}
if( parent_id ) {
status_make_quote_of( s, parent_id );
}
}
status_save(s);
result = true;
cleanup:
account_free(a);
return result;
failed:
result = false;
goto cleanup;
};
bool status_sync_from_uri( struct status* s, const char* uri )
{
bool result = false;
struct ap_object* act = fetch_ap_object_ref( uri );
if( !act ) {
printf( "Failed to get activity from %s\n", uri );
goto failed;
}
if( !status_sync_from_activity_pub(s,act) ) { goto failed; }
result = true;
cleanup:
ap_object_free(act);
return result;
failed:
if( s ) {
if( !s->stub ) {
printf( "Creating stub status for later sync\n" );
}
char filename[512];
ffdb_trie_set( "data/statuses/stubs", format(filename,512,"%d",s->id), "T" );
goto cleanup;
}
status_free(s);
result = false;
goto cleanup;
}
bool status_sync( struct status* s )
{
return status_sync_from_uri( s, s->url );
}
struct ap_object* status_replies_Collection( struct status* s )
{
struct ap_object* replies = activity_new_local_activity();
replies->type = ap_Collection;
replies->id = aformat( "https://%s/note/%d/replies", g_server->domain, s->id );
replies->total_items = s->replies.count;
for( int i = 0; i < s->replies.count; ++i ) {
struct status* reply = status_from_id( s->replies.items[i] );
struct ap_object_ptr_or_ref item;
item.tag = apaot_ref;
item.ref = strdup(reply->url);
array_append( &replies->collection_items, sizeof(item), &item );
status_free(reply);
}
return replies;
}
struct status* status_from_uri_or_fetch( const char* uri )
{
struct status* s = status_from_uri(uri);
if( s ) { return s; }
return status_fetch_from_uri( uri );
}
struct status* status_from_uri_or_stub( const char* uri )
{
struct status* s = status_from_uri(uri);
if( !s ) {
printf( "Creating stub for %s\n", uri );
s = malloc(sizeof(*s));
memset(s,0,sizeof(*s));
s->remote = true;
s->account_id = -1;
s->stub = true;
s->published = time(NULL);
free(s->url);
s->url = strdup(uri);
status_save_new(s);
char id_str[32];
snprintf( id_str,32, "%d", s->id );
ffdb_trie_set( "data/statuses/by-uri", s->url, id_str );
}
return s;
}
struct status* status_fetch_from_uri( const char* uri )
{
struct status* s = status_from_uri_or_stub( uri );
if( !status_sync_from_uri(s,uri) ) {
status_free(s);
return NULL;
}
status_save_new(s);
return s;
}
static int system_status_id = 1;
struct status* status_new_system_unfollow( int account_id )
{
struct account* a = account_from_id(account_id);
if( !a ) { return NULL; }
struct status* s;
s = malloc(sizeof(*s));
memset(s,0,sizeof(*s));
system_status_id += 1;
s->id = -system_status_id;
s->published = time(NULL);
s->account_id = -1;
asprintf( &s->content, "%s unfollowed you\n", a->display_name );
account_free(a);
return s;
}
struct status* status_new_system_block( int account_id )
{
struct account* a = account_from_id(account_id);
if( !a ) { return NULL; }
struct status* s;
s = malloc(sizeof(*s));
memset(s,0,sizeof(*s));
system_status_id += 1;
s->id = -system_status_id;
s->published = time(NULL);
s->account_id = -1;
asprintf( &s->content, "%s@%s blocked you. View their account <a href='%s'>here</a>.", a->handle, a->server, a->account_url );
account_free(a);
return s;
}
struct status* status_new_system_stub( struct status* stub )
{
struct status* s;
s = malloc(sizeof(*s));
memset(s,0,sizeof(*s));
s->id = stub->id; //-stub->id - 50;
s->account_id = -1;
s->published = time(NULL);
asprintf( &s->content, "Unable to load status #%d. View on original server at <a href='%s'>%s</a>",
stub->id,
stub->url, stub->url
);
return s;
}
struct status* status_from_activity( struct ap_object* act )
{
struct status* s;
s = malloc(sizeof(*s));
memset(s,0,sizeof(*s));
if( !status_sync_from_activity_pub(s,act) ) {
status_free(s);
return NULL;
}
return s;
}
void status_assign_local_id( struct status* s )
{
if( s->id ) { return; }
mkdir_p( "data/statuses", 0755 );
int head = fs_list_inc( "data/statuses/HEAD" );
s->id = head;
s->root_status.id = s->id;
if( s->url ) {
s->root_status.url = strdup(s->url);
}
}
bool status_save_new( struct status* s )
{
status_assign_local_id( s );
//printf( "Assigning status id %d\n", s->id );
status_save( s );
return true;
}
void status_save( struct status* s )
{
char filename[512];
json_write_object_layout_to_file( get_status_data_filename(s->id,filename,sizeof(filename)), "\t", status_layout, s );
// Index the status
if( s->url ) {
char id_str[32];
snprintf( id_str,32, "%d", s->id );
ffdb_trie_set( "data/statuses/by-uri", s->url, id_str );
//hash_index_remove( "data/statuses/uri", s->url );
}
if( s->stub ) {
mkdir_p( "data/statuses/stubs", 0755 );
ffdb_trie_set( "data/statuses/stubs", format(filename,512,"%d",s->id), "T" );
} else {
ffdb_trie_remove( "data/statuses/stubs", format(filename,512,"%d",s->id) );
}
// Set the tags
for( int i = 0; i < s->tags.count; ++i ) {
char tag_file[512];
ffdb_trie_set( "data/statuses/tags", s->tags.items[i], "T" );
struct timeline* tl = timeline_from_path( format(tag_file,512,"data/statuses/tags/%s", s->tags.items[i] ) );
timeline_add_post( tl, s );
timeline_free(tl);
}
}
void status_clean_tags()
{
}
void status_write_to_FILE( struct status* s, FILE* f )
{
json_write_object_layout_to_FILE( f, "\t", status_layout, s );
}
void status_free( struct status* s )
{
if( !s ) { return; }
free(s->url);
free(s->content);
free(s->source);
free(s->spoiler_text);
// Free media
for( int i = 0; i < s->media.count; ++i ) {
free(s->media.items[i]);
}
free(s->media.items);
for( int i = 0; i < s->media2.count; ++i ) {
media_free(s->media2.items[i] );
}
free(s->media2.items);
// Free reactions
for( int i = 0; i < s->reacts.count; ++i ) {
status_react_free(s->reacts.items[i]);
}
free(s->reacts.items);
free(s->in_reply_to.url );
free(s->quote.url);
free(s->repost.url);
free(s->root_status.url);
free(s->likes.items);
free(s->replies.items);
free(s->reposts.items);
free(s->quotes.items);
free(s->mentions.items);
for( int i = 0; i < s->emoji.count; ++i ) {
emoji_free( s->emoji.items[i] );
}
free(s->emoji.items);
// Free tags
for( int i = 0; i < s->tags.count; ++i ) {
free(s->tags.items[i]);
}
free(s->tags.items);
status_poll_free( s->poll );
free(s);
}
void status_delete( struct status* s )
{
int ids[] = {
s->account_id,
home_timeline_id,
public_timeline_id
};
for( int i = 0; i < sizeof(ids)/sizeof(ids[0]); ++i ) {
struct account* a = account_from_id( ids[i] );
if( !a ) {
printf( "No account for id %d\n", ids[i] );
continue;
}
struct timeline* tl = account_get_timeline(a);
if( tl ) {
timeline_remove_post( tl, s );
timeline_free(tl);
} else {
printf( "No timeline for account %d\n", ids[i] );
}
account_free(a);
}
char filename[512];
ffdb_trie_remove( "data/statuses/stubs", format(filename,512,"%d",s->id) );
ffdb_trie_remove( "data/statuses/by-uri", s->url );
get_status_data_filename(s->id,filename,sizeof(filename));
remove(filename);
status_free(s);
}
void status_add_to_timeline( struct status* s, int account_id )
{
//printf( "status_add_to_timeline( s, account_id=%d )\n", account_id );
struct account* a = account_from_id( account_id );
//printf( "Adding status %d to timeline for %s (%d/%d)\n", s->id, a->account_id, account_id, a->id );
struct timeline* tl = account_get_timeline(a);
if( tl ) {
timeline_add_post( tl, s );
}
timeline_free(tl);
account_free(a);
}
void status_make_reply_to( struct status* s, int in_reply_to_id )
{
// Add this status to the other
struct status* in_reply_to = status_from_id( in_reply_to_id );
if( !in_reply_to ) {
s->in_reply_to.id = 0;
return;
}
// Setup this status's reply fields
s->in_reply_to.id = in_reply_to_id;
s->root_status.id = in_reply_to->root_status.id;
if( in_reply_to->root_status.url ) {
s->root_status.url = strdup( in_reply_to->root_status.url );
}
// Record in parent as reply
status_add_reply( in_reply_to, s );
// Mention the account that made the post being replied to
status_add_mention( s, in_reply_to->account_id );
// Mention everyone else in that post
for( int i = 0; i < in_reply_to->mentions.count; ++i ) {
status_add_mention( s, in_reply_to->mentions.items[i] );
}
// Cleanup
status_save(in_reply_to);
status_free(in_reply_to);
}
void status_make_quote_of( struct status* s, int id_quote_of )
{
struct status* quoted_post = status_from_id( id_quote_of );
if( !quoted_post ) {
s->quote.id = 0;
return;
}
// Set quoted id
s->quote.id = id_quote_of;
s->quote.url = strdup(quoted_post->url);
// Record quote in parent
status_add_quote( quoted_post, s );
// Mention the account that made the post being replied to
status_add_mention( s, quoted_post->account_id );
// Mention everyone else in that post
for( int i = 0; i < quoted_post->mentions.count; ++i ) {
status_add_mention( s, quoted_post->mentions.items[i] );
}
// Save and cleanup
status_save( quoted_post );
status_free( quoted_post );
}
void status_get_context( struct status* s, void* ancestors_ptr, void* replies_ptr )
{
struct array_of_status {
struct status** items;
int count;
};
struct array_of_status* ancestors = ancestors_ptr;
struct array_of_status* replies = replies_ptr;
memset(ancestors,0,sizeof(*ancestors));
memset(replies,0,sizeof(*replies));
struct status* parent = NULL;
for( int i = s->in_reply_to.id; i != 0; i = parent->in_reply_to.id ) {
parent = status_from_id( i );
if( !parent ) { break; }
array_append( ancestors, sizeof(parent), &parent );
};
// reverse ancestors
for( int i = 0; i < ancestors->count / 2; ++i ) {
struct status* a = ancestors->items[i];
struct status* b = ancestors->items[ ancestors->count - i - 1 ];
ancestors->items[i] = b;
ancestors->items[ ancestors->count - i - 1 ] = a;
}
for( int i = 0; i < s->replies.count; ++i ) {
struct status* reply = status_from_id( s->replies.items[i] );
array_append( replies, sizeof(reply), &reply );
}
}
void status_get_quotes( struct status* s, void* quotes_ptr )
{
struct array_of_status {
struct status** items;
int count;
};
struct array_of_status* quotes = quotes_ptr;
memset( quotes, 0, sizeof(*quotes) );
for( int i = 0; i < s->quotes.count; ++i ) {
struct status* q = status_from_id( s->quotes.items[i] );
if( q ) {
array_append( quotes, sizeof(q), &q );
}
}
}
void status_add_react( struct status* s, const char* react, struct account* a )
{
if( s->repost.id ) {
struct status* reposted_status = status_from_id( s->repost.id );
status_add_react(reposted_status,react,a);
status_free(reposted_status);
return;
}
// generate outbox element
if( a->id == owner_account_id ) {
if( s->account_id == owner_account_id ) {
// Do nothing for reacting to our own post
return;
} else {
// Deliver react to post owner
activity_react( a, s, react );
}
}
if( !s->remote && a->id != owner_account_id ) { //&& s->account_id == owner_account_id && a->id != owner_account_id ) {
if( s->id != 0 ) {
// Create notification for liking the owner's post
struct notification* note = notification_new();
note->debug = 1;
note->type = nt_react;
note->status_id = s->id;
note->account_id = a->id;
note->react = strdup(react);
note->created_at = time(NULL);
notification_save( note );
notification_free( note );
}
}
struct status_react* re = NULL;
for( int i = 0; i < s->reacts.count; ++i ) {
if( 0 == strcmp( s->reacts.items[i]->code, react ) ) {
re = s->reacts.items[i];
goto update_entry;
}
}
re = malloc(sizeof(*re));
memset(re,0,sizeof(*re));
re->code = strdup(react);
array_append( &s->reacts, sizeof(re), &re );
update_entry:
for( int i = 0; i < re->accounts.count; ++i ) {
if( re->accounts.items[i] == a->id ) {
// Already present
goto done;
}
}
array_append( &re->accounts, sizeof(a->id), &a->id );
done:
status_save(s);
}
void status_remove_react( struct status* s, const char* react, struct account* a )
{
if( s->repost.id ) {
struct status* reposted_status = status_from_id( s->repost.id );
status_remove_react(reposted_status,react,a);
status_free(reposted_status);
return;
}
// TODO: generate outbox element
printf( "TODO: generate outbox activity for removing reaction '%s' to status #%d by account #%d\n", react, s->id, a->id );
/// Find react activity we need to Undo
/// Generate Undo Activity
// TODO: generate notification
/*
if( s->account_id == owner_account_id && a->id != owner_account_id ) {
// Create notification for liking the owner's post
struct notification* note = notification_new();
note->debug = 2;
note->type = nt_react;
note->account_id = a->id;
note->react = strdup(react);
note->created_at = time(NULL);
notification_save( note );
notification_free( note );
}*/
struct status_react* re = NULL;
for( int i = 0; i < s->reacts.count; ++i ) {
if( 0 == strcmp( s->reacts.items[i]->code, react ) ) {
re = s->reacts.items[i];
goto update_entry;
}
}
return;
update_entry:
array_delete( &re->accounts, sizeof(a->id), &a->id );
if( re->accounts.count == 0 ) {
array_delete( &s->reacts, sizeof(re), &re );
status_react_free(re);
}
status_save(s);
}
void status_add_like( struct status* s, struct account* a )
{
if( s->repost.id ) {
struct status* reposted_status = status_from_id( s->repost.id );
status_add_like(reposted_status,a);
status_free(reposted_status);
return;
}
for( int i = 0; i < s->likes.count; ++i ) {
if( s->likes.items[i] == a->id ) {
return;
}
}
int id = a->id;
array_append( &s->likes, sizeof(id), &id );
if( !s->stub ) {
if( s->account_id == owner_account_id ) {
if( a->id != owner_account_id ) {
// Create notification for liking the owner's post
struct notification* note = notification_new();
note->debug = 3;
note->type = nt_like;
note->account_id = a->id;
note->status_id = s->id;
note->created_at = time(NULL);
notification_save( note );
notification_free( note );
}
}
if( a->id == owner_account_id ) {
activity_like( s );
}
}
status_save(s);
}
void status_remove_like( struct status* s, struct account* a )
{
if( s->repost.id ) {
struct status* reposted_status = status_from_id( s->repost.id );
status_remove_like(reposted_status,a);
status_free(reposted_status);
return;
}
for( int i = 0; i < s->likes.count; ++i ) {
if( s->likes.items[i] == a->id ) {
// Swap with last element
s->likes.items[i] = s->likes.items[ s->likes.count-1 ];
// Then discard the last element
s->likes.count -= 1;
break;
}
}
if( !s->stub ) {
if( a->id == owner_account_id ) {
// TODO: undo activity
// activity_like( s );
}
}
status_save(s);
}
void status_set_bookmark( struct status* s )
{
s->bookmarked = true;
mkdir_p( "data/bookmarks", 0755 );
char key[32];
ffdb_trie_set( "data/owner/bookmarks", format(key,32,"Z-%012d",s->id), "T" );
ffdb_trie_set( "data/owner/bookmarks", format(key,32,"%d",s->id), NULL ); // Clear older format
status_save(s);
}
void status_clear_bookmark( struct status* s )
{
s->bookmarked = false;
char key[32];
ffdb_trie_set( "data/owner/bookmarks", format(key,32,"%d",s->id), NULL ); // Clear older format
ffdb_trie_set( "data/owner/bookmarks", format(key,32,"Z-%012d",s->id), NULL );
status_save(s);
}
void status_get_bookmarks( int offset, int limit, void* results_ptr )
{
struct {
char** items;
int count;
} keys;
memset( &keys, 0, sizeof(keys) );
struct {
struct status** items;
int count;
} *results = results_ptr;
results->count = 0;
const char* index = "data/owner/bookmarks";
enum {
migrate_count = 10,
};
int count;
int clean_limit = count = ffdb_trie_count( index );
if( clean_limit > migrate_count ) {
clean_limit = migrate_count;
}
ffdb_trie_list( index, count - clean_limit, clean_limit, &keys, NULL );
for( int i = 0; i < keys.count; ++i ) {
int id;
if( 1 == sscanf( keys.items[i], "%d", &id ) ) {
struct status* s = status_from_id(id);
if( s ) {
status_set_bookmark(s);
status_free(s);
}
}
free(keys.items[i]);
}
keys.count = 0;
ffdb_trie_list( index, offset, limit, &keys, NULL );
for( int i = 0; i < keys.count; ++i ) {
int id;
bool reset = false;
if( 1 == sscanf( keys.items[i], "Z-%012d", &id ) ) {
} else if( 1 == sscanf( keys.items[i], "%d", &id ) ) {
reset = true;
} else {
goto next_iteration;
}
struct status* s = status_from_id(id);
if( s ) {
array_append( results_ptr, sizeof(s), &s );
if( reset ) {
status_set_bookmark(s);
}
}
next_iteration:
free( keys.items[i] );
}
free( keys.items );
}
void status_gc()
{
int count = ffdb_trie_count( STATUSES_BY_URI );
enum {
per_pass = 20,
};
struct {
char** items;
int count;
} keys;
printf( "Checking and repairing uri-to-status index...\n" );
int pages = ( count + per_pass - 1 ) / per_pass;
for( int i = 0; i < pages; ++i ) {
printf( "page %d of %d\n", i, pages );
memset( &keys, 0, sizeof(keys) );
ffdb_trie_list( STATUSES_BY_URI, i * per_pass, per_pass, &keys, NULL );
// Check and repair index
//printf( "keys.count = %d\n", keys.count );
for( int j = 0; j < keys.count; ++j ) {
//printf( "processing %s\n", keys.items[j] );
status_free( status_from_uri( keys.items[j] ) );
}
free( keys.items );
//sleep(1);
}
printf( "done.\n" );
/*
printf( "Sweeping legacy uri->status_id index\n" );
// Sweep legacy url to status id index
for( int i = 0; i <= 0xFFFFF; ++i ) {
int offset = 0;
next:
char* key = ffdb_hash_get_key( "data/statuses/uri", i, offset );
if( !key ) { continue; }
printf( "Processing key %s\n", key );
int id = 0;
if( !hash_index_get( "data/statuses/uri", key, &id ) ) { continue; }
struct status* s = status_from_id( id );
if( !s ) {
printf( "Unable to load status %s (id=%d), removing from index.\n", key, id );
hash_index_remove( "data/statuses/uri", key );
free(key);
goto next;
}
// Status still exists, goto next entry
status_save(s);
status_free(s);
free(key);
offset += 1;
goto next;
}
*/
}