diff --git a/docs/agent.md b/docs/agent.md index 0be1a2d..93f8e4f 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -188,18 +188,18 @@ All of these return **raw JSON** in a `wf_response`; free with ```c // Home timeline. wf_response tl = {0}; -wf_agent_get_timeline(agent, 50, NULL, &tl); // needs network +wf_agent_get_timeline(agent, 50, NULL, NULL, &tl); // needs network /* tl.body is the JSON; tl.body_len its length */ wf_response_free(&tl); // A specific author's feed. wf_response af = {0}; -wf_agent_get_author_feed(agent, "alice.example.com", 50, NULL, NULL, &af); // needs network +wf_agent_get_author_feed(agent, "alice.example.com", 50, NULL, NULL, false, &af); // needs network wf_response_free(&af); // Post thread. wf_response th = {0}; -wf_agent_get_post_thread(agent, "at://did:plc:abc/app.bsky.feed.post/xyz", 6, &th); // needs network +wf_agent_get_post_thread(agent, "at://did:plc:abc/app.bsky.feed.post/xyz", 6, 80, &th); // needs network wf_response_free(&th); // Fetch several posts at once by AT-URI. diff --git a/docs/agent_usage.md b/docs/agent_usage.md index 7b2a304..d435a1d 100644 --- a/docs/agent_usage.md +++ b/docs/agent_usage.md @@ -274,7 +274,7 @@ Feed and graph endpoints return **raw JSON** in a `wf_response`; free it with ```c wf_response res = {0}; -wf_status s = wf_agent_get_timeline(agent, 50, NULL, &res); +wf_status s = wf_agent_get_timeline(agent, 50, NULL, NULL /* algorithm */, &res); if (s == WF_OK) { /* res.body is a NUL-terminated JSON string, res.body_len its length */ printf("timeline: %s\n", res.body); @@ -283,7 +283,7 @@ wf_response_free(&res); /* Get a full post thread */ wf_response thread = {0}; -s = wf_agent_get_post_thread(agent, post_uri, 6 /* depth */, &thread); +s = wf_agent_get_post_thread(agent, post_uri, 6 /* depth */, 80 /* parentHeight */, &thread); if (s == WF_OK) { printf("thread: %s\n", thread.body); } diff --git a/examples/thread.c b/examples/thread.c index 949b862..c2d9c8f 100644 --- a/examples/thread.c +++ b/examples/thread.c @@ -60,7 +60,7 @@ int main(int argc, char **argv) { } wf_response res = {0}; - s = wf_agent_get_post_thread(agent, uri, 0, &res); + s = wf_agent_get_post_thread(agent, uri, 0, 0, &res); if (s != WF_OK && s != WF_ERR_HTTP) { fprintf(stderr, "getPostThread failed: %d\n", (int)s); wf_response_free(&res); diff --git a/include/wolfram/agent.h b/include/wolfram/agent.h index 959e58c..4d98fd3 100644 --- a/include/wolfram/agent.h +++ b/include/wolfram/agent.h @@ -162,16 +162,17 @@ wf_status wf_agent_repost(wf_agent *agent, const char *post_uri, const char *pos wf_status wf_agent_delete_repost(wf_agent *agent, const char *repost_uri); /* Feed queries — return raw JSON in `out`; caller frees with wf_response_free. */ -wf_status wf_agent_get_timeline(wf_agent *agent, int limit, const char *cursor, wf_response *out); +wf_status wf_agent_get_timeline(wf_agent *agent, int limit, const char *cursor, + const char *algorithm, wf_response *out); wf_status wf_agent_get_timeline_lex(wf_agent *agent, int limit, const char *cursor, wf_response *out); wf_status wf_agent_get_author_feed(wf_agent *agent, const char *actor, int limit, const char *cursor, const char *filter, - wf_response *out); + bool include_pins, wf_response *out); wf_status wf_agent_get_author_feed_lex(wf_agent *agent, const char *actor, int limit, const char *cursor, const char *filter, wf_response *out); wf_status wf_agent_get_post_thread(wf_agent *agent, const char *uri, int depth, - wf_response *out); + int parent_height, wf_response *out); wf_status wf_agent_get_posts(wf_agent *agent, const char *const *uris, size_t uri_count, wf_response *out); wf_status wf_agent_search_posts(wf_agent *agent, const char *query, diff --git a/include/wolfram/feedgen_typed.h b/include/wolfram/feedgen_typed.h index b58ede6..869d46d 100644 --- a/include/wolfram/feedgen_typed.h +++ b/include/wolfram/feedgen_typed.h @@ -2,9 +2,11 @@ * feedgen_typed.h — owned typed parser for feed-generator endpoints. * * Parses the raw JSON bodies returned by: - * - app.bsky.feed.getActorFeeds -> `feeds` array of generatorView - * - app.bsky.feed.getFeed -> `feed` array of feedView - * - app.bsky.feed.getActorLikes -> `feed` array of feedViewPost (reuses + * - app.bsky.feed.getActorFeeds -> `feeds` array of generatorView + * - app.bsky.feed.getFeedGenerator -> single generatorView + isOnline/isValid + * - app.bsky.feed.getFeed -> `feed` array of feedViewPost (reuses + * wf_agent_parse_feed from feed_typed) + * - app.bsky.feed.getActorLikes -> `feed` array of feedViewPost (reuses * wf_agent_parse_feed from feed_typed) * * into owned C structs. Ownership rules mirror notification.c / feed_typed.c / @@ -22,8 +24,12 @@ extern "C" { #endif -/* Forward declaration: wf_agent_feed_list is defined in feed_typed.h and used - * here only as a pointer parameter (getActorLikes reuses the feed parser). */ +/* Forward declarations of the feed_typed.h feedViewPost structs (full + * definitions live in feed_typed.h). getFeed returns feedViewPost items — the + * SAME shape as getTimeline/getAuthorFeed — so its item/list types are aliases + * for the shared feed_typed structs. Only pointer usage appears in this header, + * so forward declarations of the tagged structs are sufficient and avoid the + * agent.h <-> feed_typed.h include cycle. */ typedef struct wf_agent_feed_list wf_agent_feed_list; /* A parsed feed-generator view (app.bsky.feed.defs#generatorView). */ @@ -47,23 +53,23 @@ typedef struct wf_agent_generator_view_list { char *cursor; } wf_agent_generator_view_list; -/* A parsed custom-feed view (app.bsky.feed.defs#feedView). */ -typedef struct wf_agent_feed_view { - char *uri; - char *cid; - char *did; - char *display_name; - char *description; - char *avatar; - char *indexed_at; -} wf_agent_feed_view; +/* A single feed-generator record with online/valid status + * (app.bsky.feed.getFeedGenerator returns {view, isOnline, isValid}). */ +typedef struct wf_agent_generator_detail { + wf_agent_generator_view view; /* owned generatorView */ + int is_online; + int is_valid; +} wf_agent_generator_detail; -/* A parsed list of feed views (app.bsky.feed.getFeed). */ -typedef struct wf_agent_feed_view_list { - wf_agent_feed_view *feeds; - size_t feed_count; - char *cursor; -} wf_agent_feed_view_list; +/* An item returned by app.bsky.feed.getFeed. getFeed returns feedViewPost + * items (post/reason/reply/feedContext/reqId) — the SAME shape used by + * getTimeline/getAuthorFeed — so this is an alias for the feed_typed structs + * and is parsed with the shared feedViewPost parser (wf_agent_parse_feed). */ +typedef struct wf_agent_feed_item wf_agent_feed_view; + +/* A parsed list of feedViewPost items (app.bsky.feed.getFeed). Alias for the + * timeline/author-feed list type so the two stay consistent. */ +typedef struct wf_agent_feed_list wf_agent_feed_view_list; /* Parse a raw getActorFeeds JSON body into owned generator views. * Returns WF_ERR_INVALID_ARG on NULL inputs, WF_ERR_PARSE on malformed JSON @@ -75,9 +81,18 @@ wf_status wf_agent_parse_generators(const char *json, size_t json_len, /* Free a parsed generator-view list and every owned field it holds. */ void wf_agent_generator_view_list_free(wf_agent_generator_view_list *list); -/* Parse a raw getFeed JSON body into owned feed views. +/* Parse a raw getFeedGenerator JSON body ({view, isOnline, isValid}) into an + * owned generatorView detail. Same ownership/error rules as the parsers above, + * reusing the generatorView parser for `view`. */ +wf_status wf_agent_parse_feed_generator(const char *json, size_t json_len, + wf_agent_generator_detail *out); + +/* Free a parsed generator detail and every owned field it holds. */ +void wf_agent_generator_detail_free(wf_agent_generator_detail *out); + +/* Parse a raw getFeed JSON body into owned feedViewPost items. * Same ownership/error rules as wf_agent_parse_generators, operating on the - * `feed` array of feedView. */ + * `feed` array of feedViewPost (delegates to wf_agent_parse_feed). */ wf_status wf_agent_parse_feed_views(const char *json, size_t json_len, wf_agent_feed_view_list *out); @@ -97,6 +112,12 @@ wf_status wf_agent_get_actor_likes_typed(wf_agent *agent, const char *actor, int limit, const char *cursor, wf_agent_feed_list *out); +/* getFeedGenerator (returns a single generatorView + isOnline + isValid). This + * is SEPARATE from getFeed (which returns feedViewPost items). */ +wf_status wf_agent_get_feed_generator_typed(wf_agent *agent, + const char *feed_uri, + wf_agent_generator_detail *out); + #ifdef __cplusplus } #endif diff --git a/src/agent/feed.c b/src/agent/feed.c index 5410490..5a86f01 100644 --- a/src/agent/feed.c +++ b/src/agent/feed.c @@ -30,7 +30,7 @@ static int wf_feed_author_feed_filter_is_valid(const char *filter) { } wf_status wf_agent_get_timeline(wf_agent *agent, int limit, const char *cursor, - wf_response *out) { + const char *algorithm, wf_response *out) { if (!agent || !out) { return WF_ERR_INVALID_ARG; } @@ -42,6 +42,11 @@ wf_status wf_agent_get_timeline(wf_agent *agent, int limit, const char *cursor, size_t param_count = 0; char limit_buf[16]; + if (algorithm && algorithm[0]) { + params[param_count].name = "algorithm"; + params[param_count].value = algorithm; + param_count++; + } if (limit > 0) { if (!wf_agent_int_to_str(limit, limit_buf, sizeof(limit_buf))) { return WF_ERR_INVALID_ARG; @@ -80,7 +85,7 @@ wf_status wf_agent_get_timeline_lex(wf_agent *agent, int limit, const char *curs wf_status wf_agent_get_author_feed(wf_agent *agent, const char *actor, int limit, const char *cursor, const char *filter, - wf_response *out) { + bool include_pins, wf_response *out) { if (!agent || !actor || !out) { return WF_ERR_INVALID_ARG; } @@ -95,7 +100,7 @@ wf_status wf_agent_get_author_feed(wf_agent *agent, const char *actor, return WF_ERR_INVALID_ARG; } - wf_xrpc_param params[4]; + wf_xrpc_param params[5]; size_t param_count = 0; char limit_buf[16]; @@ -121,6 +126,11 @@ wf_status wf_agent_get_author_feed(wf_agent *agent, const char *actor, params[param_count].value = cursor; param_count++; } + if (include_pins) { + params[param_count].name = "includePins"; + params[param_count].value = "true"; + param_count++; + } wf_agent_sync_auth(agent); return wf_xrpc_query_params(agent->client, "app.bsky.feed.getAuthorFeed", @@ -154,10 +164,16 @@ wf_status wf_agent_get_author_feed_lex(wf_agent *agent, const char *actor, } wf_status wf_agent_get_post_thread(wf_agent *agent, const char *uri, int depth, - wf_response *out) { + int parent_height, wf_response *out) { if (!agent || !uri || !out) { return WF_ERR_INVALID_ARG; } + if (depth < 0 || depth > 1000) { + return WF_ERR_INVALID_ARG; + } + if (parent_height < 0 || parent_height > 1000) { + return WF_ERR_INVALID_ARG; + } wf_syntax_aturi parsed = {0}; if (!wf_syntax_aturi_parse(uri, &parsed)) { @@ -165,14 +181,16 @@ wf_status wf_agent_get_post_thread(wf_agent *agent, const char *uri, int depth, } wf_syntax_aturi_free(&parsed); - wf_xrpc_param params[2]; + wf_xrpc_param params[3]; size_t param_count = 0; char depth_buf[16]; + char parent_buf[16]; params[param_count].name = "uri"; params[param_count].value = uri; param_count++; + /* atproto default depth is 6; only send when explicitly requested (>0). */ if (depth > 0) { if (!wf_agent_int_to_str(depth, depth_buf, sizeof(depth_buf))) { return WF_ERR_INVALID_ARG; @@ -182,6 +200,16 @@ wf_status wf_agent_get_post_thread(wf_agent *agent, const char *uri, int depth, param_count++; } + /* atproto default parentHeight is 80; only send when explicitly requested. */ + if (parent_height > 0) { + if (!wf_agent_int_to_str(parent_height, parent_buf, sizeof(parent_buf))) { + return WF_ERR_INVALID_ARG; + } + params[param_count].name = "parentHeight"; + params[param_count].value = parent_buf; + param_count++; + } + wf_agent_sync_auth(agent); return wf_xrpc_query_params(agent->client, "app.bsky.feed.getPostThread", params, param_count, out); diff --git a/src/agent/feed_typed.c b/src/agent/feed_typed.c index a0fe71c..425352a 100644 --- a/src/agent/feed_typed.c +++ b/src/agent/feed_typed.c @@ -356,7 +356,7 @@ wf_status wf_agent_get_timeline_typed(wf_agent *agent, int limit, } wf_response res = {0}; - wf_status status = wf_agent_get_timeline(agent, limit, cursor, &res); + wf_status status = wf_agent_get_timeline(agent, limit, cursor, NULL, &res); if (status != WF_OK) { wf_response_free(&res); return status; @@ -380,7 +380,7 @@ wf_status wf_agent_get_author_feed_typed(wf_agent *agent, const char *actor, wf_response res = {0}; wf_status status = wf_agent_get_author_feed(agent, actor, limit, cursor, - filter, &res); + filter, false, &res); if (status != WF_OK) { wf_response_free(&res); return status; diff --git a/src/agent/feedgen_typed.c b/src/agent/feedgen_typed.c index 8fdca6a..dc5564d 100644 --- a/src/agent/feedgen_typed.c +++ b/src/agent/feedgen_typed.c @@ -78,32 +78,6 @@ static void wf_fg_generator_list_reset(wf_agent_generator_view_list *list) { memset(list, 0, sizeof(*list)); } -static void wf_fg_feed_view_reset(wf_agent_feed_view *f) { - if (!f) { - return; - } - free(f->uri); - free(f->cid); - free(f->did); - free(f->display_name); - free(f->description); - free(f->avatar); - free(f->indexed_at); - memset(f, 0, sizeof(*f)); -} - -static void wf_fg_feed_view_list_reset(wf_agent_feed_view_list *list) { - if (!list) { - return; - } - for (size_t i = 0; i < list->feed_count; ++i) { - wf_fg_feed_view_reset(&list->feeds[i]); - } - free(list->feeds); - free(list->cursor); - memset(list, 0, sizeof(*list)); -} - /* Parse a `likeCount`-style integer, tracking presence. */ static wf_status wf_fg_parse_int(cJSON *num, int *dst, int *has) { if (cJSON_IsNumber(num)) { @@ -144,6 +118,60 @@ static wf_status wf_fg_parse_creator(wf_agent_profile_view *creator, cJSON *obj) return status; } +/* Parse a single generatorView object into an owned wf_agent_generator_view. + * On error `g` is reset. Shared by the list parser and the single-view + * getFeedGenerator parser. */ +static wf_status wf_fg_parse_generator_view(cJSON *obj, + wf_agent_generator_view *g) { + wf_status status = WF_OK; + if (!cJSON_IsObject(obj)) { + return WF_ERR_PARSE; + } + + cJSON *uri = cJSON_GetObjectItemCaseSensitive(obj, "uri"); + cJSON *cid = cJSON_GetObjectItemCaseSensitive(obj, "cid"); + cJSON *did = cJSON_GetObjectItemCaseSensitive(obj, "did"); + cJSON *creator = cJSON_GetObjectItemCaseSensitive(obj, "creator"); + cJSON *name = cJSON_GetObjectItemCaseSensitive(obj, "displayName"); + cJSON *desc = cJSON_GetObjectItemCaseSensitive(obj, "description"); + cJSON *avatar = cJSON_GetObjectItemCaseSensitive(obj, "avatar"); + cJSON *indexed = cJSON_GetObjectItemCaseSensitive(obj, "indexedAt"); + cJSON *like = cJSON_GetObjectItemCaseSensitive(obj, "likeCount"); + + if (cJSON_IsString(uri) && uri->valuestring) { + status = wf_fg_set_string(&g->uri, uri->valuestring); + } + if (status == WF_OK && cJSON_IsString(cid) && cid->valuestring) { + status = wf_fg_set_string(&g->cid, cid->valuestring); + } + if (status == WF_OK && cJSON_IsString(did) && did->valuestring) { + status = wf_fg_set_string(&g->did, did->valuestring); + } + if (status == WF_OK) { + status = wf_fg_parse_creator(&g->creator, creator); + } + if (status == WF_OK && cJSON_IsString(name) && name->valuestring) { + status = wf_fg_set_string(&g->display_name, name->valuestring); + } + if (status == WF_OK && cJSON_IsString(desc) && desc->valuestring) { + status = wf_fg_set_string(&g->description, desc->valuestring); + } + if (status == WF_OK && cJSON_IsString(avatar) && avatar->valuestring) { + status = wf_fg_set_string(&g->avatar, avatar->valuestring); + } + if (status == WF_OK && cJSON_IsString(indexed) && indexed->valuestring) { + status = wf_fg_set_string(&g->indexed_at, indexed->valuestring); + } + if (status == WF_OK) { + status = wf_fg_parse_int(like, &g->like_count, &g->has_like_count); + } + + if (status != WF_OK) { + wf_fg_generator_view_reset(g); + } + return status; +} + wf_status wf_agent_parse_generators(const char *json, size_t json_len, wf_agent_generator_view_list *out) { if (!json || !out) { @@ -181,48 +209,7 @@ wf_status wf_agent_parse_generators(const char *json, size_t json_len, status = WF_ERR_PARSE; break; } - - cJSON *uri = cJSON_GetObjectItemCaseSensitive(obj, "uri"); - cJSON *cid = cJSON_GetObjectItemCaseSensitive(obj, "cid"); - cJSON *did = cJSON_GetObjectItemCaseSensitive(obj, "did"); - cJSON *creator = cJSON_GetObjectItemCaseSensitive(obj, "creator"); - cJSON *name = cJSON_GetObjectItemCaseSensitive(obj, "displayName"); - cJSON *desc = cJSON_GetObjectItemCaseSensitive(obj, "description"); - cJSON *avatar = cJSON_GetObjectItemCaseSensitive(obj, "avatar"); - cJSON *indexed = cJSON_GetObjectItemCaseSensitive(obj, "indexedAt"); - cJSON *like = cJSON_GetObjectItemCaseSensitive(obj, "likeCount"); - - if (cJSON_IsString(uri) && uri->valuestring) { - status = wf_fg_set_string(&g->uri, uri->valuestring); - } - if (status == WF_OK && cJSON_IsString(cid) && cid->valuestring) { - status = wf_fg_set_string(&g->cid, cid->valuestring); - } - if (status == WF_OK && cJSON_IsString(did) && did->valuestring) { - status = wf_fg_set_string(&g->did, did->valuestring); - } - if (status == WF_OK) { - status = wf_fg_parse_creator(&g->creator, creator); - } - if (status == WF_OK && cJSON_IsString(name) && name->valuestring) { - status = wf_fg_set_string(&g->display_name, name->valuestring); - } - if (status == WF_OK && cJSON_IsString(desc) && desc->valuestring) { - status = wf_fg_set_string(&g->description, desc->valuestring); - } - if (status == WF_OK && cJSON_IsString(avatar) && avatar->valuestring) { - status = wf_fg_set_string(&g->avatar, avatar->valuestring); - } - if (status == WF_OK && cJSON_IsString(indexed) && indexed->valuestring) { - status = wf_fg_set_string(&g->indexed_at, indexed->valuestring); - } - if (status == WF_OK) { - status = wf_fg_parse_int(like, &g->like_count, &g->has_like_count); - } - - if (status != WF_OK) { - wf_fg_generator_view_reset(g); - } + status = wf_fg_parse_generator_view(obj, g); } if (status == WF_OK) { @@ -251,8 +238,8 @@ void wf_agent_generator_view_list_free(wf_agent_generator_view_list *list) { wf_fg_generator_list_reset(list); } -wf_status wf_agent_parse_feed_views(const char *json, size_t json_len, - wf_agent_feed_view_list *out) { +wf_status wf_agent_parse_feed_generator(const char *json, size_t json_len, + wf_agent_generator_detail *out) { if (!json || !out) { return WF_ERR_INVALID_ARG; } @@ -265,80 +252,25 @@ wf_status wf_agent_parse_feed_views(const char *json, size_t json_len, } wf_status status = WF_OK; - cJSON *arr = cJSON_GetObjectItemCaseSensitive(root, "feed"); - if (!cJSON_IsArray(arr)) { + cJSON *view = cJSON_GetObjectItemCaseSensitive(root, "view"); + cJSON *is_online = cJSON_GetObjectItemCaseSensitive(root, "isOnline"); + cJSON *is_valid = cJSON_GetObjectItemCaseSensitive(root, "isValid"); + + if (!cJSON_IsObject(view)) { cJSON_Delete(root); return WF_ERR_PARSE; } - size_t count = (size_t)cJSON_GetArraySize(arr); - wf_agent_feed_view *items = NULL; - if (count > 0) { - items = (wf_agent_feed_view *)calloc(count, sizeof(*items)); - if (!items) { - cJSON_Delete(root); - return WF_ERR_ALLOC; - } - } - - for (size_t i = 0; i < count && status == WF_OK; ++i) { - wf_agent_feed_view *f = &items[i]; - cJSON *obj = cJSON_GetArrayItem(arr, (int)i); - if (!cJSON_IsObject(obj)) { - status = WF_ERR_PARSE; - break; - } - - cJSON *uri = cJSON_GetObjectItemCaseSensitive(obj, "uri"); - cJSON *cid = cJSON_GetObjectItemCaseSensitive(obj, "cid"); - cJSON *did = cJSON_GetObjectItemCaseSensitive(obj, "did"); - cJSON *name = cJSON_GetObjectItemCaseSensitive(obj, "displayName"); - cJSON *desc = cJSON_GetObjectItemCaseSensitive(obj, "description"); - cJSON *avatar = cJSON_GetObjectItemCaseSensitive(obj, "avatar"); - cJSON *indexed = cJSON_GetObjectItemCaseSensitive(obj, "indexedAt"); - - if (cJSON_IsString(uri) && uri->valuestring) { - status = wf_fg_set_string(&f->uri, uri->valuestring); - } - if (status == WF_OK && cJSON_IsString(cid) && cid->valuestring) { - status = wf_fg_set_string(&f->cid, cid->valuestring); - } - if (status == WF_OK && cJSON_IsString(did) && did->valuestring) { - status = wf_fg_set_string(&f->did, did->valuestring); - } - if (status == WF_OK && cJSON_IsString(name) && name->valuestring) { - status = wf_fg_set_string(&f->display_name, name->valuestring); - } - if (status == WF_OK && cJSON_IsString(desc) && desc->valuestring) { - status = wf_fg_set_string(&f->description, desc->valuestring); - } - if (status == WF_OK && cJSON_IsString(avatar) && avatar->valuestring) { - status = wf_fg_set_string(&f->avatar, avatar->valuestring); - } - if (status == WF_OK && cJSON_IsString(indexed) && indexed->valuestring) { - status = wf_fg_set_string(&f->indexed_at, indexed->valuestring); - } - - if (status != WF_OK) { - wf_fg_feed_view_reset(f); - } - } - + status = wf_fg_parse_generator_view(view, &out->view); if (status == WF_OK) { - out->feeds = items; - out->feed_count = count; - - cJSON *cursor = cJSON_GetObjectItemCaseSensitive(root, "cursor"); - if (cJSON_IsString(cursor) && cursor->valuestring) { - status = wf_fg_set_string(&out->cursor, cursor->valuestring); + if (cJSON_IsBool(is_online)) { + out->is_online = cJSON_IsTrue(is_online) ? 1 : 0; } - } - - if (status != WF_OK) { - for (size_t i = 0; i < count; ++i) { - wf_fg_feed_view_reset(&items[i]); + if (cJSON_IsBool(is_valid)) { + out->is_valid = cJSON_IsTrue(is_valid) ? 1 : 0; } - free(items); + } else { + wf_fg_generator_view_reset(&out->view); memset(out, 0, sizeof(*out)); } @@ -346,8 +278,23 @@ wf_status wf_agent_parse_feed_views(const char *json, size_t json_len, return status; } +void wf_agent_generator_detail_free(wf_agent_generator_detail *out) { + if (!out) { + return; + } + wf_fg_generator_view_reset(&out->view); + memset(out, 0, sizeof(*out)); +} + +/* getFeed returns feedViewPost items — the SAME shape used by getTimeline / + * getAuthorFeed — so delegate to the shared feedViewPost parser. */ +wf_status wf_agent_parse_feed_views(const char *json, size_t json_len, + wf_agent_feed_view_list *out) { + return wf_agent_parse_feed(json, json_len, out); +} + void wf_agent_feed_view_list_free(wf_agent_feed_view_list *list) { - wf_fg_feed_view_list_reset(list); + wf_agent_feed_list_free(list); } /* ---- Typed high-level wrappers -------------------------------------------- */ @@ -385,7 +332,29 @@ wf_status wf_agent_get_feed_typed(wf_agent *agent, const char *feed_uri, return status; } - status = wf_agent_parse_feed_views(res.body, res.body_len, out); + /* getFeed returns a `feed` array of feedViewPost — the SAME shape as + * getTimeline / getAuthorFeed — so reuse the shared feedViewPost parser. */ + status = wf_agent_parse_feed(res.body, res.body_len, out); + wf_response_free(&res); + return status; +} + +wf_status wf_agent_get_feed_generator_typed(wf_agent *agent, + const char *feed_uri, + wf_agent_generator_detail *out) { + if (!agent || !feed_uri || !out) { + return WF_ERR_INVALID_ARG; + } + + wf_response res = {0}; + wf_status status = wf_agent_get_feed_generator(agent, feed_uri, &res); + if (status != WF_OK) { + wf_response_free(&res); + return status; + } + + /* getFeedGenerator returns {view: generatorView, isOnline, isValid}. */ + status = wf_agent_parse_feed_generator(res.body, res.body_len, out); wf_response_free(&res); return status; } diff --git a/test/fixtures/feed.json b/test/fixtures/feed.json index 1fe9096..b561648 100644 --- a/test/fixtures/feed.json +++ b/test/fixtures/feed.json @@ -1,22 +1,46 @@ { "feed": [ { - "uri": "at://did:plc:gen/app.bsky.feed.generator/feed1", - "cid": "bafyreibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "did": "did:plc:gen", - "displayName": "Tech Feed", - "description": "A feed about technology.", - "avatar": "https://cdn.bsky.app/img/feed/tech", - "indexedAt": "2024-06-01T08:00:00.000Z" + "post": { + "uri": "at://did:plc:alice/app.bsky.feed.post/aaa111", + "cid": "bafyreibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "author": { + "did": "did:plc:alice", + "handle": "alice.bsky.social", + "displayName": "Alice" + }, + "record": { + "$type": "app.bsky.feed.post", + "text": "First post in the custom feed.", + "createdAt": "2024-06-01T08:00:00.000Z" + }, + "indexedAt": "2024-06-01T08:00:01.000Z" + } }, { - "uri": "at://did:plc:gen/app.bsky.feed.generator/feed2", - "cid": "bafyrei11111111111111111111111111111111111111111111111111111111", - "did": "did:plc:gen", - "displayName": "Sports Feed", - "description": "A feed about sports.", - "avatar": "https://cdn.bsky.app/img/feed/sports", - "indexedAt": "2024-06-02T08:00:00.000Z" + "post": { + "uri": "at://did:plc:bob/app.bsky.feed.post/bbb222", + "cid": "bafyrei11111111111111111111111111111111111111111111111111111111", + "author": { + "did": "did:plc:bob", + "handle": "bob.bsky.social", + "displayName": "Bob" + }, + "record": { + "$type": "app.bsky.feed.post", + "text": "Second post, reposted into the feed.", + "createdAt": "2024-06-02T08:00:00.000Z" + }, + "indexedAt": "2024-06-02T08:00:01.000Z" + }, + "reason": { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": { + "did": "did:plc:carol", + "handle": "carol.bsky.social" + }, + "indexedAt": "2024-06-02T09:00:00.000Z" + } } ], "cursor": "cursor-for-feed-views" diff --git a/test/test_feedgen_typed.c b/test/test_feedgen_typed.c index f9f1494..5fc1ec5 100644 --- a/test/test_feedgen_typed.c +++ b/test/test_feedgen_typed.c @@ -135,20 +135,26 @@ int main(void) { free(json); } - /* ---- feed fixture (feedView list) ---- */ + /* ---- feed fixture (feedViewPost list) ---- */ json = load_fixture("feed.json", &len); WF_CHECK(json != NULL); if (json) { s = wf_agent_parse_feed_views(json, len, &views); WF_CHECK(s == WF_OK); - WF_CHECK(views.feed_count == 2); + WF_CHECK(views.item_count == 2); WF_CHECK(views.cursor != NULL); WF_CHECK(strcmp(views.cursor, "cursor-for-feed-views") == 0); - if (views.feed_count == 2) { - WF_CHECK(strcmp(views.feeds[0].display_name, "Tech Feed") == 0); - WF_CHECK(strcmp(views.feeds[0].did, "did:plc:gen") == 0); - WF_CHECK(strcmp(views.feeds[1].display_name, "Sports Feed") == 0); - WF_CHECK(views.feeds[1].avatar != NULL); + if (views.item_count == 2) { + WF_CHECK(views.items[0].post.uri != NULL); + WF_CHECK(strcmp(views.items[0].post.uri, + "at://did:plc:alice/app.bsky.feed.post/aaa111") == 0); + WF_CHECK(views.items[0].post.author.did != NULL); + WF_CHECK(strcmp(views.items[0].post.author.did, "did:plc:alice") == 0); + WF_CHECK(views.items[0].post.record != NULL); + WF_CHECK(views.items[1].post.author.handle != NULL); + WF_CHECK(strcmp(views.items[1].post.author.handle, + "bob.bsky.social") == 0); + WF_CHECK(views.items[1].reason != NULL); } wf_agent_feed_view_list_free(&views); free(json);