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.
642 lines
15 KiB
C
642 lines
15 KiB
C
#define _GNU_SOURCE
|
|
#include "status.h"
|
|
#include "status/react.h"
|
|
|
|
#include "model/server.h"
|
|
#include "model/account.h"
|
|
#include "model/ap/activity.h"
|
|
#include "model/notification.h"
|
|
#include "model/timeline.h"
|
|
|
|
// Submodules
|
|
#include "json/json.h"
|
|
#include "json/layout.h"
|
|
#include "ffdb/hash_index.h"
|
|
#include "sha256/sha256.h"
|
|
#include "collections/array.h"
|
|
#include "http/client/client.h"
|
|
#include "format.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stddef.h>
|
|
#include <sys/stat.h>
|
|
|
|
#define OBJ_TYPE struct status
|
|
static struct json_object_field status_layout[] = {
|
|
JSON_FIELD_INTEGER( account_id, true ),
|
|
|
|
JSON_FIELD_STRING( url, false ),
|
|
JSON_FIELD_BOOL( stub, false ),
|
|
JSON_FIELD_BOOL( remote, false ),
|
|
|
|
// DO NOT INCLUDE field content
|
|
JSON_FIELD_STRING( source, false ),
|
|
JSON_FIELD_BOOL( pinned, false ),
|
|
JSON_FIELD_DATETIME( published, false ),
|
|
|
|
JSON_FIELD_INTEGER( in_reply_to, false ),
|
|
JSON_FIELD_INTEGER( repost_id, false ),
|
|
JSON_FIELD_INTEGER( root_status_id, false ),
|
|
JSON_FIELD_INTEGER( reposted_status_id, false ),
|
|
|
|
JSON_FIELD_ARRAY_OF_STRINGS( media, false ),
|
|
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( mentions, false ),
|
|
|
|
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;
|
|
}
|
|
|
|
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( "Using new location at %s\n", filename );
|
|
return f;
|
|
}
|
|
|
|
f = fopen( format(filename,sizeof(filename), "data/statuses/%d.json", id), mode );
|
|
if( f ) {
|
|
printf( "Using existing location at %s\n", filename );
|
|
return f;
|
|
}
|
|
}
|
|
|
|
struct status* status_from_id( unsigned int id )
|
|
{
|
|
struct status* s = NULL;
|
|
|
|
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;
|
|
}
|
|
|
|
if( !s->source ) {
|
|
s->source = strdup("");
|
|
}
|
|
|
|
if( !s->remote && !s->url ) {
|
|
s->url = aformat( "https://%s/note/%d", g_server_name, s->id );
|
|
}
|
|
if( s->account_id == owner_account_id ) {
|
|
s->remote =false;
|
|
}
|
|
|
|
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->published = time(NULL);
|
|
|
|
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_name);
|
|
if( 0 != strncmp( g_server_name, 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; }
|
|
|
|
int id = -1;
|
|
if( !hash_index_get( "data/statuses/uri", uri, &id ) ) { return NULL; }
|
|
|
|
return status_from_id(id);
|
|
}
|
|
bool status_sync_from_activity_pub( struct status* s, struct ap_activity* act )
|
|
{
|
|
printf( "Syncing status from activity %s\n", act->id );
|
|
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;
|
|
}
|
|
s->account_id = a->id;
|
|
s->published = act->published;
|
|
s->remote = true;
|
|
s->stub = false;
|
|
|
|
s->source = safe_strdup(act->source.content);
|
|
s->url = strdup( act->id );
|
|
|
|
// 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));
|
|
|
|
// Recreate the media field
|
|
for( int i = 0; i < act->attachments.count; ++i ) {
|
|
struct ap_attachement* att = act->attachments.items[i];
|
|
if( att && att->url ) {
|
|
char* media = strdup( att->url );
|
|
array_append( &s->media, sizeof(char*), &media );
|
|
}
|
|
}
|
|
|
|
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 )
|
|
{
|
|
struct ap_activity* act = NULL;
|
|
FILE* f = NULL;
|
|
|
|
// Fetch the object from the remote server
|
|
char filename[512];
|
|
snprintf( filename, sizeof(filename), "data/statuses/ap/%d.json", s->id );
|
|
f = fopen(filename,"r");
|
|
if( !f ) {
|
|
printf( "* Fetching %s\n", uri );
|
|
char tmp_filename[512];
|
|
FILE* f = fopen(format(tmp_filename,512,"%s.tmp",filename),"w");
|
|
|
|
long status_code = -1;
|
|
const void* request[] = {
|
|
HTTP_REQ_URL, uri,
|
|
HTTP_REQ_HEADER, "Accept: application/ld+json",
|
|
HTTP_REQ_OUTFILE, f,
|
|
HTTP_REQ_RESULT_STATUS, &status_code,
|
|
NULL,
|
|
};
|
|
if( !http_client_do( request ) ) {
|
|
printf( "! Unable to fetch %s\n", uri );
|
|
return NULL;
|
|
}
|
|
|
|
printf( "status_code = %d\n", status_code );
|
|
if( status_code != 200 ) {
|
|
return NULL;
|
|
}
|
|
fclose(f);
|
|
|
|
rename(tmp_filename,filename);
|
|
}
|
|
|
|
f = fopen(filename,"r");
|
|
if( !f ) { return NULL; }
|
|
|
|
// Load the activity and sync status
|
|
act = ap_activity_from_FILE(f); f = NULL;
|
|
if( !act ) { goto failed; }
|
|
if( !status_sync_from_activity_pub(s,act) ) { goto failed; }
|
|
|
|
cleanup:
|
|
if( f ) { fclose(f); }
|
|
ap_activity_free(act);
|
|
return s;
|
|
failed:
|
|
if( s ) {
|
|
printf( "Creating stub status for later sync\n" );
|
|
status_flag_for_async_fetch(s);
|
|
goto cleanup;
|
|
}
|
|
|
|
status_free(s);
|
|
s = NULL;
|
|
goto cleanup;
|
|
}
|
|
bool status_sync( struct status* s )
|
|
{
|
|
return status_sync_from_uri( s, s->url );
|
|
}
|
|
struct status* status_from_uri_or_fetch( const char* uri )
|
|
{
|
|
return status_fetch_from_uri( uri );
|
|
}
|
|
struct status* status_fetch_from_uri( const char* uri )
|
|
{
|
|
struct status* s = status_from_uri(uri);
|
|
if( !s ) {
|
|
s = malloc(sizeof(*s));
|
|
memset(s,0,sizeof(*s));
|
|
|
|
s->remote = true;
|
|
s->account_id = -1;
|
|
s->stub = true;
|
|
s->published = time(NULL);
|
|
s->url = strdup(uri);
|
|
status_save_new(s);
|
|
|
|
hash_index_set( "data/statuses/uri", uri, s->id );
|
|
}
|
|
|
|
if( !status_sync_from_uri(s,uri) ) {
|
|
status_free(s);
|
|
return NULL;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
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));
|
|
|
|
s->id = -1;
|
|
s->account_id = -1;
|
|
asprintf( &s->content, "%s unfollowed you\n", a->display_name );
|
|
s->sensitive = true;
|
|
|
|
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));
|
|
|
|
s->id = -1;
|
|
s->account_id = -1;
|
|
asprintf( &s->content, "%s blocked you\n", a->display_name );
|
|
s->sensitive = true;
|
|
|
|
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 - 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
|
|
);
|
|
s->sensitive = true;
|
|
|
|
return s;
|
|
}
|
|
|
|
struct status* status_from_activity( struct ap_activity* 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;
|
|
}
|
|
|
|
bool status_save_new( struct status* s )
|
|
{
|
|
int head = -1;
|
|
FILE* f = fopen("data/statuses/HEAD","r");
|
|
fscanf(f,"%d",&head);
|
|
if( head == -1 ) { return false; }
|
|
fclose(f);
|
|
|
|
s->id = head + 1;
|
|
s->root_status_id = s->id;
|
|
|
|
f = fopen("data/statuses/HEAD.tmp","w");
|
|
fprintf( f, "%d", s->id );
|
|
fclose(f);
|
|
rename( "data/statuses/HEAD.tmp", "data/statuses/HEAD" );
|
|
|
|
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 ) {
|
|
hash_index_set( "data/statuses/uri", s->url, s->id );
|
|
}
|
|
}
|
|
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 media
|
|
for( int i = 0; i < s->media.count; ++i ) {
|
|
free(s->media.items[i]);
|
|
}
|
|
free(s->media.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->likes.items);
|
|
free(s->reposts.items);
|
|
free(s->mentions.items);
|
|
|
|
free(s);
|
|
}
|
|
struct async_status_fetch
|
|
{
|
|
struct {
|
|
int* items;
|
|
int count;
|
|
} ids;
|
|
};
|
|
#define OBJ_TYPE struct async_status_fetch
|
|
static struct json_object_field async_fetch_layout[] = {
|
|
JSON_FIELD_ARRAY_OF_INTS( ids, true ),
|
|
JSON_FIELD_END
|
|
};
|
|
#undef OBJ_TYPE
|
|
void status_flag_for_async_fetch( struct status* s )
|
|
{
|
|
struct async_status_fetch fetch;
|
|
memset( &fetch, 0, sizeof(fetch) );
|
|
|
|
json_read_object_layout_from_file( "data/statuses/async_fetch.json", async_fetch_layout, &fetch );
|
|
|
|
// Don't add if already flagged for async fetch
|
|
for( int i = 0; i < fetch.ids.count; ++i ) {
|
|
if( fetch.ids.items[i] == s->id ) {
|
|
goto already_present;
|
|
}
|
|
}
|
|
array_append( &fetch.ids, sizeof(s->id), &s->id );
|
|
|
|
json_write_object_layout_to_file( "data/statuses/async_fetch.json", "\t", async_fetch_layout, &fetch );
|
|
already_present:
|
|
free( fetch.ids.items );
|
|
}
|
|
|
|
void status_add_to_timeline( struct status* s, int timeline_id )
|
|
{
|
|
struct timeline* tl = timeline_from_id(timeline_id);
|
|
timeline_add_post( tl, s );
|
|
timeline_free(tl);
|
|
}
|
|
|
|
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 = 0;
|
|
} else {
|
|
s->in_reply_to = in_reply_to_id;
|
|
s->root_status_id = in_reply_to->root_status_id;
|
|
array_append( &in_reply_to->replies, sizeof(s->id), &s->id );
|
|
|
|
// TODO: full mention handling
|
|
int id = in_reply_to->account_id;
|
|
array_append( &s->mentions, sizeof(id), &id );
|
|
|
|
status_save(in_reply_to);
|
|
}
|
|
}
|
|
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; i != 0; i = parent->in_reply_to ) {
|
|
parent = status_from_id( i );
|
|
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_add_react( struct status* s, const char* react, struct account* a )
|
|
{
|
|
// generate outbox element
|
|
if( a->id == owner_account_id ) {
|
|
if( s->account_id == owner_account_id ) {
|
|
// De
|
|
printf( "TODO: generate outbox activity for adding reaction '%s' to status #%d by account #%d, deliver to followers\n", react, s->id );
|
|
} else {
|
|
// Deliver react to post owner
|
|
//printf( "TODO: generate outbox activity for adding reaction '%s' to status #%d by account #%d, deliver to account #%d\n", react, s->id, a->id, s->account_id );
|
|
ap_activity_react( 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 )
|
|
{
|
|
// 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 )
|
|
{
|
|
for( int i = 0; i < s->likes.count; ++i ) {
|
|
if( s->likes.items[i] == a->id ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 );
|
|
}
|
|
} else {
|
|
ap_activity_like( s );
|
|
}
|
|
}
|
|
|
|
status_save(s);
|
|
}
|
|
|