Slon/Api/V1/Polls: Implement Polls

Fixes #8
This commit is contained in:
Alec Murphy 2025-03-16 19:21:44 -04:00
parent 9a2a68ef93
commit 966be78422
8 changed files with 347 additions and 0 deletions

191
Slon/Api/V1/Polls.HC Normal file
View file

@ -0,0 +1,191 @@
U0 @slon_api_v1_async_vote_fedi(JsonObject* vote_object)
{
U8 scratch_buffer[512];
I64 this_vote_id = SysTimerRead;
JsonArray* choices = vote_object->a("choices");
JsonObject* status = vote_object->o("status");
JsonObject* ap_vote_object = NULL;
JsonObject* object = NULL;
U8* choice_name = NULL;
U8* this_actor = db->o("actors")->o(vote_object->@("username"))->@("id");
U8* remote_actor = status->o("account")->@("remote_actor");
U8* fetch_buffer = CAlloc(HTTP_FETCH_BUFFER_SIZE, slon_mem_task);
I64 i;
for (i = 0; i < choices->length; i++) {
choice_name = status->o("poll")->a("options")->o(i)->@("title");
ap_vote_object = Json.CreateObject(slon_mem_task);
ap_vote_object->set("@context", "https://www.w3.org/ns/activitystreams", JSON_STRING);
StrPrint(scratch_buffer, "%s#votes/%d/activity", this_actor, this_vote_id);
ap_vote_object->set("id", scratch_buffer, JSON_STRING);
ap_vote_object->set("to", remote_actor, JSON_STRING);
ap_vote_object->set("actor", this_actor, JSON_STRING);
ap_vote_object->set("type", "Create", JSON_STRING);
object = Json.CreateObject(slon_mem_task);
StrPrint(scratch_buffer, "%s#votes/%d", this_actor, this_vote_id);
object->set("id", scratch_buffer, JSON_STRING);
object->set("type", "Note", JSON_STRING);
object->set("name", choice_name, JSON_STRING);
object->set("attributedTo", this_actor, JSON_STRING);
object->set("to", remote_actor, JSON_STRING);
object->set("inReplyTo", status->@("uri"), JSON_STRING);
ap_vote_object->set("object", object, JSON_OBJECT);
StrPrint(scratch_buffer, "%s/inbox", remote_actor);
@slon_activitypub_signed_request(scratch_buffer, fetch_buffer, ap_vote_object);
MemSet(fetch_buffer, NULL, HTTP_FETCH_BUFFER_SIZE);
}
Free(fetch_buffer);
}
U0 @slon_api_v1_vote_fedi(U8* username, JsonObject* status, JsonArray* choices)
{
JsonObject* vote_object = Json.CreateObject(slon_mem_task);
vote_object->set("username", username, JSON_STRING);
vote_object->set("status", status, JSON_OBJECT);
vote_object->set("choices", choices, JSON_ARRAY);
Spawn(&@slon_api_v1_async_vote_fedi, vote_object, "SlonAsyncVoteTask");
}
U0 @slon_api_v1_polls_get(SlonHttpSession* session)
{
if (session->path_count() < 4) {
session->status(400);
return;
}
U8* id = session->path(3);
JsonObject* status = NULL;
JsonObject* poll = NULL;
JsonArray* poll_choices = NULL;
JsonItem* poll_choice = NULL;
JsonArray* own_votes = NULL;
U8* account_id = NULL;
if (session->auth) {
account_id = session->auth->@("account_id");
}
I64 i;
if (@slon_api_authorized(session)) {
status = @slon_api_find_status_by_poll_id(id, NULL);
if (status) {
poll = Json.Clone(status->o("poll"), session->mem_task);
poll_choices = @slon_api_status_poll_choices(session, status, account_id);
own_votes = Json.CreateArray(session->mem_task);
for (i = 0; i < poll_choices->length; i++) {
poll_choice = poll_choices->@(i, TRUE);
switch (poll_choice->type) {
case JSON_NUMBER:
own_votes->append(poll_choice->value, JSON_NUMBER);
break;
case JSON_STRING:
own_votes->append(Str2I64(poll_choice->value), JSON_NUMBER);
break;
default:
break;
}
}
poll->set("voted", poll_choices > NULL, JSON_BOOLEAN);
poll->set("own_votes", own_votes, JSON_ARRAY);
session->send(poll);
return;
}
session->status(404);
} else {
session->status(401);
}
}
U0 @slon_api_v1_polls_post(SlonHttpSession* session)
{
SLON_SCRATCH_BUFFER_AND_REQUEST_JSON
no_warn scratch_buffer;
if (session->path_count() < 5) {
session->status(400);
return;
}
U8* id = session->path(3);
JsonObject* status = NULL;
JsonObject* poll = NULL;
JsonObject* return_poll = NULL;
JsonArray* choices = NULL;
JsonArray* options = NULL;
JsonItem* choice = NULL;
JsonObject* option = NULL;
JsonArray* own_votes = NULL;
JsonObject* vote = NULL;
// Still won't let us vote: iOS Masto
if (@slon_api_authorized(session)) {
SLON_AUTH_ACCOUNT_ID
status = @slon_api_find_status_by_poll_id(id, NULL);
if (!status) {
session->status(404);
return;
}
I64 i;
I64 choice_index = 0;
poll = status->o("poll");
choices = request_json->@("choices");
options = poll->a("options");
own_votes = Json.CreateArray(session->mem_task);
for (i = 0; i < choices->length; i++) {
choice = choices->@(i, TRUE);
switch (choice->type) {
case JSON_STRING:
choice_index = Str2I64(choice->value);
break;
case JSON_NUMBER:
choice_index = choice->value;
break;
default:
break;
}
own_votes->append(choice_index, JSON_NUMBER);
option = options->o(choice_index);
option->set("votes_count", option->@("votes_count") + 1, JSON_NUMBER);
poll->set("votes_count", poll->@("votes_count") + 1, JSON_NUMBER);
}
@slon_db_save_status_to_disk(status);
vote = Json.CreateObject(slon_db_mem_task);
vote->set("status_id", status->@("id"), JSON_STRING);
vote->set("account_id", account_id, JSON_STRING);
vote->set("choices", Json.Clone(choices, slon_db_mem_task), JSON_ARRAY);
if (!db->o("votes")->a(account_id)) {
db->o("votes")->set(account_id, Json.CreateArray(slon_db_mem_task), JSON_ARRAY);
}
db->o("votes")->a(account_id)->append(vote);
@slon_db_save_votes_to_disk;
return_poll = Json.Clone(poll, session->mem_task);
return_poll->set("voted", TRUE, JSON_BOOLEAN);
return_poll->set("own_votes", own_votes, JSON_ARRAY);
// Multiple-choice votes have to be sent in separate requests for each option? wtf
if (status->o("account")->@("remote_actor")) {
// Send votes asynchronously
@slon_api_v1_vote_fedi(@slon_api_account_by_id(account_id)->@("acct"), status, vote->a("choices"));
}
session->send(return_poll);
} else {
session->status(401);
}
}

