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.
555 lines
13 KiB
C
555 lines
13 KiB
C
#include "status.h"
|
|
|
|
// Submodules
|
|
#include "http/server/request.h"
|
|
#include "json/json.h"
|
|
#include "json/layout.h"
|
|
#include "collections/array.h"
|
|
#include "util/format.h"
|
|
|
|
// Model
|
|
#include "model/server.h"
|
|
#include "model/status.h"
|
|
#include "model/status/poll.h"
|
|
#include "model/status/react.h"
|
|
#include "model/account.h"
|
|
#include "model/media.h"
|
|
#include "model/outbox_envelope.h"
|
|
|
|
// View
|
|
#include "view/api/Account.h"
|
|
#include "view/api/Status.h"
|
|
#include "view/api/Poll.h"
|
|
|
|
// Controller
|
|
#include "controller/api/client_apps.h"
|
|
|
|
// Submodules
|
|
#include "ffdb/trie.h"
|
|
|
|
// Standard Library
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
|
|
void write_json_escaped( FILE* f, const char* str );
|
|
|
|
void show_status( struct http_request* req, struct status* s )
|
|
{
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body( req );
|
|
|
|
api_Status_write( s, f, 0 );
|
|
}
|
|
|
|
// Route: /api/v1/statuses/%d{s->id}/context
|
|
void show_status_context( struct http_request* req, struct status* s )
|
|
{
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body( req );
|
|
|
|
struct {
|
|
struct status** items;
|
|
int count;
|
|
} ancestors, replies;
|
|
status_get_context( s, &ancestors, &replies );
|
|
|
|
fprintf( f, "{\"ancestors\":[" );
|
|
for( int i = 0; i < ancestors.count; ++i ) {
|
|
struct status* s2 = ancestors.items[i];
|
|
api_Status_write( s2, f, 1 );
|
|
status_free(s2);
|
|
|
|
if( i != ancestors.count - 1 ) {
|
|
fprintf( f, "," );
|
|
}
|
|
}
|
|
free(ancestors.items);
|
|
|
|
fprintf( f, "],\"descendants\":[" );
|
|
for( int i = 0; i < replies.count; ++i ) {
|
|
struct status* s2 = replies.items[i];
|
|
api_Status_write( s2, f, 1 );
|
|
status_free(s2);
|
|
|
|
if( i != replies.count - 1 ) {
|
|
fprintf( f, "," );
|
|
}
|
|
}
|
|
free(replies.items);
|
|
fprintf( f, "]}" );
|
|
}
|
|
|
|
void show_statuses( struct http_request* req, struct status** ss, int count )
|
|
{
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body( req );
|
|
|
|
fprintf( f, "[" );
|
|
for( int i = 0; i < count; ++i ) {
|
|
if( i > 0 ) {
|
|
fprintf( f, "," );
|
|
}
|
|
api_Status_write( ss[i], f, 1 );
|
|
}
|
|
fprintf( f, "]" );
|
|
}
|
|
|
|
// Route: POST /api/v1/statuses
|
|
bool handle_post( struct http_request* req, struct account* a )
|
|
{
|
|
bool result = false;
|
|
struct status* s = NULL;
|
|
|
|
struct poll_params_t
|
|
{
|
|
int expires_in;
|
|
bool multiple;
|
|
struct {
|
|
char** items;
|
|
int count;
|
|
} options;
|
|
};
|
|
#define OBJ_TYPE struct poll_params_t
|
|
static struct json_object_field poll_params_layout[] = {
|
|
JSON_FIELD_INTEGER( expires_in, true ),
|
|
JSON_FIELD_BOOL( multiple, false ),
|
|
JSON_FIELD_ARRAY_OF_STRINGS( options, false ),
|
|
JSON_FIELD_END,
|
|
};
|
|
#undef OBJ_TYPE
|
|
struct params_t
|
|
{
|
|
struct {
|
|
char** items;
|
|
int count;
|
|
} media_ids;
|
|
|
|
bool sensitive;
|
|
|
|
struct poll_params_t poll;
|
|
|
|
char* status;
|
|
char* visibility;
|
|
char* in_reply_to_id;
|
|
char* quote_id;
|
|
char* spoiler_text;
|
|
} params;
|
|
#define OBJ_TYPE struct params_t
|
|
static struct json_object_field layout[] = {
|
|
JSON_FIELD_ARRAY_OF_STRINGS( media_ids, false ),
|
|
JSON_FIELD_BOOL( sensitive, false ),
|
|
JSON_FIELD_STRING( status, true ),
|
|
JSON_FIELD_STRING( visibility, true ),
|
|
JSON_FIELD_STRING( in_reply_to_id, false ),
|
|
JSON_FIELD_STRING( quote_id, false ),
|
|
JSON_FIELD_STRING( spoiler_text, false ),
|
|
{
|
|
.key = "poll",
|
|
.offset = offsetof( OBJ_TYPE, poll ),
|
|
.required = false,
|
|
.type = &json_field_object_composite,
|
|
.composite_layout = poll_params_layout,
|
|
},
|
|
JSON_FIELD_END,
|
|
};
|
|
#undef OBJ_TYPE
|
|
|
|
memset(¶ms,0,sizeof(params));
|
|
|
|
const char* indempotency_key = http_request_get_header( req, "Idempotency-Key" );
|
|
if( indempotency_key ) {
|
|
char* existing = ffdb_trie_get( "data/indempotency", indempotency_key );
|
|
if( existing ) {
|
|
s = status_from_id( atoi(existing) );
|
|
free( existing );
|
|
goto success;
|
|
}
|
|
}
|
|
|
|
FILE* data = http_request_get_request_data( req );
|
|
if( !json_read_object_layout_from_FILE( data, layout, ¶ms ) ) { goto failed; }
|
|
|
|
s = malloc(sizeof(struct status));
|
|
memset(s,0,sizeof(*s));
|
|
|
|
s->published = time(NULL);
|
|
s->source = strdup( params.status );
|
|
|
|
// Handle poll
|
|
if( params.poll.options.count > 0 ) {
|
|
struct status_poll* poll;
|
|
poll = malloc(sizeof(*poll));
|
|
memset(poll,0,sizeof(*poll));
|
|
s->poll = poll;
|
|
|
|
poll->expires_at = time(NULL) + params.poll.expires_in;
|
|
|
|
for( int i = 0; i < params.poll.options.count; ++i ) {
|
|
struct status_poll_option* option;
|
|
option = malloc(sizeof(*option));
|
|
memset(option,0,sizeof(*option));
|
|
|
|
option->title = params.poll.options.items[i];
|
|
params.poll.options.items[i] = NULL;
|
|
|
|
array_append( &poll->options, sizeof(option), &option );
|
|
}
|
|
free( params.poll.options.items );
|
|
}
|
|
|
|
if( 0 == strcmp( "direct", params.visibility ) ) {
|
|
s->visibility = status_visibility_direct;
|
|
}
|
|
|
|
status_save_new(s);
|
|
|
|
char key[32];
|
|
snprintf( key,32, "%d", s->id );
|
|
ffdb_trie_set( "data/indempotency", indempotency_key, key );
|
|
|
|
for( int i = 0; i < params.media_ids.count; ++i ) {
|
|
struct media* m = media_from_id( atoi(params.media_ids.items[i]) );
|
|
if( !m ) { continue; }
|
|
|
|
array_append( &s->media2, sizeof(m), &m );
|
|
}
|
|
|
|
if( params.in_reply_to_id ) {
|
|
const char* id = params.in_reply_to_id;
|
|
while( *id == '0' && id[1] != '\0' ) { ++id; }
|
|
|
|
status_make_reply_to( s, atoi( id ) );
|
|
}
|
|
if( params.quote_id ) {
|
|
const char* id = params.quote_id;
|
|
while( *id == '0' && id[1] != '\0' ) { ++id; }
|
|
|
|
status_make_quote_of( s, atoi(id) );
|
|
}
|
|
|
|
// Save status data
|
|
status_save(s);
|
|
|
|
// Federate
|
|
account_create( a, s );
|
|
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body(req);
|
|
api_Status_write( s, f, 0 );
|
|
success:
|
|
result = true;
|
|
cleanup:
|
|
free(params.status);
|
|
free(params.visibility);
|
|
free(params.in_reply_to_id);
|
|
free(params.spoiler_text);
|
|
for( int i = 0; i < params.media_ids.count; ++i ) {
|
|
free( params.media_ids.items[i] );
|
|
}
|
|
free( params.media_ids.items);
|
|
status_free(s);
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
// Route: POST /api/v1/statuses/%d{id}/reblog
|
|
bool handle_repost( struct http_request* req, struct status* s )
|
|
{
|
|
bool result = false;
|
|
struct account* owner = account_from_id(owner_account_id);
|
|
if( !owner ) { goto failed; }
|
|
|
|
// Federate
|
|
struct status* repost = account_announce( owner, s, NULL, NULL );
|
|
if( !repost ) { goto failed; }
|
|
|
|
// Show the new status as the response
|
|
show_status( req, repost );
|
|
goto success;
|
|
success:
|
|
result = true;
|
|
cleanup:
|
|
account_free(owner);
|
|
status_free(repost);
|
|
|
|
return result;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
// Route: POST /api/v1/statuses/%d{id}/bookmark
|
|
bool handle_bookmark( struct http_request* req, struct status* s )
|
|
{
|
|
status_set_bookmark( s );
|
|
show_status( req, s );
|
|
|
|
return true;
|
|
}
|
|
// Route: POST /api/v1/statuses/%d{id}/unbookmark
|
|
bool handle_unbookmark( struct http_request* req, struct status* s )
|
|
{
|
|
status_clear_bookmark( s );
|
|
show_status( req, s );
|
|
|
|
return true;
|
|
}
|
|
|
|
// route: GET /api/v1/bookmarks
|
|
bool handle_show_bookmarks( struct http_request* req )
|
|
{
|
|
struct {
|
|
struct status** items;
|
|
int count;
|
|
} results;
|
|
memset(&results,0,sizeof(results));
|
|
|
|
status_get_bookmarks( 0, 100, &results );
|
|
show_statuses( req, results.items, results.count );
|
|
for( int i = 0; i < results.count; ++i ) {
|
|
status_free(results.items[i]);
|
|
}
|
|
free(results.items);
|
|
|
|
return true;
|
|
}
|
|
|
|
// route: GET /api/v1/statuses/%d{id}/favourite
|
|
bool handle_favorite( struct http_request* req, struct status* s )
|
|
{
|
|
struct account* owner = account_from_id( owner_account_id );
|
|
status_add_like(s,owner);
|
|
status_save(s);
|
|
show_status( req, s );
|
|
|
|
account_free(owner);
|
|
|
|
return true;
|
|
}
|
|
|
|
// route: GET /api/v1/statuses/%d{id}/pin
|
|
bool handle_pin( struct http_request* req, struct status* s )
|
|
{
|
|
struct account* owner = account_from_id( owner_account_id );
|
|
account_pin_status( owner, s );
|
|
status_save(s);
|
|
show_status( req, s );
|
|
|
|
account_free(owner);
|
|
|
|
return true;
|
|
}
|
|
// route: GET /api/v1/statuses/%d{id}/unpin
|
|
bool handle_unpin( struct http_request* req, struct status* s )
|
|
{
|
|
struct account* owner = account_from_id( owner_account_id );
|
|
account_unpin_status( owner, s );
|
|
status_save(s);
|
|
show_status( req, s );
|
|
|
|
account_free(owner);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool http_request_route_id( struct http_request* req, int* id )
|
|
{
|
|
char* id_str = http_request_route_get_dir_or_file(req);
|
|
if( !id_str || !*id_str ) { return false; }
|
|
*id = -1;
|
|
sscanf( id_str, "%d", id );
|
|
free(id_str);
|
|
if( *id == -1 ) { return false; }
|
|
|
|
return true;
|
|
}
|
|
|
|
// Route: /api/v1/statuses/%d{id}/
|
|
bool route_statuses( struct http_request* req )
|
|
{
|
|
bool result = false;
|
|
struct status* s = NULL;
|
|
|
|
if( http_request_route_term( req, "" ) ) {
|
|
if( http_request_route_method( req, "POST" ) ) {
|
|
|
|
if( !check_authentication_header(req) ) { return false; }
|
|
|
|
struct account* owner = account_from_id(owner_account_id);
|
|
bool res = handle_post(req, owner);
|
|
account_free(owner);
|
|
return res;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if( !http_request_route( req, "/" ) ) { return false; }
|
|
|
|
int id = -1;
|
|
|
|
if( !http_request_route_id( req, &id ) ) { goto failed; }
|
|
s = status_from_id(id);
|
|
if( !s ) { goto failed; }
|
|
|
|
if( http_request_route( req, "context" ) ) {
|
|
show_status_context( req, s );
|
|
goto success;
|
|
} else if( http_request_route_term( req, "" ) ) {
|
|
show_status( req, s );
|
|
goto success;
|
|
}
|
|
|
|
if( !check_authentication_header(req) ) { return false; }
|
|
|
|
if( http_request_route_term( req, "reblog" ) ) {
|
|
if( handle_repost( req, s ) ) { goto success; }
|
|
} else if( http_request_route_term( req, "bookmark" ) ) {
|
|
if( handle_bookmark( req, s ) ) { goto success; }
|
|
} else if( http_request_route_term( req, "unbookmark" ) ) {
|
|
if( handle_unbookmark( req, s ) ) { goto success; }
|
|
} else if( http_request_route_term( req, "favourite" ) ) {
|
|
if( handle_favorite( req, s ) ) { goto success; }
|
|
} else if( http_request_route_term( req, "pin" ) ) {
|
|
if( handle_pin( req, s ) ) { goto success; }
|
|
} else if( http_request_route_term( req, "unpin" ) ) {
|
|
if( handle_unpin( req, s ) ) { goto success; }
|
|
}
|
|
|
|
goto failed;
|
|
cleanup:
|
|
status_free(s);
|
|
return result;
|
|
success:
|
|
result = true;
|
|
goto cleanup;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
// Route: /api/v1/polls/%d{id}/votes
|
|
bool handle_votes( struct http_request* req, struct status* s )
|
|
{
|
|
bool result = false;
|
|
struct account* owner = account_from_id( owner_account_id );
|
|
|
|
// Enforce preconditions
|
|
if( !s ) { goto failed; }
|
|
if( !owner ) { goto failed; }
|
|
if( !s->poll ) { goto failed; }
|
|
|
|
printf( "Processing poll votes...\n" );
|
|
|
|
struct params_t
|
|
{
|
|
struct {
|
|
char** items;
|
|
int count;
|
|
} choices;
|
|
} params;
|
|
memset(¶ms,0,sizeof(params));
|
|
#define OBJ_TYPE struct params_t
|
|
static struct json_object_field layout[] = {
|
|
JSON_FIELD_ARRAY_OF_STRINGS( choices, true ),
|
|
JSON_FIELD_END,
|
|
};
|
|
#undef OBJ_TYPE
|
|
|
|
FILE* data = http_request_get_request_data( req );
|
|
if( !json_read_object_layout_from_FILE( data, layout, ¶ms ) ) {
|
|
printf( "Unable to read params\n" );
|
|
goto failed;
|
|
}
|
|
|
|
struct {
|
|
int* items;
|
|
int count;
|
|
} choices_ints;
|
|
memset(&choices_ints,0,sizeof(choices_ints));
|
|
|
|
// Make sure all options selected exist before voting
|
|
for( int i = 0; i < params.choices.count; ++i ) {
|
|
int idx = atoi(params.choices.items[i]);
|
|
array_append( &choices_ints, sizeof(idx), &idx );
|
|
}
|
|
|
|
if( status_poll_add_vote( s, owner, &choices_ints ) ) {
|
|
status_save(s);
|
|
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body( req );
|
|
api_Poll_write( s->poll, f, 0 );
|
|
|
|
goto success;
|
|
}
|
|
|
|
printf( "Unable to add votes\n" );
|
|
goto failed;
|
|
cleanup:
|
|
for( int i = 0; i < params.choices.count; ++i ) {
|
|
free( params.choices.items[i] );
|
|
}
|
|
free(params.choices.items );
|
|
|
|
return result;
|
|
success:
|
|
result = true;
|
|
goto cleanup;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|
|
// Route: /api/v1/polls/%d{id}/
|
|
bool route_polls( struct http_request* req )
|
|
{
|
|
bool result = false;
|
|
struct status* s = NULL;
|
|
//struct account* owner = NULL;
|
|
|
|
if( !check_authentication_header(req) ) { return false; }
|
|
|
|
//owner = account_from_id(owner_account_id);
|
|
|
|
if( !http_request_route( req, "/" ) ) { return false; }
|
|
|
|
int id = -1;
|
|
if( !http_request_route_id( req, &id ) ) { goto failed; }
|
|
s = status_from_id(id);
|
|
if( !s ) { goto failed; }
|
|
|
|
// Make sure the status has a poll
|
|
if( !s->poll ) { goto failed; }
|
|
|
|
if( http_request_route_term( req, "" ) ) {
|
|
status_sync(s);
|
|
|
|
http_request_send_headers( req, 200, "application/json", true );
|
|
FILE* f = http_request_get_response_body( req );
|
|
api_Poll_write( s->poll, f, 0 );
|
|
|
|
goto success;
|
|
} else if( http_request_route_term( req, "votes" ) ) {
|
|
if( http_request_route_method( req, "POST" ) ) {
|
|
if( handle_votes( req, s ) ) { goto success; }
|
|
}
|
|
}
|
|
|
|
goto failed;
|
|
cleanup:
|
|
status_free(s);
|
|
return result;
|
|
success:
|
|
result = true;
|
|
goto cleanup;
|
|
failed:
|
|
result = false;
|
|
goto cleanup;
|
|
}
|
|
|