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.
604 lines
14 KiB
C
604 lines
14 KiB
C
#include "inbox.h"
|
|
|
|
// Submodules
|
|
#include "http/server/request.h"
|
|
#include "json/json.h"
|
|
#include "ffdb/fs_list.h"
|
|
#include "collections/array.h"
|
|
#include "util/format.h"
|
|
#include "ap/object.h"
|
|
|
|
// Model
|
|
#include "model/server.h"
|
|
#include "model/status.h"
|
|
#include "model/account.h"
|
|
#include "model/notification.h"
|
|
#include "model/inbox_envelope.h"
|
|
#include "model/crypto/http_sign.h"
|
|
|
|
// Controller
|
|
#include "controller/inbox/follow.h"
|
|
#include "controller/inbox/forward.h"
|
|
|
|
// Standard Library
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
extern bool terminate;
|
|
|
|
// Route: /inbox
|
|
bool route_inbox( struct http_request* req )
|
|
{
|
|
// No subroutes
|
|
if( !http_request_route_term( req, "" ) ) { return false; }
|
|
if( !http_request_route_method( req, "POST" ) ) { return false; }
|
|
|
|
if( envelope_create_from_request( req ) ) {
|
|
http_request_send_headers( req, 200, "text/plain", true );
|
|
} else {
|
|
http_request_send_headers( req, 400, "text/plain", true );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool route_undo_Announce( struct ap_object* act )
|
|
{
|
|
struct status* s = NULL;
|
|
|
|
s = status_from_uri( act->object.ptr->id );
|
|
if( !s ) { return true; } // Status not local, discard
|
|
|
|
printf( "TODO: delete post\n" );
|
|
|
|
status_free(s);
|
|
return true;
|
|
}
|
|
static bool route_undo_Like( struct ap_object* act )
|
|
{
|
|
bool result = false;
|
|
struct status* s = NULL;
|
|
struct account* a = NULL;
|
|
|
|
s = status_from_uri( act->object.ptr->id );
|
|
if( !s ) { goto discard; } // Satus not local, discard
|
|
|
|
a = account_from_uri( act->actor );
|
|
if( !a )
|
|
|
|
status_remove_like( s, a );
|
|
goto cleanup;
|
|
cleanup:
|
|
status_free(s);
|
|
account_free(a);
|
|
return result;
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
}
|
|
static bool route_undo_EmojiReact( struct ap_object* act )
|
|
{
|
|
bool result = false;
|
|
struct status* s = NULL;
|
|
struct account* a = NULL;
|
|
s = status_from_uri( act->object.ptr->id );
|
|
if( !s ) { goto discard; }
|
|
|
|
a = account_from_uri( act->object.ptr->actor );
|
|
if( !a ) { goto discard; }
|
|
|
|
status_remove_react( s, act->object.ptr->content.content, a );
|
|
goto discard;
|
|
cleanup:
|
|
account_free(a);
|
|
status_free(s);
|
|
return result;
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_undo_activity( struct ap_object* act )
|
|
{
|
|
if( act->object.tag != apaot_activity ) {
|
|
printf( "Can't undo reference activities, discarding...\n" );
|
|
return true;
|
|
}
|
|
if( !act->object.ptr ) {
|
|
printf( "No object in activity\n" );
|
|
return false;
|
|
}
|
|
|
|
switch( act->object.ptr->type ) {
|
|
case ap_Follow: return route_undo_follow( act );
|
|
case ap_Announce: return route_undo_Announce( act );
|
|
case ap_Like: return route_undo_Like( act );
|
|
case pleroma_EmojiReact: return route_undo_EmojiReact(act);
|
|
default:
|
|
printf( "Unhandled object activity type %d in undo\n", act->object.ptr->type );
|
|
return false;
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
enum {
|
|
error_lookup_failed = 1,
|
|
};
|
|
static struct status* lookup_object_status( struct ap_object* act, int* error )
|
|
{
|
|
struct status* s = NULL;
|
|
|
|
switch( act->object.tag ) {
|
|
case apaot_ref:
|
|
s = status_from_uri( act->object.ref );
|
|
if( !s ) {
|
|
s = status_fetch_from_uri( act->object.ref );
|
|
if( !s ) {
|
|
*error = error_lookup_failed;
|
|
return NULL;
|
|
}
|
|
status_save_new(s);
|
|
}
|
|
break;
|
|
case apaot_activity:
|
|
s = status_from_uri( act->object.ptr->id );
|
|
if( !s ) {
|
|
s = status_from_activity( act->object.ptr );
|
|
if( !s ) {
|
|
*error = error_lookup_failed;
|
|
return NULL;
|
|
}
|
|
status_save_new( s );
|
|
}
|
|
break;
|
|
}
|
|
|
|
if( s ) { return s; }
|
|
|
|
switch( act->object.tag ) {
|
|
case apaot_ref:
|
|
printf( "! Status %s doesn't exist locally (object.ref)\n", act->object.ref );
|
|
break;
|
|
case apaot_activity:
|
|
printf( "! Status %s doesn't exist locally (object.ptr)\n", act->object.ptr->id );
|
|
break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static struct account* lookup_actor_account( struct ap_object* act )
|
|
{
|
|
struct account* a = account_from_uri( act->actor );
|
|
if( a ) { return a; }
|
|
|
|
a = account_fetch_from_uri( act->actor );
|
|
return a;
|
|
}
|
|
|
|
static bool route_like( struct ap_object* act )
|
|
{
|
|
struct status* s = NULL;
|
|
struct account* liker = NULL;
|
|
bool result = false;
|
|
int error;
|
|
s = lookup_object_status(act,&error);
|
|
liker = lookup_actor_account(act);
|
|
|
|
if( !s ) {
|
|
if( error == error_lookup_failed ) { goto discard; }
|
|
goto failed;
|
|
};
|
|
|
|
status_add_like( s, liker );
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
cleanup:
|
|
account_free(liker);
|
|
status_free(s);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_emoji_react( struct ap_object* act )
|
|
{
|
|
struct status* s = NULL;
|
|
struct account* reactor = NULL;
|
|
bool result = false;
|
|
int error;
|
|
s = lookup_object_status(act,&error);
|
|
reactor = lookup_actor_account(act);
|
|
if( !s || !reactor ) { goto failed; }
|
|
|
|
status_add_react( s, act->content.content, reactor );
|
|
goto discard;
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
cleanup:
|
|
status_free(s);
|
|
account_free(reactor);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_update_Person( struct ap_object* act )
|
|
{
|
|
bool result = false;
|
|
struct account* a = account_fetch_from_uri( act->object.ptr->id );
|
|
if( !a ) { return true; } // We don't have this user locally, we can safely discard
|
|
|
|
if( !account_sync_from_activity( a, act->object.ptr ) ) { goto failed; }
|
|
|
|
account_save(a);
|
|
|
|
result = true;
|
|
cleanup:
|
|
account_free(a);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_update( struct ap_object* act )
|
|
{
|
|
struct status* s = NULL;
|
|
|
|
if( act->object.tag == apaot_activity ) {
|
|
switch( act->object.ptr->type ) {
|
|
case ap_Note:
|
|
{
|
|
s = status_from_uri( act->object.ptr->id );
|
|
if( !s ) { return true; } // Status not available locally, discard
|
|
|
|
// TODO: update status
|
|
}; break;
|
|
case ap_Person:
|
|
return route_update_Person(act);
|
|
case ap_Question:
|
|
// TODO: update Poll
|
|
return true;
|
|
case ap_Service:
|
|
// discard
|
|
return true;
|
|
}
|
|
} else if( act->object.tag == apaot_ref ) {
|
|
s = status_from_uri( act->object.ref );
|
|
if( !s ) { return true; } // Status not available locally, discard
|
|
}
|
|
|
|
status_sync_from_uri( s, s->url );
|
|
|
|
status_save(s);
|
|
status_free(s);
|
|
return true;
|
|
}
|
|
|
|
static bool route_move( struct ap_object* act )
|
|
{
|
|
bool result = false;
|
|
struct account* a = NULL;
|
|
|
|
if( act->object.tag != apaot_ref ) { goto discard; }
|
|
|
|
// Make sure this belongs to a local account
|
|
a = account_from_uri( act->object.ref );
|
|
if( !a ) { goto discard; }
|
|
|
|
// Verify the target is an existing alias
|
|
for( int i = 0; i < a->aliases.count; ++i ) {
|
|
if( 0 == strcmp( a->aliases.items[i], act->target ) ) {
|
|
goto is_alias;
|
|
}
|
|
}
|
|
goto failed;
|
|
is_alias:
|
|
account_move( a, act->target );
|
|
goto discard;
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
cleanup:
|
|
account_free(a);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_create( struct ap_object* act )
|
|
{
|
|
struct status* s = NULL;
|
|
bool result = false;
|
|
struct account* actor_account = NULL;
|
|
struct account* owner_account = NULL;
|
|
|
|
// Requires an object
|
|
if( act->object.tag != apaot_activity ) {
|
|
printf( "TODO: fetch activity from %s\n", act->object.ref );
|
|
goto failed;
|
|
}
|
|
struct ap_object* obj = act->object.ptr;
|
|
|
|
bool mentions_me = false;
|
|
bool account_followed = false;
|
|
|
|
// Does this activity have mention me
|
|
char owner_url[512];
|
|
snprintf( owner_url, sizeof(owner_url), "https://%s/owner/actor", g_server->domain );
|
|
for( int i = 0; i < obj->tags.count; ++i ) {
|
|
struct ap_activity_tag* tag = obj->tags.items[i];
|
|
|
|
//printf( "tag = { &=%p, .type=%d, .href=%s, .name=%s }\n", tag, tag->type, tag->href, tag->name );
|
|
|
|
if( tag->type != aptag_mention ) { continue; }
|
|
if( 0 == strcmp(tag->href, owner_url) ) {
|
|
mentions_me = true;
|
|
goto check_is_follower;
|
|
}
|
|
}
|
|
|
|
check_is_follower:
|
|
// Get actor account
|
|
char* actor_uri = obj->actor;
|
|
if( !actor_uri ) {
|
|
actor_uri = obj->attributed_to;
|
|
obj->actor = strdup(actor_uri);
|
|
}
|
|
actor_account = account_from_uri_or_fetch( actor_uri );
|
|
if( !actor_account ) {
|
|
printf( "! Unable to fetch %s\n", actor_uri );
|
|
goto discard;
|
|
}
|
|
owner_account = account_from_id( owner_account_id );
|
|
|
|
if( account_does_follow( owner_account, actor_account->id ) ) {
|
|
account_followed = true;
|
|
}
|
|
|
|
if( !account_followed && !mentions_me ) {
|
|
// Discard without action
|
|
//printf( "Discarding create. account_followed=%c, mentions_me=%c\n", account_followed ? 'T' : 'F', mentions_me ? 'T' : 'F' );
|
|
goto discard;
|
|
}
|
|
|
|
// Create local status
|
|
s = status_from_uri( obj->id );
|
|
if( !s ) {
|
|
s = status_from_activity(obj);
|
|
if( !s ) {
|
|
printf( "! Failed to load status from activity\n" );
|
|
goto discard;
|
|
}
|
|
status_save_new(s);
|
|
}
|
|
|
|
// Add to timelines
|
|
if( account_followed ) {
|
|
status_add_to_timeline( s, home_timeline_id );
|
|
|
|
// TODO: create notification if user notifications are on or this is part of a watched conversation
|
|
}
|
|
if( mentions_me ) {
|
|
// Add me to mention lists
|
|
int id = owner_account_id;
|
|
array_append( &s->mentions, sizeof(id), &id );
|
|
|
|
// Create notification
|
|
struct notification* note = notification_new();
|
|
note->debug = 6;
|
|
note->type = nt_mention;
|
|
note->status_id = s->id;
|
|
note->account_id = actor_account->id;
|
|
notification_save( note );
|
|
notification_free( note );
|
|
}
|
|
|
|
// Add to standard timelines
|
|
status_add_to_timeline( s, public_timeline_id );
|
|
//status_add_to_timeline( s, federated_timeline_id );
|
|
status_add_to_timeline( s, actor_account->id );
|
|
|
|
discard:
|
|
result = true;
|
|
goto cleanup;
|
|
cleanup:
|
|
account_free(actor_account);
|
|
account_free(owner_account);
|
|
status_free(s);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_block( struct ap_object* act )
|
|
{
|
|
bool result = false;
|
|
struct account* actor_account = NULL;
|
|
|
|
// Get actor account
|
|
actor_account = account_from_uri_or_fetch( act->actor );
|
|
if( !actor_account ) {
|
|
printf( "! Unable to fetch %s\n", act->actor );
|
|
goto failed;
|
|
}
|
|
|
|
// TODO: decide what to do about blocks besides feed the block bot
|
|
|
|
struct notification* notice = notification_new();
|
|
notice->type = nt_block;
|
|
notice->account_id = system_account_id;
|
|
notice->ref_account_id = actor_account->id;
|
|
notification_save(notice);
|
|
notification_free(notice);
|
|
|
|
result = true;
|
|
cleanup:
|
|
account_free(actor_account);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
static bool route_accept( struct ap_object* act )
|
|
{
|
|
// TODO: actually handle accepts
|
|
// In particular, this needs to change a follow request to a following accepted
|
|
return true;
|
|
}
|
|
|
|
bool route_activity( struct ap_object* act )
|
|
{
|
|
printf( "Handling %s\n", act->id );
|
|
ap_object_write_to_FILE( act, stdout );
|
|
|
|
switch( act->type ) {
|
|
case ap_Undo: return route_undo_activity(act);
|
|
case ap_Follow: return route_follow(act);
|
|
case ap_Like: return route_like(act);
|
|
case ap_Create: return route_create(act);
|
|
case ap_Accept: return route_accept(act);
|
|
case ap_Announce: return route_announce(act);
|
|
case ap_Add: return route_add(act);
|
|
case pleroma_EmojiReact: return route_emoji_react(act);
|
|
case ap_Update: return route_update(act);
|
|
case ap_Move: return route_move(act);
|
|
case ap_Block: return route_block(act);
|
|
case ap_Remove: return true;
|
|
default:
|
|
printf( "Unhandled activity type: %d\n", act->type );
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool process_one()
|
|
{
|
|
// Items requiring cleanup
|
|
struct ap_object* act = NULL;
|
|
struct ap_envelope* env = NULL;
|
|
bool result = false;
|
|
|
|
int tail_pos = fs_list_get("data/inbox/TAIL");
|
|
int head_pos = fs_list_get("data/inbox/HEAD");
|
|
if( head_pos <= tail_pos ) { return false; }
|
|
|
|
// We have data to process
|
|
printf( "Inbox has %d items pending processing...\n", (head_pos - tail_pos) );
|
|
|
|
int id = tail_pos + 1;
|
|
printf( "Next item is #%d\n", id );
|
|
|
|
env = ap_envelope_from_id(id);
|
|
bool step_tail = false;
|
|
|
|
if( !env ) {
|
|
printf( "! Failed to parse envelope+activity for data/inbox/%d.json\n", id );
|
|
goto discard;
|
|
}
|
|
|
|
// Load activity
|
|
FILE* f = fmemopen( env->body, strlen(env->body), "r" );
|
|
act = ap_object_from_FILE(f);
|
|
if( !act ) { goto failed; }
|
|
|
|
if( handle_forward( env, act ) ) { goto discard; }
|
|
|
|
// Sanitize actor
|
|
if( !act->actor ) {
|
|
if( !act->attributed_to ) {
|
|
printf( "! activity does not have actor or attributedTo property, can't process\n" );
|
|
goto discard;
|
|
}
|
|
act->actor = strdup(act->attributed_to);
|
|
}
|
|
|
|
// Discard delete requests
|
|
if( act->type == ap_Delete ) {
|
|
goto discard;
|
|
}
|
|
|
|
// Validate signature
|
|
env->validated = http_signature_validate( env, "post /inbox", act->actor );
|
|
|
|
|
|
|
|
if( !env->validated ) { goto discard; }
|
|
|
|
printf( "Processing %d\n", id );
|
|
step_tail = route_activity( act );
|
|
|
|
cleanup:
|
|
printf( "\nstep_tail=%c\n", step_tail ? 'T' : 'F' );
|
|
if( step_tail ) {
|
|
fs_list_set( "data/inbox/TAIL", id );
|
|
result = true;
|
|
}
|
|
|
|
ap_object_free(act);
|
|
ap_envelope_free(env);
|
|
printf( "result=%c\n", result ? 'T' : 'F' );
|
|
fflush(stdout);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
discard:
|
|
step_tail = true;
|
|
result = true;
|
|
goto cleanup;
|
|
}
|
|
|
|
bool cleanup_box( const char* box )
|
|
{
|
|
char tail[512];
|
|
char dead[512];
|
|
|
|
int tail_pos = fs_list_get(format(tail,sizeof(tail),"%s/TAIL",box));
|
|
int dead_pos = fs_list_get(format(dead,sizeof(dead),"%s/DEAD",box));
|
|
if( dead_pos < tail_pos ) {
|
|
char filename[512];
|
|
FILE* f = fopen( format(filename,512,"%s/%d.json", box, dead_pos + 1 ), "r" );
|
|
if( f ) {
|
|
fclose(f);
|
|
if( 0 == remove(filename) ) {
|
|
// File successfully remove, advance
|
|
fs_list_set(dead, dead_pos + 1 );
|
|
|
|
return true;
|
|
}
|
|
} else {
|
|
// File already doesn't exist, advance
|
|
fs_list_set( dead, dead_pos + 1 );
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void process_inbox()
|
|
{
|
|
while( !terminate ) {
|
|
bool activity = false;
|
|
activity |= process_one();
|
|
activity |= cleanup_box( "data/inbox" );
|
|
|
|
if( !activity ) {
|
|
fflush(stdout);
|
|
sleep(1);
|
|
}
|
|
}
|
|
}
|
|
|