#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 #include #include #include #include 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); } } }