View file

@ -40,6 +40,11 @@ U0 @slon_api_v1_statuses_query(SlonHttpSession* session, JsonArray* status_array
Bool exclude_reblogs = @slon_api_get_value_as_boolean(request_json->@("exclude_reblogs", TRUE)); Bool exclude_reblogs = @slon_api_get_value_as_boolean(request_json->@("exclude_reblogs", TRUE));
Bool pinned = @slon_api_get_value_as_boolean(request_json->@("pinned", TRUE)); Bool pinned = @slon_api_get_value_as_boolean(request_json->@("pinned", TRUE));
JsonObject* poll = NULL;
JsonArray* poll_choices = NULL;
JsonItem* poll_choice = NULL;
JsonArray* own_votes = NULL;
no_warn exclude_reblogs; no_warn exclude_reblogs;
// FIXME: Implement "only_media", "exclude_reblogs", "tagged" // FIXME: Implement "only_media", "exclude_reblogs", "tagged"
Bool exclude_status = FALSE; Bool exclude_status = FALSE;
@ -71,6 +76,27 @@ U0 @slon_api_v1_statuses_query(SlonHttpSession* session, JsonArray* status_array
status->o("reblog")->set("reblogged", TRUE, JSON_BOOLEAN); status->o("reblog")->set("reblogged", TRUE, JSON_BOOLEAN);
} }
} }
if (status->@("poll")) {
poll = status->o("poll");
poll_choices = @slon_api_status_poll_choices(session, status, account_id);
poll_choice = NULL;
own_votes = Json.CreateArray(session->mem_task);
for (i = 0; i < poll_choices->length; i++) {
poll_choice = poll_choices->@(i, TRUE);
switch (poll_choice->type) {
case JSON_NUMBER:
own_votes->append(poll_choice->value, JSON_NUMBER);
break;
case JSON_STRING:
own_votes->append(Str2I64(poll_choice->value), JSON_NUMBER);
break;
default:
break;
}
}
poll->set("voted", poll_choices > NULL, JSON_BOOLEAN);
poll->set("own_votes", own_votes, JSON_ARRAY);
}
exclude_status = FALSE; exclude_status = FALSE;
if (status->@("deleted")) { if (status->@("deleted")) {
exclude_status = TRUE; exclude_status = TRUE;
@ -182,6 +208,11 @@ U0 @slon_api_v1_statuses_get(SlonHttpSession* session)
U8* id = session->path(3); U8* id = session->path(3);
JsonObject* status = NULL; JsonObject* status = NULL;
JsonObject* poll = NULL;
JsonArray* poll_choices = NULL;
JsonItem* poll_choice = NULL;
JsonArray* own_votes = NULL;
if (@slon_api_authorized(session)) { if (@slon_api_authorized(session)) {
SLON_AUTH_ACCOUNT_ID SLON_AUTH_ACCOUNT_ID
@ -218,6 +249,28 @@ U0 @slon_api_v1_statuses_get(SlonHttpSession* session)
status->o("reblog")->set("reblogged", TRUE, JSON_BOOLEAN); status->o("reblog")->set("reblogged", TRUE, JSON_BOOLEAN);
} }
} }
if (status->@("poll")) {
poll = status->o("poll");
poll_choices = @slon_api_status_poll_choices(session, status, account_id);
poll_choice = NULL;
own_votes = Json.CreateArray(session->mem_task);
I64 i;
for (i = 0; i < poll_choices->length; i++) {
poll_choice = poll_choices->@(i, TRUE);
switch (poll_choice->type) {
case JSON_NUMBER:
own_votes->append(poll_choice->value, JSON_NUMBER);
break;
case JSON_STRING:
own_votes->append(Str2I64(poll_choice->value), JSON_NUMBER);
break;
default:
break;
}
}
poll->set("voted", poll_choices > NULL, JSON_BOOLEAN);
poll->set("own_votes", own_votes, JSON_ARRAY);
}
session->send(status); session->send(status);
return; return;
} }

