diff --git a/docs/agent.md b/docs/agent.md index e65f736..0be1a2d 100644 --- a/docs/agent.md +++ b/docs/agent.md @@ -359,3 +359,144 @@ Other paged wrappers: `wf_agent_get_author_feed_paged`, which underlying endpoint is paged, use the generic `wf_agent_page` with a `wf_agent_page_call_fn` / `wf_agent_page_cb` pair, or extract a cursor from any raw response with `wf_response_cursor`. + +## Function reference + +Quick, self-contained examples for the most-used agent calls. Each snippet +assumes an authenticated `wf_agent` (see +[Lifecycle and session](#lifecycle-and-session)) and shows the call, an +`if (st != WF_OK)` error check, and the matching `_free`. Every call that talks +to a PDS is marked `// needs network`. + +### `wf_agent_new` / `wf_agent_login` + +```c +#include "wolfram/agent.h" +#include + +wf_agent *agent = wf_agent_new("https://bsky.social"); +if (!agent) { /* out of memory */ } + +wf_status st = wf_agent_login(agent, "alice.example.com", "app-password"); // needs network +if (st != WF_OK) { + fprintf(stderr, "login failed: %d\n", (int)st); + wf_agent_free(agent); + return 1; +} + +/* ... use agent ... */ + +wf_agent_free(agent); // always free when done +``` + +### `wf_agent_post` + +```c +wf_agent_post_result post = {0}; +wf_status st = wf_agent_post(agent, "Hello from wolfram! #atproto", &post); // needs network +if (st != WF_OK) { + fprintf(stderr, "post failed: %d\n", (int)st); + return; +} +printf("uri=%s cid=%s\n", post.uri, post.cid); +wf_agent_post_result_free(&post); // frees post.uri and post.cid +``` + +### `wf_agent_get_post_thread_typed` + +```c +#include "wolfram/thread_typed.h" + +wf_agent_thread thread = {0}; +wf_status st = wf_agent_get_post_thread_typed( + agent, "at://did:plc:abc/app.bsky.feed.post/xyz", 6, &thread); // needs network +if (st != WF_OK) { + fprintf(stderr, "thread failed: %d\n", (int)st); + return; +} +if (thread.root.kind == WF_AGENT_THREAD_KIND_POST) { + printf("root: %s\n", thread.root.post.uri); +} +wf_agent_thread_free(&thread); // recursively frees the reply tree +``` + +### `wf_agent_get_timeline_typed` + +```c +#include "wolfram/feed_typed.h" + +wf_agent_feed_list feed = {0}; +wf_status st = wf_agent_get_timeline_typed(agent, 50, NULL, &feed); // needs network +if (st != WF_OK) { + fprintf(stderr, "timeline failed: %d\n", (int)st); + return; +} +for (size_t i = 0; i < feed.item_count; i++) { + printf("%s\n", feed.items[i].post.uri ? feed.items[i].post.uri : "?"); +} +wf_agent_feed_list_free(&feed); +``` + +The raw-JSON sibling `wf_agent_get_timeline` returns the same data in a +`wf_response`; free it with `wf_response_free`. + +### `wf_agent_list_notifications_typed` + +```c +wf_agent_notification_list list = {0}; +wf_status st = wf_agent_list_notifications_typed(agent, 50, NULL, &list); // needs network +if (st != WF_OK) { + fprintf(stderr, "notifications failed: %d\n", (int)st); + return; +} +for (size_t i = 0; i < list.notification_count; i++) { + printf("%s from %s\n", list.notifications[i].reason, + list.notifications[i].author.handle); +} +wf_agent_notification_list_free(&list); +``` + +### `wf_agent_follow` + +```c +wf_agent_post_result follow = {0}; +wf_status st = wf_agent_follow(agent, "did:plc:target", &follow); // needs network +if (st != WF_OK) { + fprintf(stderr, "follow failed: %d\n", (int)st); + return; +} +printf("follow uri=%s\n", follow.uri); +wf_agent_post_result_free(&follow); + +/* Later, to unfollow: wf_agent_unfollow(agent, follow_uri); */ +``` + +### `wf_agent_like` + +```c +wf_agent_post_result like = {0}; +wf_status st = wf_agent_like(agent, + "at://did:plc:abc/app.bsky.feed.post/xyz", "bafyre...cid", &like); // needs network +if (st != WF_OK) { + fprintf(stderr, "like failed: %d\n", (int)st); + return; +} +printf("like uri=%s\n", like.uri); +wf_agent_post_result_free(&like); + +/* Later, to unlike: wf_agent_unlike(agent, like_uri); */ +``` + +### `wf_agent_upload_blob` + +```c +unsigned char *png = /* ... */; size_t png_len = /* ... */; // image bytes +wf_response blob = {0}; +wf_status st = wf_agent_upload_blob(agent, png, png_len, "image/png", &blob); // needs network +if (st != WF_OK) { + fprintf(stderr, "upload failed: %d\n", (int)st); + return; +} +/* blob.body is {"blob":{"$type":"blob","ref":{...},"mimeType":...}} */ +wf_response_free(&blob); +``` diff --git a/docs/oauth.md b/docs/oauth.md index d0edfec..a00f63e 100644 --- a/docs/oauth.md +++ b/docs/oauth.md @@ -282,3 +282,98 @@ wf_oauth_session_state_free(&restored); | `wf_oauth_dpop_key *` | `wf_oauth_dpop_key_free` | | `wf_oauth_session_state` / `wf_oauth_authorization_state` | `wf_oauth_session_state_free` / `wf_oauth_authorization_state_free` | | Strings from `wf_oauth_dpop_proof_create`, `wf_oauth_client_assertion_create`, `wf_oauth_authorization_url_create` | `wf_oauth_string_free` | + +## Function reference + +Concise, self-contained examples for the four OAuth entry points most apps need. +Every networking call is marked `// needs network`. A `wf_xrpc_client` provides +the transport; the DPoP key for the public-client flow is generated for you and +round-tripped through the serialized authorization state. + +### `wf_oauth_discover` (metadata discovery) + +```c +#include "wolfram/oauth.h" +#include "wolfram/xrpc.h" + +wf_xrpc_client *transport = wf_xrpc_client_new("https://bsky.social"); + +wf_oauth_resource_metadata resource = {0}; +wf_oauth_server_metadata server = {0}; +wf_status st = wf_oauth_discover(transport, "https://bsky.social", + &resource, &server); // needs network +if (st != WF_OK) { + fprintf(stderr, "discovery failed: %d\n", (int)st); + wf_xrpc_client_free(transport); + return 1; +} +wf_oauth_resource_metadata_free(&resource); +wf_oauth_server_metadata_free(&server); +wf_xrpc_client_free(transport); +``` + +### `wf_oauth_authorization_begin` (PAR) + +```c +wf_oauth_client_auth client_auth = { + .client_id = client.client_id, + .authorization_server_issuer = server.issuer, + .signing_key = NULL, // public client; DPoP key generated internally + .key_id = NULL, +}; +wf_oauth_authorization_begin_options opts = { + .redirect_uri = "https://my-app.example.com/callback", + .scope = "atproto transition:generic", + .login_hint = NULL, + .app_state = NULL, + .now = time(NULL), + .state_ttl = 600, +}; + +wf_oauth_authorization_begin_result begin = {0}; +wf_status st = wf_oauth_authorization_begin(transport, &server, &client, + &client_auth, &opts, &begin); // needs network +if (st == WF_OK) { + printf("redirect to: %s\n", begin.authorization_url); + /* Persist begin.state_json atomically under begin.state before redirecting. */ +} +wf_oauth_authorization_begin_result_free(&begin); +``` + +### `wf_oauth_authorization_complete` (callback → session) + +```c +wf_oauth_authorization_complete_result done = {0}; +wf_status st = wf_oauth_authorization_complete( + transport, &server, &client, &client_auth, + ¶ms, // populated by wf_oauth_callback_validate + expected_state, // the state you persisted at begin + state_json, state_json_len, + "https://my-app.example.com/callback", + time(NULL), &done); // needs network +if (st == WF_OK && done.error == NULL) { + printf("logged in as %s\n", done.session.subject); + /* Persist done.session_json under done.session.subject. */ +} +wf_oauth_authorization_complete_result_free(&done); +``` + +### `wf_auth_client` (authenticated XRPC) + +```c +wf_auth_client *auth = wf_auth_client_new(transport, &session, &server, &client_auth); + +wf_response resp = {0}; +wf_status st = wf_auth_client_query(auth, "app.bsky.feed.getTimeline", + "limit=50", &resp); // needs network +if (st == WF_OK) { + /* resp.body holds the JSON. */ +} +wf_response_free(&resp); + +st = wf_auth_client_procedure(auth, "com.atproto.repo.createRecord", + "{\"repo\":\"did:plc:me\",...}", &resp); // needs network +wf_response_free(&resp); + +wf_auth_client_free(auth); // does NOT free transport or session +``` diff --git a/docs/sync.md b/docs/sync.md index 67ca0d2..e5147a6 100644 --- a/docs/sync.md +++ b/docs/sync.md @@ -227,3 +227,109 @@ static void on_commit(const wf_subscribe_event *ev, void *ud) { `wf_sync_verify_commit` returns `WF_OK` even when signature verification fails — always check `*out_verified`. A non-`WF_OK` return means parsing or DID resolution failed (not a signature failure). + +## Function reference + +Self-contained examples for the most-used sync calls. All sync endpoints are +unauthenticated reads, so a plain `wf_xrpc_client` is enough — you do not need a +session. Every network call is marked `// needs network`. + +### `wf_sync_get_repo` + +```c +#include "wolfram/sync.h" +#include "wolfram/xrpc.h" + +wf_xrpc_client *client = wf_xrpc_client_new("https://bsky.social"); + +wf_car car = {0}; +// Full export: pass NULL for `since`. Diff: pass a revision TID. +wf_status st = wf_sync_get_repo(client, "did:plc:abc", NULL, &car); // needs network +if (st != WF_OK) { + fprintf(stderr, "getRepo failed: %d\n", (int)st); + wf_xrpc_client_free(client); + return 1; +} +printf("repo has %zu blocks, %zu root(s)\n", car.block_count, car.root_count); +wf_car_free(&car); // always free when done + +wf_xrpc_client_free(client); +``` + +### `wf_sync_get_record` + +```c +wf_car rec = {0}; +wf_status st = wf_sync_get_record(client, "did:plc:abc", + "app.bsky.feed.post", "rkey", &rec); // needs network +if (st != WF_OK) { + fprintf(stderr, "getRecord failed: %d\n", (int)st); + return; +} +/* rec.block_count blocks; rec.roots[0] is the record's CID. */ +wf_car_free(&rec); +``` + +### `wf_agent_sync_get_blob` (agent wrapper) + +```c +#include "wolfram/agent.h" + +wf_response blob = {0}; +wf_status st = wf_agent_sync_get_blob(agent, "did:plc:abc", + "bafyre...cid", &blob); // needs network +if (st != WF_OK) { + fprintf(stderr, "getBlob failed: %d\n", (int)st); + return; +} +/* blob.body holds the blob bytes / JSON. */ +wf_response_free(&blob); +``` + +### Firehose subscription (`wf_subscribe_start`) + +```c +#include "wolfram/sync_subscribe.h" + +static void on_event(const wf_subscribe_event *ev, void *ud) { + (void)ud; + if (ev->type == WF_SUBSCRIBE_EVENT_COMMIT) { + const wf_subscribe_commit *c = &ev->data.commit; + printf("commit seq=%lld did=%s\n", (long long)c->seq, c->did); + } +} + +wf_subscribe_options opts = { + .service = "wss://bsky.network", // needs network + .on_event = on_event, + .on_error = NULL, + .userdata = NULL, +}; + +wf_subscribe_handle *handle = NULL; +wf_subscribe_start(&opts, &handle); // blocks, delivering events + +/* From another thread / signal: wf_subscribe_stop(handle); */ +``` + +### Commit verification (`wf_sync_verify_commit`) + +```c +#include "wolfram/sync_verify.h" + +/* Inside a commit event callback, `commit` is the parsed wf_subscribe_commit. */ +int verified = 0; +wf_commit out = {0}; +wf_status st = wf_sync_verify_commit(&commit, client, &verified, &out); // needs network +if (st != WF_OK) { + fprintf(stderr, "verify parse failed: %d\n", (int)st); + return; +} +if (verified) { + char *cid = wf_cid_to_string(&out.cid); + printf("verified commit %s\n", cid); + free(cid); +} else { + printf("commit FAILED signature verification\n"); +} +``` diff --git a/docs/validate.md b/docs/validate.md index 46ee626..118ecfd 100644 --- a/docs/validate.md +++ b/docs/validate.md @@ -125,3 +125,55 @@ wf_validate_result_free(&r2); Always call `wf_validate_result_free(&res)` when you are done — it releases the entire error chain. The registry is freed separately with `wf_lexicon_registry_free(registry)`. + +## Function reference + +Self-contained examples for the two validation entry points. Both return a +`wf_validate_result` **by value**; always call `wf_validate_result_free` to +release the error chain, and free the registry with +`wf_lexicon_registry_free`. + +### `wf_validate_record` + +```c +#include "wolfram/validate.h" + +wf_lexicon_registry *registry = wf_lexicon_registry_new(); +if (!registry) { /* allocation failure */ } + +/* Load the post lexicon (and facet lexicon if you validate facets). */ +size_t len = 0; +char *post_lex = read_file("lexicons/app/bsky/feed/post.json", &len); +wf_lexicon_registry_load(registry, post_lex, len); +free(post_lex); + +const char *post = "{\"text\":\"hello\",\"createdAt\":\"2024-01-01T00:00:00Z\"}"; +wf_validate_result res = wf_validate_record(registry, "app.bsky.feed.post", + post, strlen(post)); +if (res.success) { + printf("post is valid\n"); +} else { + for (const wf_validate_error *e = res.errors; e; e = e->next) + printf(" at %s: %s\n", e->path ? e->path : "", e->message); +} +wf_validate_result_free(&res); +wf_lexicon_registry_free(registry); +``` + +### `wf_validate_value` + +```c +/* Validate an app.bsky.richtext.facet against its named definition `main`. */ +const char *facet = + "{\"index\":{\"byteStart\":0,\"byteEnd\":4}," + "\"features\":[{\"$type\":\"app.bsky.richtext.facet#link\"," + "\"uri\":\"https://example.com\"}]}"; + +wf_validate_result res = wf_validate_value( + registry, "app.bsky.richtext.facet", "main", facet, strlen(facet)); +if (!res.success) { + for (const wf_validate_error *e = res.errors; e; e = e->next) + printf(" %s: %s\n", e->path ? e->path : "", e->message); +} +wf_validate_result_free(&res); +```