From 5333b649175acbcf73a0e7548e8bd8d07c16a40c Mon Sep 17 00:00:00 2001 From: Alec Murphy Date: Wed, 5 Mar 2025 13:43:35 -0500 Subject: [PATCH] Everywhere: Store statuses by account id, generate timelines as array of object:account_id,status_id --- Slon/Api/V1/Statuses.HC | 86 ++++++++++------------ Slon/Api/V1/Timelines.HC | 18 +---- Slon/Modules/ActivityPub.HC | 63 +++++++++------- Slon/Modules/Api.HC | 140 ++++++++++++++++++++++++++++++------ 4 files changed, 196 insertions(+), 111 deletions(-) diff --git a/Slon/Api/V1/Statuses.HC b/Slon/Api/V1/Statuses.HC index da4f815..7dbe771 100644 --- a/Slon/Api/V1/Statuses.HC +++ b/Slon/Api/V1/Statuses.HC @@ -1,47 +1,25 @@ U0 (*@slon_api_status_create_fedi)(JsonObject* status) = NULL; U0 (*@slon_api_status_delete_fedi)(JsonObject* status) = NULL; -JsonArray* @slon_api_v1_statuses_lookup_descendants_by_id(U8* id, JsonArray* statuses) +JsonArray* @slon_api_v1_statuses_find_descendants_by_id(U8* id) { - if (!id || !statuses) { + if (!id) { return NULL; } - I64 i; + JsonArray* arr = Json.CreateArray(); - JsonObject* status; - for (i = 0; i < statuses->length; i++) { - status = statuses->@(i); - if (status->@("in_reply_to_id") && !StrICmp(status->@("in_reply_to_id"), id)) { + JsonObject* status = NULL; + JsonKey* key = db->o("statuses")->keys; + while (key) { + status = @slon_api_status_lookup_by_in_reply_to_id(id, key->value); + if (status) { arr->append(Json.CreateItem(status, JSON_OBJECT)); } + key = key->next; } return arr; } -JsonArray* @slon_api_v1_statuses_find_descendants_by_id(U8* id, U8* account_id) -{ - if (!id || !account_id) { - return NULL; - } - JsonArray* arr = NULL; - // Lookup in public timeline - arr = @slon_api_v1_statuses_lookup_descendants_by_id(id, db->o("timelines")->a("public")); - if (arr && arr->length) { - return arr; - } - // Then, lookup in home timeline - arr = @slon_api_v1_statuses_lookup_descendants_by_id(id, db->o("timelines")->o("home")->a(account_id)); - if (arr && arr->length) { - return arr; - } - // Finally, lookup in account's statuses - arr = @slon_api_v1_statuses_lookup_descendants_by_id(id, db->o("statuses")->a(account_id)); - if (arr && arr->length) { - return arr; - } - return SLON_EMPTY_JSON_ARRAY; -} - U0 @slon_api_v1_statuses_query(SlonHttpSession* session, JsonArray* status_array) { SLON_SCRATCH_BUFFER_AND_REQUEST_JSON @@ -55,11 +33,13 @@ U0 @slon_api_v1_statuses_query(SlonHttpSession* session, JsonArray* status_array I64 limit = 20; // default U64 max_id = 0; U64 min_id = 0; - Bool only_media = request_json->@("only_media"); - Bool exclude_replies = request_json->@("exclude_replies"); - Bool exclude_reblogs = request_json->@("exclude_reblogs"); + + Bool only_media = @slon_api_get_value_as_boolean(request_json->@("only_media", TRUE)); + Bool exclude_replies = @slon_api_get_value_as_boolean(request_json->@("exclude_replies", 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)); + no_warn exclude_reblogs; - Bool pinned = request_json->@("pinned"); // FIXME: Implement "only_media", "exclude_reblogs", "tagged" Bool exclude_status = FALSE; U64 status_id = 0; @@ -95,7 +75,7 @@ U0 @slon_api_v1_statuses_query(SlonHttpSession* session, JsonArray* status_array if (only_media && !Json.Get(status, "media_attachments")(JsonArray*)->length) { exclude_status = TRUE; } - if (exclude_replies && StrLen(status->@("in_reply_to_account_id")) > 0 && StrICmp(account_id, status->@("in_reply_to_account_id"))) { + if (exclude_replies && StrLen(status->@("in_reply_to_acct_id")) > 0 && StrICmp(account_id, status->@("in_reply_to_acct_id"))) { exclude_status = TRUE; } if (pinned && !status->@("pinned")) { @@ -192,7 +172,6 @@ U0 @slon_api_v1_statuses_get(SlonHttpSession* session) JsonObject* status = NULL; if (@slon_api_authorized(session)) { - SLON_AUTH_ACCOUNT_ID if (session->path_count() > 4 && !StrICmp("context", session->path(4))) { JsonObject* context = Json.CreateObject(); @@ -200,10 +179,9 @@ U0 @slon_api_v1_statuses_get(SlonHttpSession* session) // Get ancestors id = session->path(3); - status = @slon_api_find_status_by_id(id, account_id); + status = @slon_api_find_status_by_id(id, NULL); while (status && status->@("in_reply_to_id")) { - id = status->@("in_reply_to_id"); - status = @slon_api_find_status_by_id(id, account_id); + status = @slon_api_find_status_by_id(status->@("in_reply_to_id"), status->@("in_reply_to_acct_id")); if (status) { context->a("ancestors")->append(Json.CreateItem(status, JSON_OBJECT)); } @@ -211,13 +189,13 @@ U0 @slon_api_v1_statuses_get(SlonHttpSession* session) // Get descendants id = session->path(3); - context->set("descendants", @slon_api_v1_statuses_find_descendants_by_id(id, account_id), JSON_ARRAY); + context->set("descendants", @slon_api_v1_statuses_find_descendants_by_id(id), JSON_ARRAY); session->send(context); return; } - status = @slon_api_find_status_by_id(id, account_id); + status = @slon_api_find_status_by_id(id, NULL); if (status) { session->send(status); return; @@ -240,6 +218,15 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session) if (@slon_api_authorized(session)) { SLON_AUTH_ACCOUNT_ID + U8* id = NULL; + + if (session->path_count() > 4) { + // FIXME: Do stuff + AdamLog("session->path_count is : %d\n", session->path_count()); + session->status(404); + return; + } + Bool idempotency_key_already_seen = FALSE; U8* idempotency_key = session->header("idempotency-key"); if (StrLen(idempotency_key) > 0 && db->o("idempotency_keys")->@(idempotency_key)) { @@ -249,7 +236,7 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session) Json.Set(db->o("idempotency_keys"), idempotency_key, Now, JSON_NUMBER); } - U8* id = @slon_api_generate_unique_id(session); + id = @slon_api_generate_unique_id(session); U8* created_at = @slon_api_timestamp_from_cdate(session, Now); JsonObject* app_object = db->o("apps")->@(Json.Get(session->auth, "client_id")); @@ -285,7 +272,9 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session) // IceCubesApp lets us post with +: media_attachments, replies_count, spoiler_text, sensitive JsonObject* status = Json.CreateObject(); + JsonObject* reply_to_status = NULL; JsonArray* media_attachments = NULL; + String.Trim(request_json->@("status")); status->set("id", id, JSON_STRING); status->set("created_at", created_at, JSON_STRING); status->set("content", request_json->@("status"), JSON_STRING); @@ -318,15 +307,14 @@ U0 @slon_api_v1_statuses_post(SlonHttpSession* session) if (StrLen(in_reply_to_id) > 0) { status->set("in_reply_to_id", in_reply_to_id, JSON_STRING); + reply_to_status = @slon_api_find_status_by_id(in_reply_to_id); + if (reply_to_status) { + status->set("in_reply_to_acct_id", reply_to_status->o("account")->@("id"), JSON_STRING); + } } if (!idempotency_key_already_seen) { - db->o("statuses")->a(account_id)->append(Json.CreateItem(status, JSON_OBJECT)); - db->o("timelines")->a("public")->append(Json.CreateItem(status, JSON_OBJECT)); - @slon_db_save_statuses_to_disk; - @slon_db_save_timelines_to_disk; - @slon_db_instance_increment_status_count; - @slon_db_save_instance_to_disk; + @slon_api_create_status(status, account_id); if (@slon_api_status_create_fedi) { @slon_api_status_create_fedi(Json.Clone(status)); } diff --git a/Slon/Api/V1/Timelines.HC b/Slon/Api/V1/Timelines.HC index e3c7df7..11dea8d 100644 --- a/Slon/Api/V1/Timelines.HC +++ b/Slon/Api/V1/Timelines.HC @@ -1,27 +1,13 @@ U0 @slon_api_v1_timelines_home(SlonHttpSession* session, U8* account_id) { // Return the Account's Home timeline - JsonArray* status_array = db->o("timelines")->o("home")->a(account_id); - if (!status_array) { - session->send(SLON_EMPTY_JSON_ARRAY); - return; - } - @slon_api_v1_statuses_query(session, status_array); + @slon_api_v1_statuses_query(session, @slon_api_status_array_from_timeline(db->o("timelines")->o("home")->a(account_id))); } U0 @slon_api_v1_timelines_public(SlonHttpSession* session) { - SLON_SCRATCH_BUFFER_AND_REQUEST_JSON - no_warn scratch_buffer; - // Return the Public timeline - JsonArray* status_array = db->o("timelines")->a("public"); - if (!status_array) { - session->send(SLON_EMPTY_JSON_ARRAY); - return; - } - request_json->unset("exclude_replies"); - @slon_api_v1_statuses_query(session, status_array); + @slon_api_v1_statuses_query(session, @slon_api_status_array_from_timeline(db->o("timelines")->a("public"))); } U0 @slon_api_v1_timelines_get(SlonHttpSession* session) diff --git a/Slon/Modules/ActivityPub.HC b/Slon/Modules/ActivityPub.HC index ea8e277..f988aec 100644 --- a/Slon/Modules/ActivityPub.HC +++ b/Slon/Modules/ActivityPub.HC @@ -441,6 +441,7 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest) StrFind("/statuses/", this_actor)[0] = NULL; JsonObject* create_object = Json.CreateObject(); + JsonObject* reply_to_status = NULL; create_object->set("@context", "https://www.w3.org/ns/activitystreams", JSON_STRING); StrPrint(scratch_buffer, "%s/activity", status->@("uri")); @@ -465,16 +466,10 @@ U0 @slon_activitypub_async_create_status_to(JsonObject* status, U8* dest) note_object->set("sensitive", status->@("sensitive"), JSON_BOOLEAN); note_object->set("atomUri", status->@("uri"), JSON_STRING); if (status->@("in_reply_to_id")) { - // lookup status uri in user's home timeline - JsonArray* lookup_array = db->o("timelines")->o("home")->a(status->o("account")->@("id")); - if (lookup_array) { - for (i = 0; i < lookup_array->length; i++) { - if (!StrICmp(status->@("in_reply_to_id"), lookup_array->o(i)->@("id"))) { - note_object->set("inReplyTo", lookup_array->o(i)->@("uri"), JSON_STRING); - note_object->set("inReplyToAtomUri", lookup_array->o(i)->@("uri"), JSON_STRING); - break; - } - } + reply_to_status = @slon_api_find_status_by_id(status->@("in_reply_to_id"), status->@("in_reply_to_acct_id")); + if (reply_to_status) { + note_object->set("inReplyTo", reply_to_status->@("uri"), JSON_STRING); + note_object->set("inReplyToAtomUri", reply_to_status->@("uri"), JSON_STRING); } } else { note_object->set("inReplyTo", NULL, JSON_NULL); @@ -651,8 +646,9 @@ JsonObject* @slon_activitypub_get_account_for_remote_actor(SlonHttpSession* sess return account; } -U8* @slon_activitypub_status_id_by_uri(U8* uri, JsonArray* statuses) +JsonObject* @slon_activitypub_status_by_uri(U8* uri, JsonArray* timeline) { + JsonArray* statuses = @slon_api_status_array_from_timeline(timeline); if (!uri || !statuses) { return NULL; } @@ -661,7 +657,7 @@ U8* @slon_activitypub_status_id_by_uri(U8* uri, JsonArray* statuses) for (i = 0; i < statuses->length; i++) { status = statuses->@(i); if (status->@("uri") && !StrICmp(status->@("uri"), uri)) { - return status->@("id"); + return status; } } return NULL; @@ -682,6 +678,26 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user) JsonObject* status = NULL; JsonObject* request_object = NULL; + U8* status_id = NULL; + + if (!StrICmp("announce", request_json->@("type"))) { + if (StrICmp(session->actor_for_key_id, request_json->@("actor"))) { + session->status(401); + return; + } + status_id = StrFind("/", StrFind("/statuses/", request_json->@("object")) + 1) + 1; + statuses = db->o("statuses")->a(account->@("id")); + for (i = 0; i < statuses->length; i++) { + status = statuses->@(i); + if (!StrICmp(status_id, status->@("id"))) { + // TODO: https://docs.joinmastodon.org/methods/statuses/#reblogged_by + status->set("reblog_count", status->@("reblog_count") + 1); + break; + } + } + @slon_db_save_statuses_to_disk; + request_object = Json.Clone(request_json); + } if (!StrICmp("follow", request_json->@("type"))) { if (StrICmp(session->actor_for_key_id, request_json->@("actor"))) { @@ -780,15 +796,14 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user) return; } - if (db->o("timelines")->o("home")->a(account->@("id"))) { - if (@slon_activitypub_status_exists(db->o("timelines")->o("home")->a(account->@("id")), request_json->o("object")->@("atomUri"))) { + JsonObject* remote_account = @slon_activitypub_get_account_for_remote_actor(session); + if (db->o("statuses")->a(remote_account->@("id"))) { + if (@slon_activitypub_status_exists(db->o("statuses")->a(remote_account->@("id")), request_json->o("object")->@("atomUri"))) { session->status(200); return; } } - JsonObject* remote_account = @slon_activitypub_get_account_for_remote_actor(session); - JsonObject* new_status = Json.CreateObject(); U8* id = @slon_api_generate_unique_id(session); @@ -830,9 +845,10 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user) } if (request_json->o("object")->@("inReplyTo") || request_json->o("object")->@("inReplyToAtomUri")) { - U8* reply_to_post_id = @slon_activitypub_status_id_by_uri(request_json->o("object")->@("inReplyTo"), db->o("timelines")->o("home")->a(account->@("id"))); - if (reply_to_post_id) { - new_status->set("in_reply_to_id", reply_to_post_id, JSON_STRING); + JsonObject* reply_to_post = @slon_activitypub_status_by_uri(request_json->o("object")->@("inReplyTo"), db->o("timelines")->o("home")->a(account->@("id"))); + if (reply_to_post) { + new_status->set("in_reply_to_id", reply_to_post->@("id"), JSON_STRING); + new_status->set("in_reply_to_acct_id", reply_to_post->o("account")->@("id"), JSON_STRING); } } @@ -854,12 +870,8 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user) new_status->set("spoiler_text", "", JSON_STRING); new_status->set("sensitive", request_json->o("object")->@("sensitive"), JSON_BOOLEAN); - if (!db->o("timelines")->o("home")->a(account->@("id"))) { - db->o("timelines")->o("home")->set(account->@("id"), Json.CreateArray(), JSON_ARRAY); - } - db->o("timelines")->o("home")->a(account->@("id"))->append(Json.CreateItem(new_status, JSON_OBJECT)); + @slon_api_create_status(new_status, remote_account->@("id"), user); - @slon_db_save_timelines_to_disk; @slon_free(session, id); request_object = Json.CreateObject(); request_object->set("@context", "https://www.w3.org/ns/activitystreams", JSON_STRING); @@ -874,11 +886,12 @@ U0 @slon_activitypub_users_inbox(SlonHttpSession* session, U8* user) session->status(401); return; } - U8* status_id = StrFind("/", StrFind("/statuses/", request_json->@("object")) + 1) + 1; + status_id = StrFind("/", StrFind("/statuses/", request_json->@("object")) + 1) + 1; statuses = db->o("statuses")->a(account->@("id")); for (i = 0; i < statuses->length; i++) { status = statuses->@(i); if (!StrICmp(status_id, status->@("id"))) { + // TODO: https://docs.joinmastodon.org/methods/statuses/#favourited_by status->set("favourites_count", status->@("favourites_count") + 1); break; } diff --git a/Slon/Modules/Api.HC b/Slon/Modules/Api.HC index 1869f6f..87422c2 100644 --- a/Slon/Modules/Api.HC +++ b/Slon/Modules/Api.HC @@ -308,36 +308,134 @@ JsonObject* @slon_api_status_lookup_by_id(U8* id, JsonArray* statuses) JsonObject* status; for (i = 0; i < statuses->length; i++) { status = statuses->@(i); - if (status->@("id") && !StrICmp(status->@("id"), id)) { + if (!status->@("deleted") && status->@("id") && !StrICmp(status->@("id"), id)) { return status; } } return NULL; } -JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id) +JsonObject* @slon_api_status_lookup_by_in_reply_to_id(U8* id, JsonArray* statuses) { - if (!id) { + if (!id || !statuses) { return NULL; } - JsonObject* status = NULL; - // Lookup in public timeline - status = @slon_api_status_lookup_by_id(id, db->o("timelines")->a("public")); - if (status) { - return status; - } - if (!account_id) { - return NULL; - } - // Then, lookup in home timeline - status = @slon_api_status_lookup_by_id(id, db->o("timelines")->o("home")->a(account_id)); - if (status) { - return status; - } - // Finally, lookup in account's statuses - status = @slon_api_status_lookup_by_id(id, db->o("statuses")->a(account_id)); - if (status) { - return status; + I64 i; + JsonObject* status; + for (i = 0; i < statuses->length; i++) { + status = statuses->@(i); + if (!status->@("deleted") && status->@("in_reply_to_id") && !StrICmp(status->@("in_reply_to_id"), id)) { + return status; + } } return NULL; } + +JsonObject* @slon_api_find_status_by_id(U8* id, U8* account_id = NULL) +{ + if (account_id) { + return @slon_api_status_lookup_by_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_id(id, key->value); + if (status) { + return status; + } + key = key->next; + } + return NULL; +} + +U0 @slon_api_create_status(JsonObject* status, U8* account_id, U8* to_ap_user = NULL) +{ + if (!status || !account_id) { + return; + } + if (!db->o("statuses")->a(account_id)) { + db->o("statuses")->set(account_id, Json.CreateArray(), JSON_ARRAY); + } + db->o("statuses")->a(account_id)->append(Json.CreateItem(status, JSON_OBJECT)); + @slon_db_save_statuses_to_disk; + @slon_db_instance_increment_status_count; + @slon_db_save_instance_to_disk; + + JsonObject* status_item = Json.CreateObject(); + status_item->set("account_id", account_id, JSON_STRING); + status_item->set("status_id", status->@("id"), JSON_STRING); + + // If account_id is a local account, publish to public timeline + JsonObject* acct = @slon_api_account_by_id(account_id); + if (!acct->@("remote_actor") && !StrICmp("public", status->@("visibility"))) { + if (!db->o("timelines")->a("public")) { + db->o("timelines")->set("public", Json.CreateArray(), JSON_ARRAY); + } + db->o("timelines")->a("public")->append(Json.CreateItem(status_item, JSON_OBJECT)); + } + // If account_id is a remote account, and we have an ActivityPub user, post to their timeline + if (acct->@("remote_actor") && to_ap_user) { + JsonObject* acct_for_ap_user = @slon_api_account_by_username(to_ap_user); + if (acct_for_ap_user) { + if (!db->o("timelines")->o("home")->a(acct_for_ap_user->@("id"))) { + db->o("timelines")->o("home")->set(acct_for_ap_user->@("id"), Json.CreateArray(), JSON_ARRAY); + } + db->o("timelines")->o("home")->a(acct_for_ap_user->@("id"))->append(Json.CreateItem(status_item, JSON_OBJECT)); + } + } + @slon_db_save_timelines_to_disk; +} + +JsonObject* @slon_api_get_timeline_item(JsonObject* timeline_item) +{ + if (!timeline_item) { + return NULL; + } + JsonArray* statuses = db->o("statuses")->a(timeline_item->@("account_id")); + JsonObject* status = NULL; + if (!statuses) { + return NULL; + } + I64 i; + for (i = 0; i < statuses->length; i++) { + status = statuses->@(i); + if (!status->@("deleted") && !StrICmp(status->@("id"), timeline_item->@("status_id"))) { + return status; + } + } + return NULL; +} + +JsonArray* @slon_api_status_array_from_timeline(JsonArray* timeline) +{ + if (!timeline) { + return NULL; + } + JsonArray* status_array = Json.CreateArray(); + JsonObject* timeline_item = NULL; + JsonObject* status = NULL; + I64 i; + for (i = 0; i < timeline->length; i++) { + timeline_item = timeline->@(i); + status = @slon_api_get_timeline_item(timeline_item); + if (status) { + status_array->append(Json.CreateItem(status, JSON_OBJECT)); + } + } + return status_array; +} + +Bool @slon_api_get_value_as_boolean(JsonKey* key) +{ + if (!key) { + return FALSE; + } + switch (key->type) { + case JSON_STRING: + return key->value && !StrICmp("true", key->value); + case JSON_BOOLEAN: + return key->value; + default: + return FALSE; + } +}