View file

@ -0,0 +1,4 @@
if (String.BeginsWith("/api/v1/polls", session->path())) {
@slon_api_v1_polls_get(session);
return;
}

View file

@ -0,0 +1,4 @@
if (String.BeginsWith("/api/v1/polls", session->path())) {
@slon_api_v1_polls_post(session);
return;
}

View file

@ -681,6 +681,7 @@ U0 @slon_http_handle_get_request(SlonHttpSession* session)
#include "Endpoints/Get/Notifications"; #include "Endpoints/Get/Notifications";
#include "Endpoints/Get/NodeInfo"; #include "Endpoints/Get/NodeInfo";
#include "Endpoints/Get/OAuth"; #include "Endpoints/Get/OAuth";
#include "Endpoints/Get/Polls";
#include "Endpoints/Get/Search"; #include "Endpoints/Get/Search";
#include "Endpoints/Get/Statuses"; #include "Endpoints/Get/Statuses";
#include "Endpoints/Get/Suggestions"; #include "Endpoints/Get/Suggestions";
@ -743,6 +744,7 @@ U0 @slon_http_handle_post_request(SlonHttpSession* session)
#include "Endpoints/Post/Markers"; #include "Endpoints/Post/Markers";
#include "Endpoints/Post/Media"; #include "Endpoints/Post/Media";
#include "Endpoints/Post/OAuth"; #include "Endpoints/Post/OAuth";
#include "Endpoints/Post/Polls";
#include "Endpoints/Post/Statuses"; #include "Endpoints/Post/Statuses";
/* clang-format on */ /* clang-format on */

View file

@ -22,6 +22,7 @@ WinMax(Fs);
#include "Api/V1/Markers"; #include "Api/V1/Markers";
#include "Api/V1/Media"; #include "Api/V1/Media";
#include "Api/V1/Notifications"; #include "Api/V1/Notifications";
#include "Api/V1/Polls";
#include "Api/V1/Statuses"; #include "Api/V1/Statuses";
#include "Api/V1/Timelines"; #include "Api/V1/Timelines";

View file

