diff --git a/src/controller/api/status.c b/src/controller/api/status.c index b7cd0ce..316693d 100644 --- a/src/controller/api/status.c +++ b/src/controller/api/status.c @@ -10,6 +10,7 @@ // 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" @@ -18,6 +19,7 @@ // View #include "view/api/Account.h" #include "view/api/Status.h" +#include "view/api/Poll.h" // Controller #include "controller/api/client_apps.h" @@ -381,3 +383,122 @@ 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; + + 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; } + + // Make sure the status has a poll + if( !s->poll ) { goto failed; } + + if( http_request_route_term( req, "votes" ) ) { + 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; +} + diff --git a/src/controller/api/status.h b/src/controller/api/status.h index bf1e22e..6bd5464 100644 --- a/src/controller/api/status.h +++ b/src/controller/api/status.h @@ -16,4 +16,5 @@ bool handle_repost( struct http_request* req, struct status* s ); bool handle_show_bookmarks( struct http_request* req ); bool route_statuses( struct http_request* req ); +bool route_polls( struct http_request* req ); diff --git a/src/controller/mastodon_api.c b/src/controller/mastodon_api.c index 59240f1..54561b1 100644 --- a/src/controller/mastodon_api.c +++ b/src/controller/mastodon_api.c @@ -373,6 +373,8 @@ bool route_mastodon_api( struct http_request* req ) return route_custom_emojis(req); } else if( http_request_route( req, "statuses" ) ) { return route_statuses(req); + } else if( http_request_route( req, "polls" ) ) { + return route_polls(req); } if( !check_authentication_header(req) ) { diff --git a/src/model/status.c b/src/model/status.c index 0902ae9..90c4e14 100644 --- a/src/model/status.c +++ b/src/model/status.c @@ -146,8 +146,9 @@ struct status* status_from_id( unsigned int id ) return NULL; } + // Force the poll id to be the same as the status id if( s->poll ) { - printf( "Has poll data\n" ); + s->poll->id = s->id; } // Convert media to media2 @@ -372,10 +373,7 @@ static void sync_poll( struct status* s, void* poll_data_ptr ) } // Rebuild votes_count - s->poll->votes_count = 0; - for( int i = 0; i < s->poll->options.count; ++i ) { - s->poll->votes_count += s->poll->options.items[i]->votes.count; - } + status_poll_update_vote_count( s->poll ); } // TODO: Move to src/model/status/ap_sync.c @@ -464,7 +462,7 @@ bool status_sync_from_activity_pub( struct status* s, struct ap_object* act ) poll = malloc(sizeof(*poll)); memset(poll,0,sizeof(*poll)); - poll->id = 1; + poll->id = s->id; poll->multiple_choice = false; s->poll = poll; @@ -477,7 +475,7 @@ bool status_sync_from_activity_pub( struct status* s, struct ap_object* act ) poll = malloc(sizeof(*poll)); memset(poll,0,sizeof(*poll)); - poll->id = 1; + poll->id = s->id; poll->multiple_choice = true; s->poll = poll; diff --git a/src/model/status/poll.c b/src/model/status/poll.c index a1798e3..40de1eb 100644 --- a/src/model/status/poll.c +++ b/src/model/status/poll.c @@ -1,6 +1,10 @@ #include "poll.h" +#include "model/account.h" #include "model/emoji.h" +#include "model/status.h" + +#include "collections/array.h" #include #include @@ -18,6 +22,8 @@ JSON_FIELD_TYPE_OBJECT_LAYOUT_WITH_DEFAULTS( status_poll_option ); #define OBJ_TYPE struct status_poll struct json_object_field status_poll_layout[] = { JSON_FIELD_INTEGER( id, false ), + JSON_FIELD_INTEGER( votes_count, false ), + JSON_FIELD_ARRAY_OF_INTS( own_votes, false ), JSON_FIELD_BOOL( multiple_choice, false ), JSON_FIELD_DATETIME( expires_at, false ), JSON_FIELD_BOOL( voted, false ), @@ -48,6 +54,79 @@ void status_poll_free( struct status_poll* p ) } free( p->emoji.items ); + free(p->own_votes.items); + free(p); } +bool status_poll_has_option( struct status_poll* p, int idx ) +{ + if( idx < 0 ) { return false; } + return idx < p->options.count; +} +bool status_poll_add_vote( struct status* s, struct account* a, void* choices_ptr ) +{ + // Verify preconditions + if( !s ) { return false; } + if( !a ) { return false; } + if( !choices_ptr ) { return false; } + + struct { + int* items; + int count; + } *choices = choices_ptr; + + // Validate vote choices + for( int i = 0; i < choices->count; ++i ) { + if( !status_poll_has_option( s->poll, choices->items[i] ) ) { + printf( "Poll doesn't have option %d\n", choices->items[i] ); + return false; + } + } + + // Update model + for( int i = 0; i < choices->count; ++i ) { + int choice = choices->items[i]; + struct status_poll_option* option = s->poll->options.items[choice]; + + int id = a->id; + bool should_add = true; + if( id != poll_vote_unknown_id ) { + for( int j = 0; j < option->votes.count; ++j ) { + if( option->votes.items[j] == id ) { + should_add = false; + break; + } + } + } + if( should_add ) { + array_append( &option->votes, sizeof(id), &id ); + if( a->id == owner_account_id ) { + array_append( &s->poll->own_votes, sizeof(choice), &choice ); + } + } + } + + // Federate response if account is owner + if( a->id == owner_account_id ) { + s->poll->voted = true; + + // https://www.w3.org/TR/activitystreams-vocabulary/#questions + // See Example 153 + + // TODO: Create federation object + //struct ap_object* obj; + } + + status_poll_update_vote_count( s->poll ); + + return true; +} + +void status_poll_update_vote_count( struct status_poll* poll ) +{ + poll->votes_count = 0; + for( int i = 0; i < poll->options.count; ++i ) { + poll->votes_count += poll->options.items[i]->votes.count; + } +} diff --git a/src/model/status/poll.h b/src/model/status/poll.h index 3d04188..fcffcb6 100644 --- a/src/model/status/poll.h +++ b/src/model/status/poll.h @@ -5,6 +5,8 @@ #include struct emoji; +struct account; +struct status; struct status_poll_option { @@ -23,6 +25,12 @@ struct status_poll bool multiple_choice; int votes_count; bool voted; + + struct { + int* items; + int count; + } own_votes; + struct { struct status_poll_option** items; int count; @@ -37,4 +45,7 @@ struct status_poll extern struct json_field_type status_poll_type; void status_poll_option_free( struct status_poll_option* o ); void status_poll_free( struct status_poll* p ); +bool status_poll_has_option( struct status_poll* p, int idx ); +bool status_poll_add_vote( struct status* s, struct account* a, void* choices ); +void status_poll_update_vote_count( struct status_poll* poll ); diff --git a/src/view/api/Poll.c b/src/view/api/Poll.c index a1fef66..0e1bcb7 100644 --- a/src/view/api/Poll.c +++ b/src/view/api/Poll.c @@ -2,6 +2,7 @@ #include "model/status/poll.h" +#include "json/json.h" #include "json/layout.h" extern struct json_field_type api_Emoji_type; @@ -45,6 +46,7 @@ struct json_object_field api_Poll_layout[] = { .required = true, .type = &json_field_bool, }, + JSON_FIELD_ARRAY_OF_INTS( own_votes, true ), JSON_FIELD_INTEGER( votes_count, true ), JSON_FIELD_FIXED_NULL( voters_count ), JSON_FIELD_BOOL( voted, true ), @@ -73,3 +75,13 @@ struct json_field_type api_Poll_type = { .type_string = "Poll", }; +void api_Poll_write( struct status_poll* poll, FILE* f, int indent ) +{ + struct json_writer jw = { + .f = f, + .indentation = "\t", + .indent = indent, + }; + json_write_pretty_object_layout( &jw, api_Poll_layout, poll ); +} + diff --git a/src/view/api/Poll.h b/src/view/api/Poll.h index 3b76236..714b651 100644 --- a/src/view/api/Poll.h +++ b/src/view/api/Poll.h @@ -1,6 +1,10 @@ #pragma once +#include +struct status_poll; struct json_field_type; extern struct json_field_type api_Poll_type; +void api_Poll_write( struct status_poll* poll, FILE* f, int indent ); +