From 0994ab38870b52b45cbd349a82e9e900ba464bad Mon Sep 17 00:00:00 2001 From: Alec Murphy Date: Sun, 16 Feb 2025 20:34:42 -0500 Subject: [PATCH] Slon/Api/V1/Timelines: Add initial support for Home timeline --- Slon/Api/V1/Timelines.HC | 130 +++++++++++++++++++++++++++++++++++++-- Slon/Modules/Db.HC | 39 ++++++++++++ 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/Slon/Api/V1/Timelines.HC b/Slon/Api/V1/Timelines.HC index 6415444..d29120b 100644 --- a/Slon/Api/V1/Timelines.HC +++ b/Slon/Api/V1/Timelines.HC @@ -1,12 +1,132 @@ +U0 @slon_api_v1_timelines_home(SlonHttpSession* session, U8* account_id) +{ + SLON_SCRATCH_BUFFER_AND_REQUEST_JSON + no_warn scratch_buffer; + + // Return the Account's Home timeline + JsonArray* status_array = db->o("timelines")->o("home")->a(account_id); + if (!status_array) { + @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + return; + } + + I64 count = 0; + + // FILTERS + 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"); + no_warn exclude_reblogs; + Bool pinned = request_json->@("pinned"); + // FIXME: Implement "only_media", "exclude_reblogs", "tagged" + + Bool exclude_status = FALSE; + U64 status_id = 0; + + if (StrLen(request_json->@("limit")) > 0) { + // 40 = maximum per https://docs.joinmastodon.org/methods/accounts/#statuses + limit = MinI64(40, Str2I64(request_json->@("limit"))); + } + if (StrLen(request_json->@("max_id")) > 0) { + max_id = Str2I64(request_json->@("max_id")); + } + if (StrLen(request_json->@("min_id")) > 0) { + min_id = Str2I64(request_json->@("min_id")); + } + + JsonArray* statuses = Json.CreateArray(); + JsonObject* status = NULL; + + if (status_array && status_array->length) { + I64 i; + for (i = 0; i < status_array->length; i++) { + status = status_array->o(i); + status_id = Str2I64(status->@("id")); + exclude_status = FALSE; + if (status->@("deleted")) { + exclude_status = TRUE; + } + if (max_id > 0 && status_id >= max_id) { + exclude_status = TRUE; + } + if (min_id > 0 && status_id <= min_id) { + exclude_status = TRUE; + } + 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"))) { + exclude_status = TRUE; + } + if (pinned && !status->@("pinned")) { + exclude_status = TRUE; + } + if (!exclude_status) { + statuses->append(Json.CreateItem(status, JSON_OBJECT)); + count++; + } + if (limit > 0 && count >= limit) { + break; + } + } + } + + @slon_http_send_json(session, statuses); + + Json.Delete(statuses); +} + U0 @slon_api_v1_timelines_get(SlonHttpSession* session) { - // SLON_SCRATCH_BUFFER_AND_REQUEST_JSON - if (@slon_api_authorized(session)) { - // SLON_AUTH_ACCOUNT_ID - // FIXME: Implement this - @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + SLON_AUTH_ACCOUNT_ID + + U8* path = @slon_strnew(session, @slon_http_request_path(session)); + I64 path_segments_count = 0; + U8** path_segments = String.Split(path, '/', &path_segments_count); + + if (path_segments_count < 4) { + goto slon_api_v1_timelines_get_return; + } + + if (!StrICmp("public", path_segments[3])) { + // FIXME: Implement this + @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + goto slon_api_v1_timelines_get_return; + } + + if (!StrICmp("tag", path_segments[3])) { + // FIXME: Implement this + @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + goto slon_api_v1_timelines_get_return; + } + + if (!StrICmp("home", path_segments[3])) { + // FIXME: Implement this + @slon_api_v1_timelines_home(session, account_id); + goto slon_api_v1_timelines_get_return; + } + + if (!StrICmp("link", path_segments[3])) { + // FIXME: Implement this + @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + goto slon_api_v1_timelines_get_return; + } + + if (!StrICmp("list", path_segments[3])) { + // FIXME: Implement this + @slon_http_send_json(session, SLON_EMPTY_JSON_ARRAY); + goto slon_api_v1_timelines_get_return; + } + + @slon_http_set_status_code(session, 404); } else { @slon_http_set_status_code(session, 401); } +slon_api_v1_timelines_get_return: + Free(path_segments); + @slon_free(session, path); } diff --git a/Slon/Modules/Db.HC b/Slon/Modules/Db.HC index 8d2d0c7..f8c29bb 100644 --- a/Slon/Modules/Db.HC +++ b/Slon/Modules/Db.HC @@ -84,6 +84,30 @@ U0 @slon_db_load_statuses_from_disk() db->set("statuses", statuses, JSON_OBJECT); } +U0 @slon_db_load_timelines_from_disk() +{ + JsonObject* timelines = Json.CreateObject(); + + JsonObject* home_statuses = Json.CreateObject(); + U8 scratch_buffer[256]; + StrPrint(scratch_buffer, "%s/timelines/home/*.json", SLON_DB_PATH); + CDirEntry* files = FilesFind(scratch_buffer); + CDirEntry* de = files; + JsonArray* status_array = NULL; + while (de) { + status_array = Json.ParseFile(de->full_name); + if (status_array) { + StrFind(".json", de->name)[0] = NULL; + home_statuses->set(de->name, status_array, JSON_ARRAY); + } + de = de->next; + } + DirTreeDel(files); + timelines->set("home", home_statuses, JSON_OBJECT); + + db->set("timelines", timelines, JSON_OBJECT); +} + U0 @slon_db_save_accounts_to_disk() { U8 scratch_buffer[256]; @@ -148,6 +172,17 @@ U0 @slon_db_save_statuses_to_disk() } } +U0 @slon_db_save_timelines_to_disk() +{ + U8 scratch_buffer[256]; + JsonKey* key = db->o("timelines")->o("home")->keys; + while (key) { + StrPrint(scratch_buffer, "%s/timelines/home/%s.json", SLON_DB_PATH, key->name); + Json.DumpToFile(scratch_buffer, key->value); + key = key->next; + } +} + U0 @slon_db_save_to_disk() { @slon_db_save_accounts_to_disk(); @@ -158,6 +193,7 @@ U0 @slon_db_save_to_disk() @slon_db_save_oauth_to_disk(); @slon_db_save_private_keys_to_disk(); @slon_db_save_statuses_to_disk(); + @slon_db_save_timelines_to_disk(); } U0 @slon_db_load_from_defaults() @@ -172,6 +208,8 @@ U0 @slon_db_load_from_defaults() db->set("followers", Json.CreateObject(), JSON_OBJECT); db->set("instance", Json.ParseFile("M:/Slon/Static/defaults/instance.json"), JSON_OBJECT); db->set("statuses", Json.CreateObject(), JSON_OBJECT); + db->set("timelines", Json.CreateObject(), JSON_OBJECT); + db->o("timelines")->set("home", Json.CreateObject(), JSON_OBJECT); JsonObject* oauth = Json.CreateObject(); oauth->set("codes", Json.CreateObject(), JSON_OBJECT); oauth->set("requests", Json.CreateObject(), JSON_OBJECT); @@ -194,6 +232,7 @@ U0 @slon_db_load_from_disk() @slon_db_load_instance_from_disk(); @slon_db_load_oauth_from_disk(); @slon_db_load_statuses_from_disk(); + @slon_db_load_timelines_from_disk(); db->set("setup", TRUE, JSON_BOOLEAN); }