@ -950,6 +950,14 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
JsonArray* statuses = NULL; JsonArray* statuses = NULL;
JsonObject* status = NULL; JsonObject* status = NULL;
JsonObject* poll = NULL;
JsonArray* poll_options = NULL;
JsonObject* poll_option = NULL;
I64 votes_count = 0;
JsonArray* ap_poll_options = NULL;
JsonObject* ap_poll_option = NULL;
JsonObject* request_object = NULL; JsonObject* request_object = NULL;
U8* status_id = NULL; U8* status_id = NULL;
@ -1101,6 +1109,39 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user)
request_object = Json.Clone(request_json, slon_mem_task); request_object = Json.Clone(request_json, slon_mem_task);
} }
if (!StrICmp("update", request_json->@("type"))) {
if (request_json->o("object") && !StrICmp("question", request_json->o("object")->@("type"))) {
status_id = StrFind("/", StrFind("/statuses/", request_json->o("object")->@("id")) + 1) + 1;
status = @slon_api_find_status_by_id(status_id, @slon_api_account_by_remote_actor(request_json->@("actor")));
if (status) {
// Update local copy of poll with latest vote counts
poll = status->o("poll");
poll_options = poll->a("options");
if (request_json->o("object")->a("anyOf")) {
ap_poll_options = request_json->o("object")->a("anyOf");
}
if (request_json->o("object")->a("oneOf")) {
ap_poll_options = request_json->o("object")->a("oneOf");
}
if (ap_poll_options) {
for (i = 0; i < ap_poll_options->length; i++) {
ap_poll_option = ap_poll_options->@(i);
for (j = 0; j < poll_options->length; j++) {
poll_option = poll_options->o(j);
if (!StrICmp(ap_poll_option->@("name"), poll_option->@("title"))) {
poll_option->set("votes_count", ap_poll_option->o("replies")->@("totalItems"), JSON_NUMBER);
votes_count += ap_poll_option->o("replies")->@("totalItems");
}
}
}
@slon_db_save_status_to_disk(status);
}
}
}
request_object = Json.Clone(request_json, slon_mem_task);
}
if (request_object) { if (request_object) {
JsonObject* o = Json.CreateObject(slon_mem_task); JsonObject* o = Json.CreateObject(slon_mem_task);
o->set("actor_for_key_id", session->actor_for_key_id, JSON_STRING); o->set("actor_for_key_id", session->actor_for_key_id, JSON_STRING);

View file

@ -179,6 +179,22 @@ JsonObject* @slon_api_status_lookup_by_in_reply_to_id(U8* id, JsonArray* statuse
return NULL; return NULL;
} }
JsonObject* @slon_api_status_lookup_by_poll_id(U8* id, JsonArray* statuses)
{
if (!id || !statuses) {
return NULL;
}
I64 i;
JsonObject* status;
for (i = 0; i < statuses->length; i++) {
status = statuses->@(i);
if (!status->@("deleted") && status->@("poll") && !StrICmp(status->o("poll")->@("id"), id)) {
return status;
}
}
return NULL;
}
JsonObject* @slon_api_status_lookup_by_uri(U8* uri, JsonArray* statuses) JsonObject* @slon_api_status_lookup_by_uri(U8* uri, JsonArray* statuses)
{ {
if (!uri || !statuses) { if (!uri || !statuses) {
@ -212,6 +228,23 @@ JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id = NULL)
return NULL; return NULL;
} }
JsonObject* @slon_api_find_status_by_poll_id(U8* id, U8* account_id = NULL)
{
if (account_id) {
return @slon_api_status_lookup_by_poll_id(id, db->o("statuses")->a(account_id));
}
JsonObject* status = NULL;
JsonKey* key = db->o("statuses")->keys;
while (key) {
status = @slon_api_status_lookup_by_poll_id(id, key->value);
if (status) {
return status;
}
key = key->next;
}
return NULL;
}
JsonObject* @slon_api_find_status_by_uri(U8* uri, U8* account_id = NULL) JsonObject* @slon_api_find_status_by_uri(U8* uri, U8* account_id = NULL)
{ {
if (account_id) { if (account_id) {
@ -285,6 +318,24 @@ Bool @slon_api_status_is_bookmarked(SlonHttpSession* session, JsonObject* status
return FALSE; return FALSE;
} }
JsonArray* @slon_api_status_poll_choices(SlonHttpSession* session, JsonObject* status, U8* account_id)
{
no_warn session;
JsonArray* votes = db->o("votes")->a(account_id);
JsonObject* vote = NULL;
if (!votes) {
return NULL;
}
I64 i;
for (i = 0; i < votes->length; i++) {
vote = votes->o(i);
if (!StrICmp(vote->@("status_id"), status->@("id")) && !StrICmp(vote->@("account_id"), account_id)) {
return vote->a("choices");
}
}
return NULL;
}
U0 @slon_api_bookmark_status(SlonHttpSession* session, JsonObject* status, U8* account_id) U0 @slon_api_bookmark_status(SlonHttpSession* session, JsonObject* status, U8* account_id)
{ {
Bool is_already_bookmarked = FALSE; Bool is_already_bookmarked = FALSE;