diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ee18f5..7e19e1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ if(POLICY CMP0156) endif() project( wolfram - VERSION 0.12.2 + VERSION 0.13.0 DESCRIPTION "A C/C++ SDK for the AT Protocol" LANGUAGES C CXX) @@ -967,7 +967,7 @@ endif() # already owns the `wolfram` target name; OUTPUT_NAME keeps the binary # `wolfram`. if(NOT WOLFRAM_BUILD_EMBEDDED) - add_executable(wolfram_cli src/cli/main.c src/cli/cli_oauth.c) + add_executable(wolfram_cli src/cli/main.c src/cli/cli_oauth.c src/cli/cli_auth.c src/cli/cli_post.c src/cli/cli_thread.c src/cli/cli_social.c src/cli/cli_search.c src/cli/cli_feed.c src/cli/cli_graph.c src/cli/cli_server.c src/cli/cli_identity.c src/cli/cli_sync.c src/cli/cli_prefs.c src/cli/cli_chat.c src/cli/cli_stream.c src/cli/cli_repo.c src/cli/cli_video.c src/cli/cli_admin.c src/cli/cli_ozone.c) target_link_libraries(wolfram_cli PRIVATE wolfram) target_include_directories(wolfram_cli PRIVATE ${cjson_SOURCE_DIR} ${cjson_BINARY_DIR}) diff --git a/src/cli/cli_admin.c b/src/cli/cli_admin.c new file mode 100644 index 0000000..eb7f062 --- /dev/null +++ b/src/cli/cli_admin.c @@ -0,0 +1,380 @@ +#include "cli_admin.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/admin_typed.h" + +#include +#include +#include +#include +#include + +static void print_account_view_list(const wf_admin_account_view_list *list) { + cJSON *root = cJSON_CreateObject(); + if (!root) return; + if (list->cursor) + cJSON_AddStringToObject(root, "cursor", list->cursor); + cJSON *accounts = cJSON_CreateArray(); + if (accounts) { + for (size_t i = 0; i < list->account_count; ++i) { + const wf_admin_account_view *v = &list->accounts[i]; + cJSON *acc = cJSON_CreateObject(); + if (!acc) continue; + if (v->did) cJSON_AddStringToObject(acc, "did", v->did); + if (v->handle) cJSON_AddStringToObject(acc, "handle", v->handle); + if (v->email) cJSON_AddStringToObject(acc, "email", v->email); + if (v->indexed_at) cJSON_AddStringToObject(acc, "indexedAt", v->indexed_at); + if (v->has_invites_disabled) + cJSON_AddBoolToObject(acc, "invitesDisabled", v->invites_disabled); + if (v->deactivated_at) + cJSON_AddStringToObject(acc, "deactivatedAt", v->deactivated_at); + if (v->extra) + cJSON_AddItemToObject(acc, "extra", cJSON_Duplicate(v->extra, true)); + cJSON_AddItemToArray(accounts, acc); + } + cJSON_AddItemToObject(root, "accounts", accounts); + } + char *json = cJSON_Print(root); + if (json) { + printf("%s\n", json); + cJSON_free(json); + } + cJSON_Delete(root); +} + +static void print_account_view(const wf_admin_account_view *view) { + cJSON *root = cJSON_CreateObject(); + if (!root) return; + if (view->did) cJSON_AddStringToObject(root, "did", view->did); + if (view->handle) cJSON_AddStringToObject(root, "handle", view->handle); + if (view->email) cJSON_AddStringToObject(root, "email", view->email); + if (view->indexed_at) cJSON_AddStringToObject(root, "indexedAt", view->indexed_at); + if (view->has_invites_disabled) + cJSON_AddBoolToObject(root, "invitesDisabled", view->invites_disabled); + if (view->deactivated_at) + cJSON_AddStringToObject(root, "deactivatedAt", view->deactivated_at); + if (view->extra) + cJSON_AddItemToObject(root, "extra", cJSON_Duplicate(view->extra, true)); + char *json = cJSON_Print(root); + if (json) { + printf("%s\n", json); + cJSON_free(json); + } + cJSON_Delete(root); +} + +static void print_subject_status(const wf_admin_subject_status *status) { + cJSON *root = cJSON_CreateObject(); + if (!root) return; + if (status->did) cJSON_AddStringToObject(root, "did", status->did); + if (status->has_takedown) + cJSON_AddBoolToObject(root, "takedownApplied", status->takedown_applied); + if (status->takedown_ref) + cJSON_AddStringToObject(root, "takedownRef", status->takedown_ref); + if (status->has_deactivated) + cJSON_AddBoolToObject(root, "deactivatedApplied", status->deactivated_applied); + if (status->deactivated_ref) + cJSON_AddStringToObject(root, "deactivatedRef", status->deactivated_ref); + if (status->subject) + cJSON_AddItemToObject(root, "subject", cJSON_Duplicate(status->subject, true)); + char *json = cJSON_Print(root); + if (json) { + printf("%s\n", json); + cJSON_free(json); + } + cJSON_Delete(root); +} + +static void print_invite_code_list(const wf_admin_invite_code_list *list) { + cJSON *root = cJSON_CreateObject(); + if (!root) return; + if (list->cursor) + cJSON_AddStringToObject(root, "cursor", list->cursor); + cJSON *codes = cJSON_CreateArray(); + if (codes) { + for (size_t i = 0; i < list->code_count; ++i) { + const wf_admin_invite_code *c = &list->codes[i]; + cJSON *code = cJSON_CreateObject(); + if (!code) continue; + if (c->code) cJSON_AddStringToObject(code, "code", c->code); + if (c->has_available) + cJSON_AddNumberToObject(code, "available", c->available); + if (c->has_disabled) + cJSON_AddBoolToObject(code, "disabled", c->disabled); + if (c->for_account) + cJSON_AddStringToObject(code, "forAccount", c->for_account); + if (c->created_by) + cJSON_AddStringToObject(code, "createdBy", c->created_by); + if (c->created_at) + cJSON_AddStringToObject(code, "createdAt", c->created_at); + if (c->extra) + cJSON_AddItemToObject(code, "extra", cJSON_Duplicate(c->extra, true)); + cJSON_AddItemToArray(codes, code); + } + cJSON_AddItemToObject(root, "codes", codes); + } + char *json = cJSON_Print(root); + if (json) { + printf("%s\n", json); + cJSON_free(json); + } + cJSON_Delete(root); +} + +int cmd_admin(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + + const char *sub = argv[1]; + + if (strcmp(sub, "search-accounts") == 0) { + const char *service = NULL, *handle = NULL, *password = NULL, *email = NULL; + int limit = 50; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (!service) + service = argv[i]; + else if (!handle) + handle = argv[i]; + else if (!password) + password = argv[i]; + else if (!email) + email = argv[i]; + } + if (!service || !handle || !password) { + fprintf(stderr, "error: usage: wolfram admin search-accounts " + " [email] " + "[--limit N]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_admin_account_view_list list = {0}; + wf_status s = wf_agent_admin_search_accounts(agent, email ? email : "", + limit, NULL, &list); + if (s != WF_OK) { + fprintf(stderr, "error: searchAccounts failed (status %d)\n", + (int)s); + wf_admin_account_view_list_free(&list); + wf_agent_free(agent); + return 1; + } + print_account_view_list(&list); + wf_admin_account_view_list_free(&list); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "get-account-info") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram admin get-account-info " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + wf_admin_account_view view = {0}; + wf_status s = wf_agent_admin_get_account_info(agent, argv[5], &view); + if (s != WF_OK) { + fprintf(stderr, "error: getAccountInfo failed (status %d)\n", + (int)s); + wf_admin_account_view_free(&view); + wf_agent_free(agent); + return 1; + } + print_account_view(&view); + wf_admin_account_view_free(&view); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "get-subject-status") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram admin get-subject-status " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + wf_admin_subject_status status = {0}; + wf_status s = wf_agent_admin_get_subject_status(agent, argv[5], &status); + if (s != WF_OK) { + fprintf(stderr, "error: getSubjectStatus failed (status %d)\n", + (int)s); + wf_admin_subject_status_free(&status); + wf_agent_free(agent); + return 1; + } + print_subject_status(&status); + wf_admin_subject_status_free(&status); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "get-invite-codes") == 0) { + const char *service = NULL, *handle = NULL, *password = NULL; + const char *cursor = NULL; + int create_count = 0; + int use_count = 1; + bool do_create = false; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--create") == 0 && i + 1 < argc) { + create_count = atoi(argv[++i]); + do_create = true; + } else if (strcmp(argv[i], "--use-count") == 0 && i + 1 < argc) { + use_count = atoi(argv[++i]); + } else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) { + cursor = argv[++i]; + } else if (!service) + service = argv[i]; + else if (!handle) + handle = argv[i]; + else if (!password) + password = argv[i]; + } + if (!service || !handle || !password) { + fprintf(stderr, "error: usage: wolfram admin get-invite-codes " + " [--create N] " + "[--use-count N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + if (do_create) { + for (int i = 0; i < create_count; ++i) { + wf_response res = {0}; + wf_status s = wf_agent_create_invite_code(agent, use_count, &res); + if (s != WF_OK) { + fprintf(stderr, "error: createInviteCode failed " + "(status %d)\n", (int)s); + wf_response_free(&res); + wf_agent_free(agent); + return 1; + } + printf("%s\n", res.body ? res.body : "(no result)"); + wf_response_free(&res); + } + } else { + wf_admin_invite_code_list list = {0}; + wf_status s = wf_agent_admin_get_invite_codes(agent, cursor, &list); + if (s != WF_OK) { + fprintf(stderr, "error: getInviteCodes failed (status %d)\n", + (int)s); + wf_admin_invite_code_list_free(&list); + wf_agent_free(agent); + return 1; + } + print_invite_code_list(&list); + wf_admin_invite_code_list_free(&list); + } + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "delete-account") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram admin delete-account " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + wf_status s = wf_agent_admin_delete_account(agent, argv[5]); + if (s != WF_OK) { + fprintf(stderr, "error: deleteAccount failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("account deleted\n"); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "disable-account-invites") == 0) { + if (argc < 5) { + fprintf(stderr, "error: usage: wolfram admin " + "disable-account-invites " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + wf_status s = wf_agent_admin_disable_account_invites(agent); + if (s != WF_OK) { + fprintf(stderr, "error: disableAccountInvites failed (status " + "%d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("account invites disabled\n"); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "enable-account-invites") == 0) { + if (argc < 5) { + fprintf(stderr, "error: usage: wolfram admin " + "enable-account-invites " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + wf_status s = wf_agent_admin_enable_account_invites(agent); + if (s != WF_OK) { + fprintf(stderr, "error: enableAccountInvites failed (status " + "%d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("account invites enabled\n"); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "send-email") == 0) { + if (argc < 8) { + fprintf(stderr, "error: usage: wolfram admin send-email " + " " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + const char *sender_did = wf_agent_get_did(agent); + wf_status s = wf_agent_admin_send_email(agent, argv[5], argv[7], argv[6], + sender_did); + if (s != WF_OK) { + fprintf(stderr, "error: sendEmail failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("email sent\n"); + wf_agent_free(agent); + return 0; + } + + fprintf(stderr, "error: unknown admin subcommand '%s'\n", sub); + return 1; +} diff --git a/src/cli/cli_admin.h b/src/cli/cli_admin.h new file mode 100644 index 0000000..a5d5590 --- /dev/null +++ b/src/cli/cli_admin.h @@ -0,0 +1,6 @@ +#ifndef CLI_ADMIN_H +#define CLI_ADMIN_H + +int cmd_admin(int argc, char **argv); + +#endif diff --git a/src/cli/cli_auth.c b/src/cli/cli_auth.c new file mode 100644 index 0000000..e80bfe4 --- /dev/null +++ b/src/cli/cli_auth.c @@ -0,0 +1,109 @@ +/* + * cli_auth.c — the `login` / `whoami` / `describe-server` subcommands, split + * out of main.c as their own self-contained concern. + */ + +#include "cli_auth.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/server_typed.h" +#include "wolfram/syntax.h" + +#include +#include +#include +#include + +int cmd_login(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + printf("logged in as %s (%s)\n", sd.handle ? sd.handle : "?", + sd.did ? sd.did : "?"); + wf_agent_session_data_free(&sd); + wf_agent_free(agent); + return 0; +} + +int cmd_whoami(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + printf("did: %s\n", sd.did ? sd.did : "?"); + printf("handle: %s\n", sd.handle ? sd.handle : "?"); + wf_agent_session_data_free(&sd); + wf_agent_free(agent); + return 0; +} + +int cmd_describe_server(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_agent_server_description desc = {0}; + wf_status s = wf_agent_describe_server(agent, &desc); + if (s != WF_OK) { + fprintf(stderr, "error: describeServer failed (status %d)\n", (int)s); + wf_agent_server_description_free(&desc); + wf_agent_free(agent); + return 1; + } + printf("did: %s\n", desc.did ? desc.did : "?"); + printf("inviteCodeRequired: %d\n", desc.invite_code_required); + printf("phoneVerificationRequired: %d\n", desc.phone_verification_required); + printf("availableUserDomains (%zu):\n", desc.available_user_domain_count); + for (size_t i = 0; i < desc.available_user_domain_count; ++i) { + printf(" %s\n", desc.available_user_domains[i] + ? desc.available_user_domains[i] + : "?"); + } + printf("privacyPolicy: %s\n", + desc.privacy_policy ? desc.privacy_policy : "?"); + printf("termsOfService: %s\n", + desc.terms_of_service ? desc.terms_of_service : "?"); + printf("contactEmail: %s\n", + desc.contact_email ? desc.contact_email : "?"); + wf_agent_server_description_free(&desc); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_auth.h b/src/cli/cli_auth.h new file mode 100644 index 0000000..7749a59 --- /dev/null +++ b/src/cli/cli_auth.h @@ -0,0 +1,12 @@ +#ifndef WOLFRAM_CLI_AUTH_H +#define WOLFRAM_CLI_AUTH_H + +/* `login` / `whoami` / `describe-server` subcommand handlers, dispatched from + * main.c's argv switch. Not part of any installed API -- this is the CLI + * demo program, not the SDK. */ + +int cmd_login(int argc, char **argv); +int cmd_whoami(int argc, char **argv); +int cmd_describe_server(int argc, char **argv); + +#endif /* WOLFRAM_CLI_AUTH_H */ diff --git a/src/cli/cli_chat.c b/src/cli/cli_chat.c new file mode 100644 index 0000000..d150083 --- /dev/null +++ b/src/cli/cli_chat.c @@ -0,0 +1,294 @@ +/* + * cli_chat.c — the `convos` / `messages` / `chat` / `groups` / `reactions` + * subcommands, split out of main.c as their own self-contained concern. + */ + +#include "cli_chat.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/chat_typed.h" +#include "wolfram/moderation.h" + +#include +#include +#include +#include +#include + +/* wolfram convos [limit] */ +int cmd_convos(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_chat_list_convos(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram messages --convo [limit] */ +int cmd_messages(int argc, char **argv) { + const char *convo_id = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--convo") == 0 && i + 1 < argc) + convo_id = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !convo_id) { + fprintf(stderr, "error: usage: wolfram messages " + " --convo [limit]\n"); + return 1; + } + int limit = 50; + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = + wf_agent_chat_get_messages(agent, convo_id, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram chat send --convo + * --text */ +int cmd_chat(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + if (strcmp(sub, "send") == 0) { + const char *convo_id = NULL, *text = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--convo") == 0 && i + 1 < argc) + convo_id = argv[++i]; + else if (strcmp(argv[i], "--text") == 0 && i + 1 < argc) + text = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !convo_id || !text) { + fprintf(stderr, "error: usage: wolfram chat send " + " --convo --text " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = + wf_agent_chat_send_message(agent, convo_id, text, NULL, &res); + return finish_agent_response(agent, s, &res); + } + + fprintf(stderr, "error: unknown chat subcommand '%s' (try send)\n", sub); + return 1; +} + +/* wolfram groups create --members ... + * --name + * wolfram groups edit --convo + * --name + * wolfram groups add-members --convo + * --members ... + * wolfram groups remove-members --convo + * --members ... */ +int cmd_groups(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + if (strcmp(sub, "create") == 0) { + const char *name = NULL; + const char *members[16]; + int n_members = 0; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) + name = argv[++i]; + else if (strcmp(argv[i], "--members") == 0) { + while (i + 1 < argc && argv[i + 1][0] != '-' && + n_members < 16) + members[n_members++] = argv[++i]; + } else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !name || n_members == 0) { + fprintf(stderr, "error: usage: wolfram groups create " + " --members ... --name " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_chat_create_group(agent, members, n_members, + name, &res); + return finish_agent_response(agent, s, &res); + } + + if (strcmp(sub, "edit") == 0) { + const char *convo_id = NULL, *name = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--convo") == 0 && i + 1 < argc) + convo_id = argv[++i]; + else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) + name = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !convo_id || !name) { + fprintf(stderr, "error: usage: wolfram groups edit " + " --convo --name " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_chat_edit_group(agent, convo_id, name, &res); + return finish_agent_response(agent, s, &res); + if (s != WF_OK) { + fprintf(stderr, "error: editGroup failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("group %s renamed to %s\n", convo_id, name); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "add-members") == 0 || strcmp(sub, "remove-members") == 0) { + const char *convo_id = NULL; + const char *members[16]; + int n_members = 0; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--convo") == 0 && i + 1 < argc) + convo_id = argv[++i]; + else if (strcmp(argv[i], "--members") == 0) { + while (i + 1 < argc && argv[i + 1][0] != '-' && + n_members < 16) + members[n_members++] = argv[++i]; + } else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !convo_id || n_members == 0) { + fprintf(stderr, "error: usage: wolfram groups %s " + " --convo --members " + "...\n", + sub); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s; + if (strcmp(sub, "add-members") == 0) + s = wf_agent_chat_add_members(agent, convo_id, members, + n_members, &res); + else + s = wf_agent_chat_remove_members(agent, convo_id, members, + n_members, &res); + return finish_agent_response(agent, s, &res); + if (s != WF_OK) { + fprintf(stderr, "error: %s failed (status %d)\n", + strcmp(sub, "add-members") == 0 ? "addMembers" + : "removeMembers", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("%s %d members %s convo %s\n", + strcmp(sub, "add-members") == 0 ? "added" : "removed", + n_members, + strcmp(sub, "add-members") == 0 ? "to" : "from", convo_id); + wf_agent_free(agent); + return 0; + } + + fprintf(stderr, "error: unknown groups subcommand '%s' (try create/edit/" + "add-members/remove-members)\n", + sub); + return 1; +} + +/* wolfram reactions + * --convo --message --value */ +int cmd_reactions(int argc, char **argv) { + const char *action = NULL, *convo_id = NULL, *msg_id = NULL, + *value = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--convo") == 0 && i + 1 < argc) + convo_id = argv[++i]; + else if (strcmp(argv[i], "--message") == 0 && i + 1 < argc) + msg_id = argv[++i]; + else if (strcmp(argv[i], "--value") == 0 && i + 1 < argc) + value = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + else if (!action) + action = argv[i]; + } + if (pi < 3 || !action || !convo_id || !msg_id || !value) { + fprintf(stderr, "error: usage: wolfram reactions " + " --convo --message " + " --value \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s; + if (strcmp(action, "add") == 0) + s = wf_agent_chat_add_reaction(agent, convo_id, msg_id, value); + else if (strcmp(action, "remove") == 0) + s = wf_agent_chat_remove_reaction(agent, convo_id, msg_id, value); + else { + fprintf(stderr, "error: unknown reactions action '%s' (try " + "add/remove)\n", + action); + wf_agent_free(agent); + return 1; + } + + if (s != WF_OK) { + fprintf(stderr, "error: %sReaction failed (status %d)\n", action, + (int)s); + wf_agent_free(agent); + return 1; + } + printf("%s reaction %s on message %s\n", action, value, msg_id); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_chat.h b/src/cli/cli_chat.h new file mode 100644 index 0000000..5131644 --- /dev/null +++ b/src/cli/cli_chat.h @@ -0,0 +1,14 @@ +#ifndef WOLFRAM_CLI_CHAT_H +#define WOLFRAM_CLI_CHAT_H + +/* `convos` / `messages` / `chat` / `groups` / `reactions` subcommand + * handlers, dispatched from main.c's argv switch. Not part of any installed + * API -- this is the CLI demo program, not the SDK. */ + +int cmd_convos(int argc, char **argv); +int cmd_messages(int argc, char **argv); +int cmd_chat(int argc, char **argv); +int cmd_groups(int argc, char **argv); +int cmd_reactions(int argc, char **argv); + +#endif /* WOLFRAM_CLI_CHAT_H */ diff --git a/src/cli/cli_feed.c b/src/cli/cli_feed.c new file mode 100644 index 0000000..54a03e5 --- /dev/null +++ b/src/cli/cli_feed.c @@ -0,0 +1,575 @@ +/* + * cli_feed.c — the `feed` / `get-likes` / `get-reposted-by` / `get-quotes` / + * `get-actor-likes` / `get-actor-feeds` / `describe-feed` / + * `get-suggested-feeds` / `get-suggestions` / `known-followers` / + * `relationships` / `get-profiles` / `unread-count` subcommands, split out + * of main.c as their own self-contained concern. + */ + +#include "cli_feed.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/feed_typed.h" +#include "wolfram/feedgen_typed.h" +#include "wolfram/unspecced_typed.h" +#include "wolfram/graph_social_typed.h" +#include "wolfram/notification_typed.h" +#include "wolfram/actor_status_typed.h" +#include "wolfram/ageassurance_typed.h" + +#include +#include +#include +#include +#include + +int cmd_feed(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + /* wolfram feed get --feed + * [--limit N] [--cursor C] */ + if (strcmp(sub, "get") == 0) { + const char *feed = NULL, *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--feed") == 0 && i + 1 < argc) + feed = argv[++i]; + else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !feed) { + fprintf( + stderr, + "error: usage: wolfram feed get " + " --feed [--limit N] [--cursor C]\n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + if (g_json) { + wf_response res = {0}; + wf_status s = wf_agent_get_feed(agent, feed, limit, cursor, &res); + return finish_agent_response(agent, s, &res); + } + wf_agent_feed_view_list list = {0}; + wf_status s = + wf_agent_get_feed_typed(agent, feed, limit, cursor, &list); + if (s != WF_OK) { + fprintf(stderr, "error: getFeed failed (status %d)\n", (int)s); + wf_agent_feed_view_list_free(&list); + wf_agent_free(agent); + return 1; + } + for (size_t i = 0; i < list.item_count; ++i) { + const wf_agent_post_view *post = &list.items[i].post; + const char *author = + post->author.handle + ? post->author.handle + : (post->author.did ? post->author.did : "?"); + const char *text = ""; + if (post->record) { + cJSON *t = + cJSON_GetObjectItemCaseSensitive(post->record, "text"); + if (cJSON_IsString(t) && t->valuestring) text = t->valuestring; + } + printf("%s: %s\n", author, text); + } + if (list.cursor) printf("cursor: %s\n", list.cursor); + if (list.item_count == 0) printf("(empty feed)\n"); + wf_agent_feed_view_list_free(&list); + wf_agent_free(agent); + return 0; + } + + /* wolfram feed author --actor + * [--limit N] [--cursor C] */ + if (strcmp(sub, "author") == 0) { + const char *actor = NULL, *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--actor") == 0 && i + 1 < argc) + actor = argv[++i]; + else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !actor) { + fprintf(stderr, + "error: usage: wolfram feed author " + " --actor [--limit N] [--cursor " + "C]\n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + if (g_json) { + wf_response res = {0}; + wf_status s = wf_agent_get_author_feed(agent, actor, limit, cursor, + NULL, false, &res); + return finish_agent_response(agent, s, &res); + } + wf_agent_feed_list list = {0}; + wf_status s = wf_agent_get_author_feed_typed(agent, actor, limit, + cursor, NULL, &list); + if (s != WF_OK) { + fprintf(stderr, "error: getAuthorFeed failed (status %d)\n", + (int)s); + wf_agent_feed_list_free(&list); + wf_agent_free(agent); + return 1; + } + for (size_t i = 0; i < list.item_count; ++i) { + const wf_agent_post_view *post = &list.items[i].post; + const char *author = + post->author.handle + ? post->author.handle + : (post->author.did ? post->author.did : "?"); + const char *text = ""; + if (post->record) { + cJSON *t = + cJSON_GetObjectItemCaseSensitive(post->record, "text"); + if (cJSON_IsString(t) && t->valuestring) text = t->valuestring; + } + printf("%s: %s\n", author, text); + } + if (list.cursor) printf("cursor: %s\n", list.cursor); + if (list.item_count == 0) printf("(empty feed)\n"); + wf_agent_feed_list_free(&list); + wf_agent_free(agent); + return 0; + } + + fprintf(stderr, "error: unknown feed subcommand '%s' (try get/author)\n", + sub); + return 1; +} + +/* wolfram get-likes [limit] */ +int cmd_get_likes(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 50; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_likes(agent, at_uri, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-reposted-by [limit] */ +int cmd_get_reposted_by(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 50; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_reposted_by(agent, at_uri, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-quotes [limit] */ +int cmd_get_quotes(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 50; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_quotes(agent, at_uri, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-actor-likes [limit] */ +int cmd_get_actor_likes(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_actor_likes(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-actor-feeds [limit] */ +int cmd_get_actor_feeds(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_actor_feeds(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram describe-feed [--feed ] */ +int cmd_describe_feed(int argc, char **argv) { + const char *feed = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--feed") == 0 && i + 1 < argc) + feed = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + usage_stream(stderr); + return 0; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + if (feed) { + wf_response res = {0}; + wf_status s = wf_agent_get_feed_generator(agent, feed, &res); + return finish_agent_response(agent, s, &res); + } + + wf_response res = {0}; + wf_status s = wf_agent_describe_feed_generator(agent, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-suggested-feeds */ +int cmd_get_suggested_feeds(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_suggested_feeds(agent, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-suggestions [limit] */ +int cmd_get_suggestions(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_suggestions(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram known-followers [limit] */ +int cmd_known_followers(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_known_followers(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram relationships ... */ +int cmd_relationships(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + /* argv[3..] are the other DIDs */ + int n_others = argc - 3; + const char **others = (const char **)(argv + 3); + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_get_relationships(agent, actor, others, n_others, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-profiles ... */ +int cmd_get_profiles(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + int n_actors = argc - 2; + const char **actors = (const char **)(argv + 2); + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_profiles(agent, actors, n_actors, 0, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram unread-count */ +int cmd_unread_count(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + int count = 0; + wf_status s = wf_agent_get_unread_count_typed(agent, &count); + if (s != WF_OK) { + fprintf(stderr, "error: getUnreadCount failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("%d\n", count); + wf_agent_free(agent); + return 0; +} + +/* wolfram get-actor-status */ +int cmd_get_actor_status(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_actor_status_view status = {0}; + wf_status s = wf_agent_get_actor_status(agent, actor, &status); + if (s != WF_OK) { + fprintf(stderr, "error: getActorStatus failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("actor: %s\n", actor); + printf("status: %s\n", status.status ? status.status : "(none)"); + printf("createdAt: %s\n", status.created_at ? status.created_at : "(none)"); + if (status.has_duration_minutes) + printf("durationMinutes: %d\n", status.duration_minutes); + printf("isActive: %d\n", status.has_is_active ? status.is_active : 0); + printf("isDisabled: %d\n", status.has_is_disabled ? status.is_disabled : 0); + printf("expiresAt: %s\n", status.expires_at ? status.expires_at : "(none)"); + printf("uri: %s\n", status.uri ? status.uri : "(none)"); + printf("cid: %s\n", status.cid ? status.cid : "(none)"); + + wf_actor_status_view_free(&status); + wf_agent_free(agent); + return 0; +} + +/* wolfram get-feed-generators ... */ +int cmd_get_feed_generators(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + int n_feeds = argc - 2; + const char **feeds = (const char **)(argv + 2); + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_get_feed_generators(agent, feeds, n_feeds, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram get-suggested-follows-by-actor */ +int cmd_get_suggested_follows_by_actor(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_get_suggested_follows_by_actor(agent, actor, &res); + return finish_agent_response(agent, s, &res); +} + +/* wolfram age-assurance */ +int cmd_age_assurance(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *sub = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + if (strcmp(sub, "begin") == 0) { + wf_ageassurance_begin out = {0}; + wf_status s = wf_agent_begin_ageassurance(agent, &out); + if (s != WF_OK) { + fprintf(stderr, "error: ageAssurance.begin failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("status: %s\n", out.status ? out.status : "(none)"); + printf("access: %s\n", out.access ? out.access : "(none)"); + printf("lastInitiatedAt: %s\n", + out.last_initiated_at ? out.last_initiated_at : "(none)"); + wf_ageassurance_begin_free(&out); + } else if (strcmp(sub, "get-config") == 0) { + wf_ageassurance_config out = {0}; + wf_status s = wf_agent_get_ageassurance_config(agent, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: ageAssurance.getConfig failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("required: %s\n", + out.regions && cJSON_IsTrue(out.regions) ? "true" : "false"); + if (out.regions && cJSON_IsArray(out.regions)) { + int n = cJSON_GetArraySize(out.regions); + printf("regions (%d):\n", n); + for (int i = 0; i < n; ++i) { + cJSON *r = cJSON_GetArrayItem(out.regions, i); + printf(" %s\n", cJSON_IsObject(r) ? "(object)" : "(unknown)"); + } + } + wf_ageassurance_config_free(&out); + } else if (strcmp(sub, "get-state") == 0) { + wf_ageassurance_state out = {0}; + wf_status s = wf_agent_get_ageassurance_state(agent, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: ageAssurance.getState failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("status: %s\n", out.state.status ? out.state.status : "(none)"); + printf("access: %s\n", out.state.access ? out.state.access : "(none)"); + printf("lastInitiatedAt: %s\n", + out.state.last_initiated_at ? out.state.last_initiated_at + : "(none)"); + printf("accountCreatedAt: %s\n", + out.metadata.account_created_at ? out.metadata.account_created_at + : "(none)"); + wf_ageassurance_state_free(&out); + } else { + fprintf(stderr, + "error: unknown age-assurance subcommand '%s' (try begin, " + "get-config, get-state)\n", + sub); + wf_agent_free(agent); + return 1; + } + + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_feed.h b/src/cli/cli_feed.h new file mode 100644 index 0000000..4b042e9 --- /dev/null +++ b/src/cli/cli_feed.h @@ -0,0 +1,30 @@ +#ifndef WOLFRAM_CLI_FEED_H +#define WOLFRAM_CLI_FEED_H + +/* `feed` / `get-likes` / `get-reposted-by` / `get-quotes` / + * `get-actor-likes` / `get-actor-feeds` / `describe-feed` / + * `get-suggested-feeds` / `get-suggestions` / `known-followers` / + * `relationships` / `get-profiles` / `unread-count` / `get-actor-status` / + * `get-feed-generators` / `get-suggested-follows-by-actor` / + * `age-assurance` subcommand handlers, dispatched from main.c's argv switch. + * Not part of any installed API -- this is the CLI demo program, not the SDK. */ + +int cmd_feed(int argc, char **argv); +int cmd_get_likes(int argc, char **argv); +int cmd_get_reposted_by(int argc, char **argv); +int cmd_get_quotes(int argc, char **argv); +int cmd_get_actor_likes(int argc, char **argv); +int cmd_get_actor_feeds(int argc, char **argv); +int cmd_describe_feed(int argc, char **argv); +int cmd_get_suggested_feeds(int argc, char **argv); +int cmd_get_suggestions(int argc, char **argv); +int cmd_known_followers(int argc, char **argv); +int cmd_relationships(int argc, char **argv); +int cmd_get_profiles(int argc, char **argv); +int cmd_unread_count(int argc, char **argv); +int cmd_get_actor_status(int argc, char **argv); +int cmd_get_feed_generators(int argc, char **argv); +int cmd_get_suggested_follows_by_actor(int argc, char **argv); +int cmd_age_assurance(int argc, char **argv); + +#endif /* WOLFRAM_CLI_FEED_H */ diff --git a/src/cli/cli_graph.c b/src/cli/cli_graph.c new file mode 100644 index 0000000..e86537e --- /dev/null +++ b/src/cli/cli_graph.c @@ -0,0 +1,617 @@ +#include "cli_graph.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/graph_typed.h" +#include "wolfram/graph_social_typed.h" +#include "wolfram/graph_write.h" +#include "wolfram/unspecced_typed.h" + +#include +#include +#include +#include +#include + +int parse_list_create_args(int argc, char **argv, const char *pos[3], + const char **name, const char **purpose, + const char **description) { + *name = NULL; + *purpose = NULL; + *description = NULL; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) + *name = argv[++i]; + else if (strcmp(argv[i], "--purpose") == 0 && i + 1 < argc) + *purpose = argv[++i]; + else if (strcmp(argv[i], "--description") == 0 && i + 1 < argc) + *description = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + return (pi >= 3 && *name != NULL) ? 0 : -1; +} + +int cmd_follows(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_follows(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_followers(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_followers(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_blocks(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_blocks(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_mutes(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_mutes(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_list(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *list_uri = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_list(agent, list_uri, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_lists(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + int limit = (argc >= 4) ? atoi(argv[3]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_lists(agent, actor, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_list_create(int argc, char **argv) { + const char *pos[3]; + const char *name, *purpose, *description; + if (parse_list_create_args(argc, argv, pos, &name, &purpose, + &description) != 0) { + fprintf(stderr, + "error: usage: wolfram list create " + " --name [--purpose modlist|curatelist|" + "referencelist] [--description ]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + const char *purpose_str = purpose ? purpose : "app.bsky.graph.defs#curatelist"; + + wf_agent_post_result out = {0}; + wf_status s = wf_agent_graph_create_list(agent, purpose_str, name, + description, &out); + if (s != WF_OK) { + fprintf(stderr, "error: createList failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_list_update(int argc, char **argv) { + const char *list_uri = NULL, *name = NULL, *description = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) + name = argv[++i]; + else if (strcmp(argv[i], "--description") == 0 && i + 1 < argc) + description = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram list update " + " --list [--name ] " + "[--description ]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + /* Extract rkey from list URI */ + const char *rkey = strrchr(list_uri, '/'); + if (!rkey) { + fprintf(stderr, "error: invalid list URI '%s'\n", list_uri); + wf_agent_free(agent); + return 1; + } + rkey++; + + char *record_json = NULL; + cJSON *rec = cJSON_CreateObject(); + if (name) + cJSON_AddStringToObject(rec, "name", name); + if (description) + cJSON_AddStringToObject(rec, "description", description); + record_json = cJSON_PrintUnformatted(rec); + cJSON_Delete(rec); + + wf_agent_post_result out = {0}; + wf_status s = wf_agent_graph_update_list(agent, rkey, record_json, + &out); + free(record_json); + if (s != WF_OK) { + fprintf(stderr, "error: updateList failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("updated list %s\n", list_uri); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_list_delete(int argc, char **argv) { + const char *list_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram list delete " + " --list \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_graph_delete_list(agent, list_uri); + if (s != WF_OK) { + fprintf(stderr, "error: deleteList failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("deleted list %s\n", list_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_list_add_item(int argc, char **argv) { + const char *list_uri = NULL, *actor = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (strcmp(argv[i], "--actor") == 0 && i + 1 < argc) + actor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri || !actor) { + fprintf(stderr, "error: usage: wolfram list add-item " + " --list --actor " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + char *did = NULL; + wf_status s = resolve_actor_to_did(agent, actor, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + free(did); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result out = {0}; + s = wf_agent_graph_create_list_item(agent, list_uri, did, &out); + free(did); + if (s != WF_OK) { + fprintf(stderr, "error: createListItem failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_list_remove_item(int argc, char **argv) { + const char *item_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--item") == 0 && i + 1 < argc) + item_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !item_uri) { + fprintf(stderr, "error: usage: wolfram list remove-item " + " --item \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_graph_delete_list_item(agent, item_uri); + if (s != WF_OK) { + fprintf(stderr, "error: deleteListItem failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("removed list item %s\n", item_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_mute_list(int argc, char **argv) { + const char *list_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram mute-list " + " --list \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_graph_mute_actor_list(agent, list_uri); + if (s != WF_OK) { + fprintf(stderr, "error: muteActorList failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("muted list %s\n", list_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_unmute_list(int argc, char **argv) { + const char *list_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram unmute-list " + " --list \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_graph_unmute_actor_list(agent, list_uri); + if (s != WF_OK) { + fprintf(stderr, "error: unmuteActorList failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("unmuted list %s\n", list_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_block_list(int argc, char **argv) { + const char *list_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram block-list " + " --list \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_agent_post_result out = {0}; + wf_status s = wf_agent_graph_create_list_block(agent, list_uri, &out); + if (s != WF_OK) { + fprintf(stderr, "error: createListBlock failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_unblock_list(int argc, char **argv) { + const char *list_uri = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--list") == 0 && i + 1 < argc) + list_uri = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !list_uri) { + fprintf(stderr, "error: usage: wolfram unblock-list " + " --list \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_graph_delete_list_block(agent, list_uri); + if (s != WF_OK) { + fprintf(stderr, "error: deleteListBlock failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("unblocked list %s\n", list_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_mute_thread(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_status s = wf_agent_mute_thread(agent, argv[4]); + if (s != WF_OK) { + fprintf(stderr, "error: muteThread failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("muted thread %s\n", argv[4]); + wf_agent_free(agent); + return 0; +} + +int cmd_unmute_thread(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_status s = wf_agent_unmute_thread(agent, argv[4]); + if (s != WF_OK) { + fprintf(stderr, "error: unmuteThread failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("unmuted thread %s\n", argv[4]); + wf_agent_free(agent); + return 0; +} + +int cmd_get_list_blocks(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_list_blocks(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_get_list_mutes(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_list_mutes(agent, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_get_suggested_follows(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_suggested_follows(agent, actor, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_starter_pack(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + if (strcmp(sub, "get") == 0) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[2]; + const char *uri = argv[3]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + /* Use the agent-level getStarterPack wrapper */ + wf_status s = wf_agent_get_starter_pack(agent, uri, &res); + return finish_agent_response(agent, s, &res); + } + + if (strcmp(sub, "get-membership") == 0) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[2]; + const char *actor = argv[3]; + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_starter_packs_with_membership(agent, actor, + limit, NULL, + &res); + return finish_agent_response(agent, s, &res); + } + + fprintf(stderr, + "error: unknown starter-pack subcommand '%s' (try get/" + "get-membership)\n", + sub); + return 1; +} diff --git a/src/cli/cli_graph.h b/src/cli/cli_graph.h new file mode 100644 index 0000000..7964903 --- /dev/null +++ b/src/cli/cli_graph.h @@ -0,0 +1,28 @@ +#ifndef WOLFRAM_CLI_GRAPH_H +#define WOLFRAM_CLI_GRAPH_H + +#include + +int cmd_follows(int argc, char **argv); +int cmd_followers(int argc, char **argv); +int cmd_blocks(int argc, char **argv); +int cmd_mutes(int argc, char **argv); +int cmd_list(int argc, char **argv); +int cmd_lists(int argc, char **argv); +int cmd_list_create(int argc, char **argv); +int cmd_list_update(int argc, char **argv); +int cmd_list_delete(int argc, char **argv); +int cmd_list_add_item(int argc, char **argv); +int cmd_list_remove_item(int argc, char **argv); +int cmd_mute_list(int argc, char **argv); +int cmd_unmute_list(int argc, char **argv); +int cmd_block_list(int argc, char **argv); +int cmd_unblock_list(int argc, char **argv); +int cmd_mute_thread(int argc, char **argv); +int cmd_unmute_thread(int argc, char **argv); +int cmd_get_list_blocks(int argc, char **argv); +int cmd_get_list_mutes(int argc, char **argv); +int cmd_get_suggested_follows(int argc, char **argv); +int cmd_starter_pack(int argc, char **argv); + +#endif /* WOLFRAM_CLI_GRAPH_H */ diff --git a/src/cli/cli_identity.c b/src/cli/cli_identity.c new file mode 100644 index 0000000..843d0dd --- /dev/null +++ b/src/cli/cli_identity.c @@ -0,0 +1,223 @@ +/* + * cli_identity.c — the `resolve-did` / `check-handle` / + * `get-recommended-did-credentials` / `rotate-handle` / `plc` subcommands, + * split out of main.c as their own self-contained concern. + */ + +#include "cli_identity.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/identity_typed.h" +#include "wolfram/plc.h" + +#include +#include +#include +#include +#include + +int cmd_resolve_did(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *did = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_resolve_did(agent, did, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_check_handle(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_check_handle(agent, handle, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_get_recommended_did_credentials(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_get_recommended_did_credentials(agent, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_rotate_handle(int argc, char **argv) { + const char *new_handle = NULL, *token = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--token") == 0 && i + 1 < argc) + token = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + else if (!new_handle) + new_handle = argv[i]; + } + if (pi < 3 || !new_handle) { + fprintf(stderr, "error: usage: wolfram rotate-handle " + " [--token " + "]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s; + if (token) { + s = wf_agent_request_plc_operation_signature_typed(agent, wf_agent_get_did(agent)); + if (s == WF_OK) { + s = wf_agent_update_handle_typed(agent, new_handle); + } + } else { + s = wf_agent_update_handle(agent, new_handle); + } + if (s != WF_OK) { + fprintf(stderr, "error: rotateHandle failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("handle rotated to %s\n", new_handle); + wf_agent_free(agent); + return 0; +} + +int cmd_plc(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "error: usage: wolfram plc ...\n"); + return 1; + } + const char *sub = argv[1]; + + if (strcmp(sub, "request-signature") == 0) { + if (argc < 4) { + fprintf(stderr, "error: usage: wolfram plc request-signature " + " \n"); + return 1; + } + const char *service = argv[2]; + const char *did = argv[3]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + wf_status s = + wf_agent_request_plc_operation_signature_typed(agent, did); + wf_agent_free(agent); + if (s != WF_OK) { + fprintf(stderr, "error: requestSignature failed (status %d)\n", + (int)s); + return 1; + } + printf("PLC signature requested for %s\n", did); + return 0; + } + + if (strcmp(sub, "sign") == 0) { + if (argc < 4) { + fprintf(stderr, "error: usage: wolfram plc sign " + " [--rotation-key ]... [--also-known-as " + "]...\n"); + return 1; + } + const char *service = argv[2]; + const char *token = argv[3]; + + const char *rotation_keys[16]; + int n_rot = 0; + const char *aka[16]; + int n_aka = 0; + for (int i = 4; i < argc; ++i) { + if (strcmp(argv[i], "--rotation-key") == 0 && i + 1 < argc && + n_rot < 16) + rotation_keys[n_rot++] = argv[++i]; + else if (strcmp(argv[i], "--also-known-as") == 0 && i + 1 < argc && + n_aka < 16) + aka[n_aka++] = argv[++i]; + } + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_identity_signed_operation out = {0}; + wf_status s = wf_agent_sign_plc_operation_typed( + agent, token, rotation_keys, n_rot, aka, n_aka, NULL, NULL, + &out); + wf_agent_free(agent); + if (s != WF_OK) { + fprintf(stderr, "error: signPlcOperation failed (status %d)\n", + (int)s); + wf_identity_signed_operation_free(&out); + return 1; + } + printf("signed operation: %s\n", + out.operation_json ? out.operation_json : "(none)"); + wf_identity_signed_operation_free(&out); + return 0; + } + + if (strcmp(sub, "submit") == 0) { + if (argc < 4) { + fprintf(stderr, "error: usage: wolfram plc submit " + "\n"); + return 1; + } + const char *service = argv[2]; + const char *op_json = argv[3]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = + wf_agent_submit_plc_operation_typed(agent, op_json); + wf_agent_free(agent); + if (s != WF_OK) { + fprintf(stderr, "error: submitPlcOperation failed (status %d)\n", + (int)s); + return 1; + } + printf("PLC operation submitted\n"); + return 0; + } + + fprintf(stderr, "error: unknown plc subcommand '%s' (try " + "request-signature/sign/submit)\n", + sub); + return 1; +} diff --git a/src/cli/cli_identity.h b/src/cli/cli_identity.h new file mode 100644 index 0000000..1a81dc5 --- /dev/null +++ b/src/cli/cli_identity.h @@ -0,0 +1,28 @@ +#ifndef WOLFRAM_CLI_IDENTITY_H +#define WOLFRAM_CLI_IDENTITY_H + +/* `resolve-did` / `check-handle` / `get-recommended-did-credentials` / + * `rotate-handle` / `plc` subcommand handlers, dispatched from main.c's + * argv switch. Not part of any installed API -- this is the CLI demo + * program, not the SDK. */ + +/* wolfram resolve-did */ +int cmd_resolve_did(int argc, char **argv); + +/* wolfram check-handle */ +int cmd_check_handle(int argc, char **argv); + +/* wolfram get-recommended-did-credentials */ +int cmd_get_recommended_did_credentials(int argc, char **argv); + +/* wolfram rotate-handle + * [--token ] */ +int cmd_rotate_handle(int argc, char **argv); + +/* wolfram plc request-signature + * wolfram plc sign [--rotation-key ]... + * [--also-known-as ]... + * wolfram plc submit */ +int cmd_plc(int argc, char **argv); + +#endif /* WOLFRAM_CLI_IDENTITY_H */ diff --git a/src/cli/cli_ozone.c b/src/cli/cli_ozone.c new file mode 100644 index 0000000..f4fb572 --- /dev/null +++ b/src/cli/cli_ozone.c @@ -0,0 +1,724 @@ +#include "cli_ozone.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/ozone_typed.h" + +#include +#include +#include +#include +#include +#include + +static void print_subject_status( + const wf_lex_tools_ozone_moderation_defs_subject_status_view *st, + size_t idx) { + printf("status %zu: id=%" PRId64 " review_state=%s subject_repo_handle=%s " + "created_at=%s updated_at=%s\n", + idx, st->id, + st->review_state ? st->review_state : "?", + st->subject_repo_handle ? st->subject_repo_handle : "?", + st->created_at ? st->created_at : "?", + st->updated_at ? st->updated_at : "?"); + if (st->has_priority_score) + printf(" priority_score: %" PRId64 "\n", st->priority_score); + if (st->has_comment && st->comment) + printf(" comment: %s\n", st->comment); + if (st->has_takendown) + printf(" takendown: %s\n", st->takendown ? "true" : "false"); + if (st->has_appealed) + printf(" appealed: %s\n", st->appealed ? "true" : "false"); + if (st->has_tags && st->tags.count > 0) { + printf(" tags:"); + for (size_t j = 0; j < st->tags.count; ++j) + printf(" %s", st->tags.items[j]); + printf("\n"); + } + if (st->has_subject_blob_cids && st->subject_blob_cids.count > 0) { + printf(" blob_cids:"); + for (size_t j = 0; j < st->subject_blob_cids.count; ++j) + printf(" %s", st->subject_blob_cids.items[j]); + printf("\n"); + } + if (st->has_last_reviewed_by && st->last_reviewed_by) + printf(" last_reviewed_by: %s\n", st->last_reviewed_by); + if (st->has_last_reviewed_at && st->last_reviewed_at) + printf(" last_reviewed_at: %s\n", st->last_reviewed_at); + if (st->has_mute_until && st->mute_until) + printf(" mute_until: %s\n", st->mute_until); + if (st->has_age_assurance_state && st->age_assurance_state) + printf(" age_assurance_state: %s\n", st->age_assurance_state); +} + +static void print_team_member( + const wf_lex_tools_ozone_team_defs_member *m, size_t idx) { + printf("member %zu: did=%s role=%s disabled=%s created_at=%s " + "updated_at=%s\n", + idx, m->did ? m->did : "?", + m->role ? m->role : "?", + m->has_disabled ? (m->disabled ? "true" : "false") : "?", + m->created_at ? m->created_at : "?", + m->updated_at ? m->updated_at : "?"); + if (m->has_profile) { + printf(" profile_handle: %s\n", + m->profile->handle ? m->profile->handle : "?"); + } + if (m->last_updated_by) + printf(" last_updated_by: %s\n", m->last_updated_by); +} + +static void print_template( + const wf_lex_tools_ozone_communication_defs_template_view *t, + size_t idx) { + printf("template %zu: id=%s name=%s disabled=%s lang=%s " + "created_at=%s updated_at=%s\n", + idx, t->id ? t->id : "?", + t->name ? t->name : "?", + t->disabled ? "true" : "false", + t->has_lang && t->lang ? t->lang : "?", + t->created_at ? t->created_at : "?", + t->updated_at ? t->updated_at : "?"); + if (t->has_subject && t->subject) + printf(" subject: %s\n", t->subject); + if (t->content_markdown) + printf(" content_markdown: %s\n", t->content_markdown); + if (t->last_updated_by) + printf(" last_updated_by: %s\n", t->last_updated_by); +} + +static void print_option( + const wf_lex_tools_ozone_setting_defs_option *o, size_t idx) { + printf("option %zu: key=%s scope=%s description=%s created_at=%s " + "updated_at=%s\n", + idx, o->key ? o->key : "?", + o->scope ? o->scope : "?", + o->has_description && o->description ? o->description : "?", + o->has_created_at && o->created_at ? o->created_at : "?", + o->has_updated_at && o->updated_at ? o->updated_at : "?"); + if (o->did) + printf(" did: %s\n", o->did); + if (o->has_manager_role && o->manager_role) + printf(" manager_role: %s\n", o->manager_role); + if (o->created_by) + printf(" created_by: %s\n", o->created_by); + if (o->last_updated_by) + printf(" last_updated_by: %s\n", o->last_updated_by); +} + +static void print_account( + const wf_lex_com_atproto_admin_defs_account_view *a, size_t idx) { + printf("account %zu: did=%s handle=%s email=%s indexed_at=%s\n", + idx, a->did ? a->did : "?", + a->handle ? a->handle : "?", + a->has_email && a->email ? a->email : "?", + a->indexed_at ? a->indexed_at : "?"); + if (a->has_invites_disabled) + printf(" invites_disabled: %s\n", + a->invites_disabled ? "true" : "false"); + if (a->has_deactivated_at && a->deactivated_at) + printf(" deactivated_at: %s\n", a->deactivated_at); + if (a->has_email_confirmed_at && a->email_confirmed_at) + printf(" email_confirmed_at: %s\n", a->email_confirmed_at); +} + +static void print_report( + const wf_lex_tools_ozone_report_defs_report_view *r, size_t idx) { + printf("report %zu: id=%" PRId64 " status=%s report_type=%s " + "reported_by=%s created_at=%s\n", + idx, r->id, + r->status ? r->status : "?", + r->report_type ? r->report_type : "?", + r->reported_by ? r->reported_by : "?", + r->created_at ? r->created_at : "?"); + if (r->has_comment && r->comment) + printf(" comment: %s\n", r->comment); + if (r->has_updated_at && r->updated_at) + printf(" updated_at: %s\n", r->updated_at); + if (r->has_related_report_count) + printf(" related_report_count: %" PRId64 "\n", + r->related_report_count); + if (r->has_is_muted) + printf(" is_muted: %s\n", r->is_muted ? "true" : "false"); + if (r->has_is_automated) + printf(" is_automated: %s\n", r->is_automated ? "true" : "false"); +} + +static void print_queue( + const wf_lex_tools_ozone_queue_defs_queue_view *q, size_t idx) { + printf("queue %zu: id=%" PRId64 " name=%s enabled=%s " + "created_by=%s created_at=%s updated_at=%s\n", + idx, q->id, + q->name ? q->name : "?", + q->enabled ? "true" : "false", + q->created_by ? q->created_by : "?", + q->created_at ? q->created_at : "?", + q->updated_at ? q->updated_at : "?"); + if (q->has_description && q->description) + printf(" description: %s\n", q->description); + if (q->has_subject_types && q->subject_types.count > 0) { + printf(" subject_types:"); + for (size_t j = 0; j < q->subject_types.count; ++j) + printf(" %s", q->subject_types.items[j]); + printf("\n"); + } + if (q->has_collection && q->collection) + printf(" collection: %s\n", q->collection); + if (q->has_report_types && q->report_types.count > 0) { + printf(" report_types:"); + for (size_t j = 0; j < q->report_types.count; ++j) + printf(" %s", q->report_types.items[j]); + printf("\n"); + } + if (q->has_deleted_at && q->deleted_at) + printf(" deleted_at: %s\n", q->deleted_at); + if (q->stats) { + printf(" stats:"); + if (q->stats->has_pending_count) + printf(" pending=%" PRId64, q->stats->pending_count); + if (q->stats->has_actioned_count) + printf(" actioned=%" PRId64, q->stats->actioned_count); + if (q->stats->has_escalated_count) + printf(" escalated=%" PRId64, q->stats->escalated_count); + if (q->stats->has_inbound_count) + printf(" inbound=%" PRId64, q->stats->inbound_count); + printf("\n"); + } +} + +int cmd_ozone(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + + const char *group = argv[1]; + const char *sub = argv[2]; + + /* moderation query-statuses [--limit N] [--cursor C] */ + if (strcmp(group, "moderation") == 0 && + strcmp(sub, "query-statuses") == 0) { + const char *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone moderation query-statuses " + " [--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_lex_tools_ozone_moderation_query_statuses_main_params params = {0}; + params.has_limit = true; + params.limit = limit; + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_moderation_query_statuses_main_output *out = NULL; + wf_status s = + wf_ozone_moderation_queryStatuses(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: queryStatuses failed (status %d)\n", (int)s); + wf_lex_tools_ozone_moderation_query_statuses_main_output_free( + out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->subject_statuses.count); + for (size_t i = 0; i < out->subject_statuses.count; ++i) + print_subject_status(out->subject_statuses.items[i], i); + if (out->subject_statuses.count == 0) + printf("(no subject statuses)\n"); + + wf_lex_tools_ozone_moderation_query_statuses_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* moderation get-suggestions [--ignore-subjects uri]... [--limit N] */ + if (strcmp(group, "moderation") == 0 && + strcmp(sub, "get-suggestions") == 0) { + const char *ignore_subjects[256]; + size_t ignore_count = 0; + int limit = 50; + const char *cursor = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--ignore-subjects") == 0 && i + 1 < argc) + ignore_subjects[ignore_count++] = argv[++i]; + else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone moderation get-suggestions " + " [--ignore-subjects uri]... " + "[--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + char *out_json = NULL; + wf_status s = wf_ozone_moderation_get_suggestions( + agent, ignore_subjects, ignore_count, limit, cursor, &out_json); + if (s != WF_OK) { + fprintf(stderr, + "error: getSuggestions failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + if (out_json) + printf("%s\n", out_json); + free(out_json); + wf_agent_free(agent); + return 0; + } + + /* moderation get-label-definitions [--uri uri]... */ + if (strcmp(group, "moderation") == 0 && + strcmp(sub, "get-label-definitions") == 0) { + const char *uris[256]; + size_t uri_count = 0; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--uri") == 0 && i + 1 < argc) + uris[uri_count++] = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone moderation get-label-definitions " + " [--uri uri]...\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + char *out_json = NULL; + wf_status s = wf_ozone_moderation_get_label_definitions( + agent, uris, uri_count, &out_json); + if (s != WF_OK) { + fprintf(stderr, + "error: getLabelDefinitions failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + if (out_json) + printf("%s\n", out_json); + free(out_json); + wf_agent_free(agent); + return 0; + } + + /* team list-members [--limit N] [--cursor C] */ + if (strcmp(group, "team") == 0 && strcmp(sub, "list-members") == 0) { + const char *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone team list-members " + " [--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_lex_tools_ozone_team_list_members_main_params params = {0}; + params.has_limit = true; + params.limit = limit; + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_team_list_members_main_output *out = NULL; + wf_status s = wf_ozone_team_listMembers(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: listMembers failed (status %d)\n", (int)s); + wf_lex_tools_ozone_team_list_members_main_output_free(out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->members.count); + for (size_t i = 0; i < out->members.count; ++i) + print_team_member(out->members.items[i], i); + if (out->members.count == 0) + printf("(no team members)\n"); + + wf_lex_tools_ozone_team_list_members_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* server get-config */ + if (strcmp(group, "server") == 0 && strcmp(sub, "get-config") == 0) { + if (argc < 6) { + fprintf(stderr, + "error: usage: wolfram ozone server get-config " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[3], argv[4], argv[5]); + if (!agent) return 1; + + wf_lex_tools_ozone_server_get_config_main_output *out = NULL; + wf_status s = wf_ozone_server_getConfig(agent, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: getConfig failed (status %d)\n", (int)s); + wf_lex_tools_ozone_server_get_config_main_output_free(out); + wf_agent_free(agent); + return 1; + } + + if (out->has_appview && out->appview && out->appview->has_url) + printf("appview: %s\n", out->appview->url); + if (out->has_pds && out->pds && out->pds->has_url) + printf("pds: %s\n", out->pds->url); + if (out->has_blob_divert && out->blob_divert && out->blob_divert->has_url) + printf("blob_divert: %s\n", out->blob_divert->url); + if (out->has_chat && out->chat && out->chat->has_url) + printf("chat: %s\n", out->chat->url); + if (out->has_viewer && out->viewer && out->viewer->has_role) + printf("viewer_role: %s\n", out->viewer->role); + if (out->has_verifier_did && out->verifier_did) + printf("verifier_did: %s\n", out->verifier_did); + + wf_lex_tools_ozone_server_get_config_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* communication list-templates */ + if (strcmp(group, "communication") == 0 && + strcmp(sub, "list-templates") == 0) { + if (argc < 6) { + fprintf(stderr, + "error: usage: wolfram ozone communication list-templates " + " \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(argv[3], argv[4], argv[5]); + if (!agent) return 1; + + wf_lex_tools_ozone_communication_list_templates_main_output *out = NULL; + wf_status s = wf_ozone_communication_listTemplates(agent, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: listTemplates failed (status %d)\n", (int)s); + wf_lex_tools_ozone_communication_list_templates_main_output_free( + out); + wf_agent_free(agent); + return 1; + } + + printf("count: %zu\n", out->communication_templates.count); + for (size_t i = 0; i < out->communication_templates.count; ++i) + print_template(out->communication_templates.items[i], i); + if (out->communication_templates.count == 0) + printf("(no communication templates)\n"); + + wf_lex_tools_ozone_communication_list_templates_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* setting list-options [--key key] [--scope scope] [--limit N] */ + if (strcmp(group, "setting") == 0 && + strcmp(sub, "list-options") == 0) { + const char *keys[256]; + size_t key_count = 0; + const char *scope = NULL; + int limit = 50; + const char *cursor = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) + keys[key_count++] = argv[++i]; + else if (strcmp(argv[i], "--scope") == 0 && i + 1 < argc) + scope = argv[++i]; + else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone setting list-options " + " [--key key] [--scope scope] " + "[--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_lex_tools_ozone_setting_list_options_main_params params = {0}; + params.has_limit = true; + params.limit = limit; + if (key_count > 0) { + params.has_keys = true; + params.keys.items = keys; + params.keys.count = key_count; + } + if (scope) { + params.has_scope = true; + params.scope = scope; + } + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_setting_list_options_main_output *out = NULL; + wf_status s = wf_ozone_setting_listOptions(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: listOptions failed (status %d)\n", (int)s); + wf_lex_tools_ozone_setting_list_options_main_output_free(out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->options.count); + for (size_t i = 0; i < out->options.count; ++i) + print_option(out->options.items[i], i); + if (out->options.count == 0) + printf("(no settings options)\n"); + + wf_lex_tools_ozone_setting_list_options_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* signature search-accounts [--limit N] */ + if (strcmp(group, "signature") == 0 && + strcmp(sub, "search-accounts") == 0) { + if (argc < 6) { + fprintf(stderr, + "error: usage: wolfram ozone signature search-accounts " + " [--limit N]\n"); + return 1; + } + + const char *did = argv[3]; + const char *service = argv[4]; + const char *handle = argv[5]; + const char *password = argv[6]; + int limit = 50; + const char *cursor = NULL; + for (int i = 7; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + } + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + const char *values[] = {did}; + wf_lex_tools_ozone_signature_search_accounts_main_params params = {0}; + params.values.items = values; + params.values.count = 1; + params.has_limit = true; + params.limit = limit; + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_signature_search_accounts_main_output *out = NULL; + wf_status s = + wf_ozone_signature_searchAccounts(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: searchAccounts failed (status %d)\n", (int)s); + wf_lex_tools_ozone_signature_search_accounts_main_output_free( + out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->accounts.count); + for (size_t i = 0; i < out->accounts.count; ++i) + print_account(out->accounts.items[i], i); + if (out->accounts.count == 0) + printf("(no accounts)\n"); + + wf_lex_tools_ozone_signature_search_accounts_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* report query-reports [--limit N] [--cursor C] */ + if (strcmp(group, "report") == 0 && + strcmp(sub, "query-reports") == 0) { + const char *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone report query-reports " + " [--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_lex_tools_ozone_report_query_reports_main_params params = {0}; + params.has_limit = true; + params.limit = limit; + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_report_query_reports_main_output *out = NULL; + wf_status s = wf_ozone_report_queryReports(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: queryReports failed (status %d)\n", (int)s); + wf_lex_tools_ozone_report_query_reports_main_output_free(out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->reports.count); + for (size_t i = 0; i < out->reports.count; ++i) + print_report(out->reports.items[i], i); + if (out->reports.count == 0) + printf("(no reports)\n"); + + wf_lex_tools_ozone_report_query_reports_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + /* queue list-queues [--limit N] [--cursor C] */ + if (strcmp(group, "queue") == 0 && + strcmp(sub, "list-queues") == 0) { + const char *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram ozone queue list-queues " + " [--limit N] [--cursor C]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_lex_tools_ozone_queue_list_queues_main_params params = {0}; + params.has_limit = true; + params.limit = limit; + if (cursor) { + params.has_cursor = true; + params.cursor = cursor; + } + + wf_lex_tools_ozone_queue_list_queues_main_output *out = NULL; + wf_status s = wf_ozone_queue_listQueues(agent, ¶ms, &out); + if (s != WF_OK) { + fprintf(stderr, + "error: listQueues failed (status %d)\n", (int)s); + wf_lex_tools_ozone_queue_list_queues_main_output_free(out); + wf_agent_free(agent); + return 1; + } + + if (out->has_cursor && out->cursor) + printf("cursor: %s\n", out->cursor); + printf("count: %zu\n", out->queues.count); + for (size_t i = 0; i < out->queues.count; ++i) + print_queue(out->queues.items[i], i); + if (out->queues.count == 0) + printf("(no queues)\n"); + + wf_lex_tools_ozone_queue_list_queues_main_output_free(out); + wf_agent_free(agent); + return 0; + } + + fprintf(stderr, + "error: unknown ozone subcommand '%s %s' (try moderation, team, " + "server, communication, setting, signature, report, queue)\n", + group, sub); + return 1; +} diff --git a/src/cli/cli_ozone.h b/src/cli/cli_ozone.h new file mode 100644 index 0000000..940fd94 --- /dev/null +++ b/src/cli/cli_ozone.h @@ -0,0 +1,6 @@ +#ifndef CLI_OZONE_H +#define CLI_OZONE_H + +int cmd_ozone(int argc, char **argv); + +#endif diff --git a/src/cli/cli_post.c b/src/cli/cli_post.c new file mode 100644 index 0000000..8d678a3 --- /dev/null +++ b/src/cli/cli_post.c @@ -0,0 +1,309 @@ +/* + * cli_post.c — the `post` / `reply` / `delete` / `timeline` / `get-post` + * subcommands, split out of main.c as their own self-contained concern. + */ + +#include "cli_post.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/feed_typed.h" +#include "wolfram/thread_typed.h" +#include "wolfram/repo_typed.h" +#include "wolfram/moderation.h" +#include "wolfram/syntax.h" + +#include +#include +#include +#include + +int cmd_post(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + + char *text = join_args(argc, argv, 4); + if (!text) { + fprintf(stderr, "error: failed to assemble post text\n"); + return 1; + } + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + free(text); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + free(text); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result result = {0}; + s = wf_agent_post(agent, text, &result); + free(text); + if (s != WF_OK) { + cli_agent_error("post", s, agent); + wf_agent_free(agent); + return 1; + } + + printf("%s\n", result.uri ? result.uri : "(no uri returned)"); + wf_agent_post_result_free(&result); + wf_agent_free(agent); + return 0; +} + +int cmd_delete(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + wf_status s = wf_agent_delete_post(agent, at_uri); + if (s != WF_OK) { + cli_agent_error("delete", s, agent); + wf_agent_free(agent); + return 1; + } + printf("deleted %s\n", at_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_reply(int argc, char **argv) { + if (argc < 6) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *parent_uri = argv[4]; + + char *text = join_args(argc, argv, 5); + if (!text) { + fprintf(stderr, "error: failed to assemble reply text\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + free(text); + return 1; + } + + char *parent_cid = NULL, *root_uri = NULL, *root_cid = NULL; + wf_status s = resolve_post_for_reply(agent, parent_uri, &parent_cid, + &root_uri, &root_cid); + if (s != WF_OK || !parent_cid || !root_uri || !root_cid) { + fprintf(stderr, + "error: could not resolve reply refs for %s (status %d)\n", + parent_uri, (int)s); + free(parent_cid); + free(root_uri); + free(root_cid); + free(text); + wf_agent_free(agent); + return 1; + } + + char created_at[32]; + now_rfc3339(created_at, sizeof(created_at)); + + cJSON *rec = cJSON_CreateObject(); + cJSON_AddStringToObject(rec, "$type", "app.bsky.feed.post"); + cJSON_AddStringToObject(rec, "text", text); + cJSON_AddStringToObject(rec, "createdAt", created_at); + + cJSON *reply = cJSON_CreateObject(); + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "uri", root_uri); + cJSON_AddStringToObject(root, "cid", root_cid); + cJSON *parent = cJSON_CreateObject(); + cJSON_AddStringToObject(parent, "uri", parent_uri); + cJSON_AddStringToObject(parent, "cid", parent_cid); + cJSON_AddItemToObject(reply, "root", root); + cJSON_AddItemToObject(reply, "parent", parent); + cJSON_AddItemToObject(rec, "reply", reply); + + char *record_json = cJSON_PrintUnformatted(rec); + cJSON_Delete(rec); + free(parent_cid); + free(root_uri); + free(root_cid); + free(text); + if (!record_json) { + fprintf(stderr, "error: failed to serialize reply record\n"); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result out = {0}; + s = wf_agent_create_record(agent, "app.bsky.feed.post", record_json, &out); + free(record_json); + if (s != WF_OK) { + cli_agent_error("reply", s, agent); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +typedef struct { + int printed; +} timeline_ctx; + +static wf_status timeline_on_page(wf_agent *agent, + const wf_agent_feed_list *feed, + const char *cursor, void *ud) { + (void)agent; + (void)cursor; + timeline_ctx *ctx = (timeline_ctx *)ud; + + for (size_t i = 0; i < feed->item_count; ++i) { + const wf_agent_post_view *post = &feed->items[i].post; + const char *author = post->author.handle + ? post->author.handle + : (post->author.did ? post->author.did : "?"); + const char *text = ""; + if (post->record) { + cJSON *t = cJSON_GetObjectItemCaseSensitive(post->record, "text"); + if (cJSON_IsString(t) && t->valuestring) { + text = t->valuestring; + } + } + printf("%s: %s\n", author, text); + ctx->printed++; + } + return WF_OK; +} + +int cmd_timeline(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + int max_pages = (argc >= 5) ? atoi(argv[4]) : 0; /* 0 = until exhausted */ + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + if (g_json) { + wf_response res = {0}; + s = wf_agent_get_timeline(agent, 50, NULL, NULL, &res); + if (s != WF_OK) + fprintf(stderr, "error: request failed (status %d)\n", (int)s); + else if (res.body && res.body_len > 0) + printf("%s\n", res.body); + else + printf("(empty response, HTTP %ld)\n", res.status); + wf_response_free(&res); + wf_agent_free(agent); + return 0; + } + + timeline_ctx ctx = {0}; + char *last_cursor = NULL; + s = wf_agent_get_timeline_paged(agent, 10, max_pages, timeline_on_page, + &ctx, &last_cursor); + if (s != WF_OK) { + fprintf(stderr, "error: timeline failed (status %d)\n", (int)s); + free(last_cursor); + wf_agent_free(agent); + return 1; + } + + if (ctx.printed == 0) { + printf("(timeline empty)\n"); + } + free(last_cursor); + wf_agent_free(agent); + return 0; +} + +int cmd_get_post(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *uri = argv[2]; + + wf_syntax_aturi parsed = {0}; + if (!wf_syntax_aturi_parse(uri, &parsed) || !parsed.authority || + !parsed.collection || !parsed.record_key) { + fprintf(stderr, "error: invalid at-uri: %s\n", uri); + wf_syntax_aturi_free(&parsed); + return 1; + } + + wf_xrpc_client *client = wf_xrpc_client_new(service); + if (!client) { + fprintf(stderr, "error: failed to create XRPC client\n"); + wf_syntax_aturi_free(&parsed); + return 1; + } + + wf_xrpc_param params[] = { + {"repo", parsed.authority}, + {"collection", parsed.collection}, + {"rkey", parsed.record_key}, + }; + + wf_response res = {0}; + wf_status s = wf_xrpc_query_params(client, "com.atproto.repo.getRecord", + params, 3, &res); + wf_syntax_aturi_free(&parsed); + + if (s != WF_OK && s != WF_ERR_HTTP) { + fprintf(stderr, "error: getRecord failed (status %d)\n", (int)s); + wf_response_free(&res); + wf_xrpc_client_free(client); + return 1; + } + + if (res.body && res.body_len > 0) { + printf("%s\n", res.body); + } else { + printf("(empty response, HTTP %ld)\n", res.status); + } + + wf_response_free(&res); + wf_xrpc_client_free(client); + return 0; +} diff --git a/src/cli/cli_post.h b/src/cli/cli_post.h new file mode 100644 index 0000000..a4bfeae --- /dev/null +++ b/src/cli/cli_post.h @@ -0,0 +1,14 @@ +#ifndef WOLFRAM_CLI_POST_H +#define WOLFRAM_CLI_POST_H + +/* `post` / `reply` / `delete` / `timeline` / `get-post` subcommand handlers, + * dispatched from main.c's argv switch. Not part of any installed API -- this + * is the CLI demo program, not the SDK. */ + +int cmd_post(int argc, char **argv); +int cmd_delete(int argc, char **argv); +int cmd_reply(int argc, char **argv); +int cmd_timeline(int argc, char **argv); +int cmd_get_post(int argc, char **argv); + +#endif /* WOLFRAM_CLI_POST_H */ diff --git a/src/cli/cli_prefs.c b/src/cli/cli_prefs.c new file mode 100644 index 0000000..87ffd9e --- /dev/null +++ b/src/cli/cli_prefs.c @@ -0,0 +1,366 @@ +#include "cli_prefs.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/actor_prefs_typed.h" +#include "wolfram/actor_typed.h" +#include "wolfram/repo_typed.h" +#include "wolfram/chat_typed.h" + +#include +#include +#include +#include +#include + +int cmd_preferences(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + if (strcmp(sub, "get") == 0) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[2], argv[3], argv[4]); + if (!agent) return 1; + + char *prefs_json = NULL; + wf_status s = wf_agent_get_preferences(agent, &prefs_json); + if (s != WF_OK) { + fprintf(stderr, "error: getPreferences failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("%s\n", prefs_json ? prefs_json : "(none)"); + free(prefs_json); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "put") == 0) { + const char *prefs_json_str = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--json") == 0 && i + 1 < argc) + prefs_json_str = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !prefs_json_str) { + fprintf(stderr, "error: usage: wolfram preferences put " + " --json \n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = + wf_agent_put_preferences(agent, prefs_json_str, &res); + return finish_agent_response(agent, s, &res); + } + + fprintf(stderr, "error: unknown preferences subcommand '%s' (try " + "get/put)\n", + sub); + return 1; +} + +int cmd_register_push(int argc, char **argv) { + const char *service_did = NULL, *token = NULL, *platform = NULL, + *app_id = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--service-did") == 0 && i + 1 < argc) + service_did = argv[++i]; + else if (strcmp(argv[i], "--token") == 0 && i + 1 < argc) + token = argv[++i]; + else if (strcmp(argv[i], "--platform") == 0 && i + 1 < argc) + platform = argv[++i]; + else if (strcmp(argv[i], "--app-id") == 0 && i + 1 < argc) + app_id = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !service_did || !token) { + fprintf(stderr, "error: usage: wolfram register-push " + " --service-did --token " + " [--platform

] [--app-id ]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s; + if (platform || app_id) { + s = wf_agent_register_push_ext(agent, service_did, token, platform, + app_id, &res); + } else { + s = wf_agent_register_push(agent, service_did, token, &res); + } + return finish_agent_response(agent, s, &res); +} + +int cmd_unregister_push(int argc, char **argv) { + const char *service_did = NULL, *token = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--service-did") == 0 && i + 1 < argc) + service_did = argv[++i]; + else if (strcmp(argv[i], "--token") == 0 && i + 1 < argc) + token = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !service_did || !token) { + fprintf(stderr, "error: usage: wolfram unregister-push " + " --service-did --token " + "\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_unregister_push(agent, service_did, token, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_update_profile(int argc, char **argv) { + wf_agent_profile_update update = {0}; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--display-name") == 0 && i + 1 < argc) + update.display_name = argv[++i]; + else if (strcmp(argv[i], "--description") == 0 && i + 1 < argc) + update.description = argv[++i]; + else if (strcmp(argv[i], "--avatar-cid") == 0 && i + 1 < argc) + update.avatar_cid = argv[++i]; + else if (strcmp(argv[i], "--banner-cid") == 0 && i + 1 < argc) + update.banner_cid = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + usage_stream(stderr); + return 0; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_update_profile(agent, &update); + if (s != WF_OK) { + fprintf(stderr, "error: updateProfile failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("profile updated\n"); + wf_agent_free(agent); + return 0; +} + +int cmd_put_actor_status(int argc, char **argv) { + const char *status = NULL, *embed_json = NULL; + int duration = 0; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--duration") == 0 && i + 1 < argc) + duration = atoi(argv[++i]); + else if (strcmp(argv[i], "--embed") == 0 && i + 1 < argc) + embed_json = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + else if (!status) + status = argv[i]; + } + if (pi < 3 || !status) { + fprintf(stderr, "error: usage: wolfram put-actor-status " + " [--duration ] " + "[--embed ]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_agent_post_result out = {0}; + wf_status s = wf_agent_put_actor_status(agent, status, duration, + embed_json, &out); + if (s != WF_OK) { + fprintf(stderr, "error: putActorStatus failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_upload_blob(int argc, char **argv) { + const char *content_type = NULL; + const char *pos[3]; + const char *file_path = NULL; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--content-type") == 0 && i + 1 < argc) + content_type = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + else if (!file_path) + file_path = argv[i]; + } + if (pi < 3 || !file_path) { + fprintf(stderr, "error: usage: wolfram upload-blob " + " [--content-type ]\n"); + return 1; + } + + char *data = read_text_file(pos[2] ? pos[2] : file_path); + /* Re-read the actual file, not the password */ + data = read_text_file(file_path); + if (!data) { + fprintf(stderr, "error: could not read file '%s'\n", file_path); + return 1; + } + + /* Determine file size */ + FILE *f = fopen(file_path, "rb"); + if (!f) { + fprintf(stderr, "error: could not open file '%s'\n", file_path); + free(data); + return 1; + } + fseek(f, 0, SEEK_END); + long file_len = ftell(f); + fclose(f); + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) { + free(data); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_upload_blob(agent, data, (size_t)file_len, + content_type, &res); + free(data); + return finish_agent_response(agent, s, &res); +} + +int cmd_apply_writes(int argc, char **argv) { + const char *repo = NULL, *writes_json = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--repo") == 0 && i + 1 < argc) + repo = argv[++i]; + else if (strcmp(argv[i], "--writes-json") == 0 && i + 1 < argc) + writes_json = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !repo || !writes_json) { + fprintf(stderr, "error: usage: wolfram apply-writes " + " --repo --writes-json \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + /* Parse the writes JSON into wf_agent_write array */ + cJSON *writes_arr = cJSON_Parse(writes_json); + if (!writes_arr || !cJSON_IsArray(writes_arr)) { + fprintf(stderr, "error: --writes-json must be a JSON array\n"); + cJSON_Delete(writes_arr); + wf_agent_free(agent); + return 1; + } + + int n_writes = cJSON_GetArraySize(writes_arr); + wf_agent_write *writes = calloc(n_writes, sizeof(wf_agent_write)); + if (!writes) { + cJSON_Delete(writes_arr); + wf_agent_free(agent); + return 1; + } + + for (int i = 0; i < n_writes; ++i) { + cJSON *w = cJSON_GetArrayItem(writes_arr, i); + cJSON *type = cJSON_GetObjectItemCaseSensitive(w, "$type"); + cJSON *collection = cJSON_GetObjectItemCaseSensitive(w, "collection"); + cJSON *rkey = cJSON_GetObjectItemCaseSensitive(w, "rkey"); + cJSON *value = cJSON_GetObjectItemCaseSensitive(w, "value"); + if (cJSON_IsString(type) && type->valuestring) { + if (strcmp(type->valuestring, "create") == 0) + writes[i].type = WF_AGENT_WRITE_CREATE; + else if (strcmp(type->valuestring, "update") == 0) + writes[i].type = WF_AGENT_WRITE_UPDATE; + else if (strcmp(type->valuestring, "delete") == 0) + writes[i].type = WF_AGENT_WRITE_DELETE; + } + if (cJSON_IsString(collection) && collection->valuestring) + writes[i].collection = collection->valuestring; + if (cJSON_IsString(rkey) && rkey->valuestring) + writes[i].rkey = rkey->valuestring; + if (value) { + writes[i].value_json = cJSON_PrintUnformatted(value); + } + } + + wf_response res = {0}; + wf_status s = wf_agent_apply_writes(agent, writes, n_writes, &res); + + /* Free allocated values */ + for (int i = 0; i < n_writes; ++i) { + free((void *)writes[i].value_json); + } + free(writes); + cJSON_Delete(writes_arr); + + return finish_agent_response(agent, s, &res); +} + +int cmd_send_interactions(int argc, char **argv) { + const char *feed_uri = NULL, *interactions_json = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--feed") == 0 && i + 1 < argc) + feed_uri = argv[++i]; + else if (strcmp(argv[i], "--interactions-json") == 0 && i + 1 < argc) + interactions_json = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !interactions_json) { + fprintf(stderr, "error: usage: wolfram send-interactions " + " [--feed ] " + "--interactions-json \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_send_interactions(agent, feed_uri, + interactions_json, &res); + return finish_agent_response(agent, s, &res); +} diff --git a/src/cli/cli_prefs.h b/src/cli/cli_prefs.h new file mode 100644 index 0000000..5adb3ca --- /dev/null +++ b/src/cli/cli_prefs.h @@ -0,0 +1,13 @@ +#ifndef CLI_PREFS_H +#define CLI_PREFS_H + +int cmd_preferences(int argc, char **argv); +int cmd_register_push(int argc, char **argv); +int cmd_unregister_push(int argc, char **argv); +int cmd_update_profile(int argc, char **argv); +int cmd_put_actor_status(int argc, char **argv); +int cmd_upload_blob(int argc, char **argv); +int cmd_apply_writes(int argc, char **argv); +int cmd_send_interactions(int argc, char **argv); + +#endif diff --git a/src/cli/cli_repo.c b/src/cli/cli_repo.c new file mode 100644 index 0000000..61183d6 --- /dev/null +++ b/src/cli/cli_repo.c @@ -0,0 +1,271 @@ +#include "cli_repo.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/repo_typed.h" +#include "wolfram/sync_typed.h" +#include "wolfram/xrpc.h" + +#include +#include +#include +#include +#include + +int cmd_repo(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[1]; + + /* wolfram repo put-record --collection + * --rkey --json */ + if (strcmp(sub, "put-record") == 0) { + const char *collection = NULL, *rkey = NULL, *json = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) + collection = argv[++i]; + else if (strcmp(argv[i], "--rkey") == 0 && i + 1 < argc) + rkey = argv[++i]; + else if (strcmp(argv[i], "--json") == 0 && i + 1 < argc) + json = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !collection || !rkey || !json) { + fprintf(stderr, + "error: usage: wolfram repo put-record " + " --collection --rkey " + "--json \n"); + return 1; + } + const char *record_json = json; + char *file_buf = NULL; + if (json[0] != '{') { + file_buf = read_text_file(json); + if (!file_buf) { + fprintf(stderr, "error: cannot read record file '%s'\n", json); + return 1; + } + record_json = file_buf; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) { + free(file_buf); + return 1; + } + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + const char *repo = sd.did ? sd.did : pos[1]; + wf_repo_write_record_result out = {0}; + wf_status s = wf_agent_put_record_typed( + agent, repo, collection, rkey, -1, record_json, NULL, NULL, &out); + free(file_buf); + wf_agent_session_data_free(&sd); + if (s != WF_OK) { + fprintf(stderr, "error: putRecord failed (status %d)\n", (int)s); + wf_repo_write_record_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_repo_write_record_result_free(&out); + wf_agent_free(agent); + return 0; + } + + /* wolfram repo delete-record --collection + * + * --rkey */ + if (strcmp(sub, "delete-record") == 0) { + const char *collection = NULL, *rkey = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) + collection = argv[++i]; + else if (strcmp(argv[i], "--rkey") == 0 && i + 1 < argc) + rkey = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !collection || !rkey) { + fprintf(stderr, + "error: usage: wolfram repo delete-record " + " --collection --rkey \n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + const char *repo = sd.did ? sd.did : pos[1]; + wf_status s = wf_agent_delete_record_typed(agent, repo, collection, + rkey, NULL, NULL); + wf_agent_session_data_free(&sd); + if (s != WF_OK) { + fprintf(stderr, "error: deleteRecord failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("deleted %s/%s/%s\n", repo, collection, rkey); + wf_agent_free(agent); + return 0; + } + + /* wolfram repo list-records --collection + * + * [--limit N] [--cursor C] */ + if (strcmp(sub, "list-records") == 0) { + const char *collection = NULL, *cursor = NULL; + int limit = 50; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) + collection = argv[++i]; + else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) + limit = atoi(argv[++i]); + else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !collection) { + fprintf(stderr, + "error: usage: wolfram repo list-records " + " --collection [--limit N] " + "[--cursor C]\n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + const char *repo = sd.did ? sd.did : pos[1]; + if (g_json) { + wf_response res = {0}; + wf_status s = + wf_agent_list_records(agent, collection, limit, cursor, &res); + wf_agent_session_data_free(&sd); + return finish_agent_response(agent, s, &res); + } + wf_repo_record_list list = {0}; + wf_status s = wf_agent_list_records_typed(agent, repo, collection, + limit, cursor, 0, &list); + wf_agent_session_data_free(&sd); + if (s != WF_OK) { + fprintf(stderr, "error: listRecords failed (status %d)\n", (int)s); + wf_repo_record_list_free(&list); + wf_agent_free(agent); + return 1; + } + for (size_t i = 0; i < list.count; ++i) { + const wf_repo_record *r = &list.items[i]; + printf("%s (cid=%s)\n", r->uri ? r->uri : "?", + r->has_cid && r->cid ? r->cid : "?"); + } + if (list.cursor) printf("cursor: %s\n", list.cursor); + if (list.count == 0) printf("(no records)\n"); + wf_repo_record_list_free(&list); + wf_agent_free(agent); + return 0; + } + + /* wolfram repo describe --repo + * */ + if (strcmp(sub, "describe") == 0) { + const char *repo = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--repo") == 0 && i + 1 < argc) + repo = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !repo) { + fprintf(stderr, "error: usage: wolfram repo describe " + " --repo \n"); + return 1; + } + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + if (g_json) { + wf_response res = {0}; + wf_status s = wf_agent_describe_repo(agent, repo, &res); + return finish_agent_response(agent, s, &res); + } + wf_repo_description desc = {0}; + wf_status s = wf_agent_describe_repo_typed(agent, repo, &desc); + if (s != WF_OK) { + fprintf(stderr, "error: describeRepo failed (status %d)\n", (int)s); + wf_repo_description_free(&desc); + wf_agent_free(agent); + return 1; + } + printf("handle: %s\n", desc.handle ? desc.handle : "?"); + printf("did: %s\n", desc.did ? desc.did : "?"); + printf("handleIsCorrect: %d\n", desc.handle_is_correct); + printf("collections (%zu):\n", desc.collection_count); + for (size_t i = 0; i < desc.collection_count; ++i) { + printf(" %s\n", desc.collections[i] ? desc.collections[i] : "?"); + } + wf_repo_description_free(&desc); + wf_agent_free(agent); + return 0; + } + + fprintf(stderr, + "error: unknown repo subcommand '%s' " + "(try put-record/delete-record/list-records/describe)\n", + sub); + return 1; +} + +int cmd_get_record(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *repo = argv[2]; + const char *collection = argv[3]; + const char *rkey = argv[4]; + + wf_xrpc_client *client = wf_xrpc_client_new(service); + if (!client) { + fprintf(stderr, "error: failed to create XRPC client\n"); + return 1; + } + + wf_xrpc_param params[] = { + {"repo", repo}, + {"collection", collection}, + {"rkey", rkey}, + }; + + wf_response res = {0}; + wf_status s = + wf_xrpc_query_params(client, "com.atproto.repo.getRecord", params, 3, + &res); + wf_xrpc_client_free(client); + + if (s != WF_OK && s != WF_ERR_HTTP) { + fprintf(stderr, "error: getRecord failed (status %d)\n", (int)s); + wf_response_free(&res); + return 1; + } + + if (res.body && res.body_len > 0) { + printf("%s\n", res.body); + } else { + printf("(empty response, HTTP %ld)\n", res.status); + } + + wf_response_free(&res); + return 0; +} \ No newline at end of file diff --git a/src/cli/cli_repo.h b/src/cli/cli_repo.h new file mode 100644 index 0000000..c5f0115 --- /dev/null +++ b/src/cli/cli_repo.h @@ -0,0 +1,7 @@ +#ifndef WOLFRAM_CLI_REPO_H +#define WOLFRAM_CLI_REPO_H + +int cmd_repo(int argc, char **argv); +int cmd_get_record(int argc, char **argv); + +#endif \ No newline at end of file diff --git a/src/cli/cli_search.c b/src/cli/cli_search.c new file mode 100644 index 0000000..e65aa0c --- /dev/null +++ b/src/cli/cli_search.c @@ -0,0 +1,368 @@ +#include "cli_search.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/feed_typed.h" +#include "wolfram/moderation.h" +#include "wolfram/moderation_report_typed.h" +#include "wolfram/label.h" +#include "wolfram/server_typed.h" +#include "wolfram/temp_typed.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define CLI_LABEL_MAX_EVENTS 100 + +static volatile sig_atomic_t g_label_stop = 0; +static int g_label_count = 0; +static time_t g_label_start = 0; +static int g_label_seconds = 30; +static wf_label_subscribe_handle **g_label_handle_ptr = NULL; + +static void label_on_sigint(int sig) { + (void)sig; + g_label_stop = 1; + if (g_label_handle_ptr && *g_label_handle_ptr) { + wf_label_subscribe_stop(*g_label_handle_ptr); + } +} + +static void label_on_label(const wf_label *label, void *userdata) { + (void)userdata; + g_label_count++; + printf("label: uri=%s cid=%s val=%s src=%s exp=%s neg=%d\n", + label->uri ? label->uri : "", label->cid ? label->cid : "", + label->val ? label->val : "", label->src ? label->src : "", + label->exp ? label->exp : "", label->neg); + fflush(stdout); + if (g_label_stop || + (g_label_seconds > 0 && + (time(NULL) - g_label_start) >= g_label_seconds) || + (g_label_handle_ptr && *g_label_handle_ptr && + g_label_count >= CLI_LABEL_MAX_EVENTS)) { + if (g_label_handle_ptr && *g_label_handle_ptr) { + wf_label_subscribe_stop(*g_label_handle_ptr); + } + } +} + +static void label_on_info(const wf_label_info *info, void *userdata) { + (void)userdata; + printf("info: name=%s message=%s\n", info->name ? info->name : "", + info->message ? info->message : "(none)"); + fflush(stdout); +} + +static void label_on_error(wf_status status, const char *msg, void *userdata) { + (void)userdata; + fprintf(stderr, "error: label stream status=%d %s\n", (int)status, + msg ? msg : ""); +} + +int cmd_moderation(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + + if (strcmp(argv[1], "report") == 0) { + const char *subject = NULL, *reason = NULL, *reason_type = NULL, + *cid = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--subject") == 0 && i + 1 < argc) + subject = argv[++i]; + else if (strcmp(argv[i], "--reason") == 0 && i + 1 < argc) + reason = argv[++i]; + else if (strcmp(argv[i], "--reason-type") == 0 && i + 1 < argc) + reason_type = argv[++i]; + else if (strcmp(argv[i], "--cid") == 0 && i + 1 < argc) + cid = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !subject || !reason) { + fprintf(stderr, + "error: usage: wolfram moderation report " + " --subject --reason " + "[--reason-type ] [--cid ]\n"); + return 1; + } + + const char *rt = reason_type ? reason_type : reason; + const char *free_reason = reason_type ? reason : NULL; + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + const char *subject_did = NULL; + const char *subject_uri = NULL; + const char *subject_cid = NULL; + if (strncmp(subject, "did:", 4) == 0) { + subject_did = subject; + } else { + subject_uri = subject; + subject_cid = cid; + } + + wf_moderation_report_record out = {0}; + wf_status s = + wf_agent_report_typed(agent, subject_did, subject_uri, subject_cid, + rt, free_reason, NULL, NULL, &out); + if (s != WF_OK) { + fprintf(stderr, "error: createReport failed (status %d)\n", (int)s); + wf_moderation_report_record_free(&out); + wf_agent_free(agent); + return 1; + } + printf("report id=%" PRId64 "\n", out.id); + if (out.reason_type) printf("reasonType: %s\n", out.reason_type); + if (out.reported_by) printf("reportedBy: %s\n", out.reported_by); + wf_moderation_report_record_free(&out); + wf_agent_free(agent); + return 0; + } + + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + char *did = NULL; + wf_status rs = resolve_actor_to_did(agent, actor, &did); + if (rs != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)rs); + wf_agent_free(agent); + return 1; + } + + wf_mod_decision *decision = NULL; + wf_status s = wf_agent_moderate_profile(agent, did, &decision); + free(did); + if (s != WF_OK || !decision) { + fprintf(stderr, "error: moderation failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + wf_mod_ui ui = {0}; + s = wf_mod_decision_ui(decision, WF_MOD_CTX_PROFILE_VIEW, &ui); + if (s != WF_OK) { + fprintf(stderr, "error: decision UI failed (status %d)\n", (int)s); + wf_mod_decision_free(decision); + wf_agent_free(agent); + return 1; + } + + printf("moderation for %s: alerts=%zu blurs=%zu informs=%zu\n", + decision->did ? decision->did : actor, ui.alert_count, ui.blur_count, + ui.inform_count); + + wf_mod_ui_free(&ui); + wf_mod_decision_free(decision); + wf_agent_free(agent); + return 0; +} + +int cmd_search(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *query = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 25; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_search_posts(agent, query, limit, NULL, NULL, NULL, NULL, NULL, + &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_search_actors(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *query = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 25; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_search_actors(agent, query, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_search_typeahead(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *query = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 10; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_search_actors_typeahead(agent, query, limit, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_search_starter_packs(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *query = argv[4]; + int limit = (argc >= 6) ? atoi(argv[5]) : 25; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_search_starter_packs(agent, query, limit, NULL, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_labels(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + + const char *sub = argv[1]; + if (strcmp(sub, "subscribe") != 0) { + fprintf(stderr, + "error: unknown labels subcommand '%s' (try 'subscribe')\n", + sub); + return 1; + } + + const char *service = argv[2]; + + int64_t cursor = 0; + int has_cursor = 0; + for (int i = 3; i < argc; ++i) { + if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) { + cursor = (int64_t)strtoll(argv[++i], NULL, 10); + has_cursor = 1; + } else if (strcmp(argv[i], "--seconds") == 0 && i + 1 < argc) { + g_label_seconds = atoi(argv[++i]); + } else { + fprintf(stderr, "error: unexpected labels argument '%s'\n", + argv[i]); + return 1; + } + } + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = label_on_sigint; + sigaction(SIGINT, &sa, NULL); + + g_label_stop = 0; + g_label_count = 0; + g_label_start = time(NULL); + + wf_label_subscribe_options opts; + memset(&opts, 0, sizeof(opts)); + opts.service = service; + opts.cursor = cursor; + opts.has_cursor = has_cursor; + opts.reconnect_delay_ms = 1000; + opts.on_label = label_on_label; + opts.on_neg = label_on_label; + opts.on_info = label_on_info; + opts.on_error = label_on_error; + + printf( + "subscribing to label stream at %s (cursor=%s, max %d events, %ds)\n", + service, has_cursor ? argv[2] : "none", CLI_LABEL_MAX_EVENTS, + g_label_seconds); + + wf_label_subscribe_handle *handle = NULL; + g_label_handle_ptr = &handle; + wf_status s = wf_label_subscribe_start(&opts, &handle); + g_label_handle_ptr = NULL; + + if (s != WF_OK) { + fprintf(stderr, "error: label subscription ended (status %d)\n", + (int)s); + return 1; + } + printf("label subscription ended cleanly (%d labels)\n", g_label_count); + return 0; +} + +int cmd_revoke_account_credentials(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *account = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_status s = wf_agent_revoke_account_credentials(agent, account); + if (s != WF_OK) { + fprintf(stderr, "error: revokeAccountCredentials failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("revoked credentials for %s\n", account); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_search.h b/src/cli/cli_search.h new file mode 100644 index 0000000..0c44ba2 --- /dev/null +++ b/src/cli/cli_search.h @@ -0,0 +1,12 @@ +#ifndef WOLFRAM_CLI_SEARCH_H +#define WOLFRAM_CLI_SEARCH_H + +int cmd_search(int argc, char **argv); +int cmd_search_actors(int argc, char **argv); +int cmd_search_typeahead(int argc, char **argv); +int cmd_search_starter_packs(int argc, char **argv); +int cmd_moderation(int argc, char **argv); +int cmd_labels(int argc, char **argv); +int cmd_revoke_account_credentials(int argc, char **argv); + +#endif diff --git a/src/cli/cli_server.c b/src/cli/cli_server.c new file mode 100644 index 0000000..f8b243b --- /dev/null +++ b/src/cli/cli_server.c @@ -0,0 +1,420 @@ +#include "cli_server.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/server.h" +#include "wolfram/server_typed.h" +#include "wolfram/identity_typed.h" +#include "wolfram/plc.h" + +#include +#include +#include +#include +#include + +int cmd_create_account(int argc, char **argv) { + const char *handle = NULL, *password = NULL, *email = NULL, + *invite_code = NULL; + const char *service = NULL; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--handle") == 0 && i + 1 < argc) + handle = argv[++i]; + else if (strcmp(argv[i], "--password") == 0 && i + 1 < argc) + password = argv[++i]; + else if (strcmp(argv[i], "--email") == 0 && i + 1 < argc) + email = argv[++i]; + else if (strcmp(argv[i], "--invite-code") == 0 && i + 1 < argc) + invite_code = argv[++i]; + else if (!service) + service = argv[i]; + } + if (!service || !handle || !password || !email) { + fprintf(stderr, "error: usage: wolfram create-account " + "--handle --password --email " + " [--invite-code ]\n"); + return 1; + } + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_server_create_account_input input = { + .handle = handle, + .email = email, + .password = password, + .invite_code = invite_code, + }; + wf_server_create_account_result result = {0}; + wf_status s = wf_agent_create_account_typed(agent, &input, &result); + if (s != WF_OK) { + fprintf(stderr, "error: createAccount failed (status %d)\n", (int)s); + wf_server_create_account_result_free(&result); + wf_agent_free(agent); + return 1; + } + printf("account created: %s (%s)\n", handle, + result.did ? result.did : "?"); + wf_server_create_account_result_free(&result); + wf_agent_free(agent); + return 0; +} + +int cmd_app_password(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *sub = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + if (strcmp(sub, "create") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram app-password " + " create \n"); + wf_agent_free(agent); + return 1; + } + wf_server_app_password out = {0}; + wf_status s = + wf_agent_create_app_password_typed(agent, argv[5], 0, &out); + if (s != WF_OK) { + fprintf(stderr, "error: createAppPassword failed (status %d)\n", + (int)s); + wf_server_app_password_free(&out); + wf_agent_free(agent); + return 1; + } + printf("name=%s password=%s\n", out.name ? out.name : "?", + out.password ? out.password : "?"); + wf_server_app_password_free(&out); + } else if (strcmp(sub, "list") == 0) { + wf_server_app_password_list list = {0}; + wf_status s = wf_agent_list_app_passwords_typed(agent, &list); + if (s != WF_OK) { + fprintf(stderr, "error: listAppPasswords failed (status %d)\n", + (int)s); + wf_server_app_password_list_free(&list); + wf_agent_free(agent); + return 1; + } + for (size_t i = 0; i < list.password_count; ++i) { + printf("name=%s created=%s\n", + list.passwords[i].name ? list.passwords[i].name : "?", + list.passwords[i].created_at + ? list.passwords[i].created_at + : "?"); + } + if (list.password_count == 0) printf("(no app passwords)\n"); + wf_server_app_password_list_free(&list); + } else if (strcmp(sub, "revoke") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram app-password " + " revoke \n"); + wf_agent_free(agent); + return 1; + } + wf_status s = wf_agent_revoke_app_password(agent, argv[5]); + if (s != WF_OK) { + fprintf(stderr, "error: revokeAppPassword failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("revoked app password '%s'\n", argv[5]); + } else { + fprintf(stderr, "error: unknown app-password subcommand '%s' (try " + "create/list/revoke)\n", + sub); + wf_agent_free(agent); + return 1; + } + wf_agent_free(agent); + return 0; +} + +int cmd_invite_codes(int argc, char **argv) { + const char *pos[3]; + int pi = 0; + int create_count = 0; + int use_count = 1; + bool do_create = false; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--create") == 0 && i + 1 < argc) { + create_count = atoi(argv[++i]); + do_create = true; + } else if (strcmp(argv[i], "--use-count") == 0 && i + 1 < argc) { + use_count = atoi(argv[++i]); + } else if (pi < 3) { + pos[pi++] = argv[i]; + } + } + if (pi < 3) { + usage_stream(stderr); + return 0; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + if (do_create) { + for (int i = 0; i < create_count; ++i) { + wf_response res = {0}; + wf_status s = wf_agent_create_invite_code(agent, use_count, &res); + if (s != WF_OK) { + fprintf(stderr, "error: createInviteCode failed (status %d)\n", + (int)s); + wf_response_free(&res); + wf_agent_free(agent); + return 1; + } + printf("%s\n", res.body ? res.body : "(no result)"); + wf_response_free(&res); + } + printf("created %d invite codes (use-count %d)\n", create_count, + use_count); + } else { + wf_response res = {0}; + wf_status s = wf_agent_get_account_invite_codes(agent, 50, 0, &res); + return finish_agent_response(agent, s, &res); + } + wf_agent_free(agent); + return 0; +} + +int cmd_activate(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_activate_account(agent, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_deactivate(int argc, char **argv) { + const char *delete_after = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--delete-after") == 0 && i + 1 < argc) + delete_after = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + usage_stream(stderr); + return 0; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_deactivate_account_typed(agent, delete_after); + if (s != WF_OK) { + fprintf(stderr, "error: deactivateAccount failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("account deactivated\n"); + wf_agent_free(agent); + return 0; +} + +int cmd_check_status(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_check_account_status(agent, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_email(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *sub = argv[4]; + + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + if (strcmp(sub, "confirm") == 0) { + if (argc < 7) { + fprintf(stderr, "error: usage: wolfram email " + " confirm \n"); + wf_agent_free(agent); + return 1; + } + wf_status s = wf_agent_confirm_email_typed(agent, argv[5], argv[6]); + if (s != WF_OK) { + fprintf(stderr, "error: confirmEmail failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("email confirmed\n"); + } else if (strcmp(sub, "update") == 0) { + if (argc < 6) { + fprintf(stderr, "error: usage: wolfram email " + " update \n"); + wf_agent_free(agent); + return 1; + } + wf_status s = wf_agent_update_email_typed(agent, argv[5], NULL, 0); + if (s != WF_OK) { + fprintf(stderr, "error: updateEmail failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("email update requested\n"); + } else { + fprintf(stderr, "error: unknown email subcommand '%s' (try " + "confirm/update)\n", + sub); + wf_agent_free(agent); + return 1; + } + wf_agent_free(agent); + return 0; +} + +int cmd_password_reset(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *email = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_request_password_reset_typed(agent, email); + if (s != WF_OK) { + fprintf(stderr, "error: requestPasswordReset failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("password reset requested for %s\n", email); + wf_agent_free(agent); + return 0; +} + +int cmd_reserve_signing_key(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + const char *did = (argc >= 5) ? argv[4] : NULL; + wf_response res = {0}; + wf_status s = wf_agent_reserve_signing_key_typed(agent, did, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_get_service_auth(int argc, char **argv) { + const char *aud = NULL, *lxm = NULL; + int exp = 0; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--aud") == 0 && i + 1 < argc) + aud = argv[++i]; + else if (strcmp(argv[i], "--exp") == 0 && i + 1 < argc) + exp = atoi(argv[++i]); + else if (strcmp(argv[i], "--lxm") == 0 && i + 1 < argc) + lxm = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3 || !aud) { + fprintf(stderr, "error: usage: wolfram get-service-auth " + " --aud [--exp ] " + "[--lxm ]\n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = + wf_agent_get_service_auth_typed(agent, aud, exp, lxm, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_request_account_delete(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_status s = wf_agent_request_account_delete_typed(agent); + if (s != WF_OK) { + fprintf(stderr, "error: requestAccountDelete failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("account deletion requested\n"); + wf_agent_free(agent); + return 0; +} + +int cmd_request_email_update(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_response res = {0}; + wf_status s = wf_agent_request_email_update_typed(agent, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_request_email_confirmation(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + wf_agent *agent = agent_login_or_err(argv[1], argv[2], argv[3]); + if (!agent) return 1; + + wf_status s = wf_agent_request_email_confirmation_typed(agent); + if (s != WF_OK) { + fprintf(stderr, "error: requestEmailConfirmation failed (status %d)\n", + (int)s); + wf_agent_free(agent); + return 1; + } + printf("email confirmation requested\n"); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_server.h b/src/cli/cli_server.h new file mode 100644 index 0000000..e5e9c48 --- /dev/null +++ b/src/cli/cli_server.h @@ -0,0 +1,18 @@ +#ifndef CLI_SERVER_H +#define CLI_SERVER_H + +int cmd_create_account(int argc, char **argv); +int cmd_app_password(int argc, char **argv); +int cmd_invite_codes(int argc, char **argv); +int cmd_activate(int argc, char **argv); +int cmd_deactivate(int argc, char **argv); +int cmd_check_status(int argc, char **argv); +int cmd_email(int argc, char **argv); +int cmd_password_reset(int argc, char **argv); +int cmd_reserve_signing_key(int argc, char **argv); +int cmd_get_service_auth(int argc, char **argv); +int cmd_request_account_delete(int argc, char **argv); +int cmd_request_email_update(int argc, char **argv); +int cmd_request_email_confirmation(int argc, char **argv); + +#endif diff --git a/src/cli/cli_social.c b/src/cli/cli_social.c new file mode 100644 index 0000000..d22b7f2 --- /dev/null +++ b/src/cli/cli_social.c @@ -0,0 +1,573 @@ +/* + * cli_social.c — the `profile` / `follow` / `unfollow` / `like` / `unlike` / + * `repost` / `delete-repost` / `mute` / `unmute` / `block` / `unblock` / + * `resolve` subcommands, split out of main.c as their own self-contained + * concern. + */ + +#include "cli_social.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/graph_typed.h" +#include "wolfram/graph_social_typed.h" +#include "wolfram/graph_write.h" +#include "wolfram/identity_typed.h" +#include "wolfram/label.h" +#include "wolfram/syntax.h" + +#include +#include +#include +#include +#include + +int cmd_profile(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *actor = argv[2]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_agent_profile prof = {0}; + wf_status s = wf_agent_get_profile(agent, actor, &prof); + if (s != WF_OK) { + fprintf(stderr, "error: getProfile failed (status %d)\n", (int)s); + wf_agent_profile_free(&prof); + wf_agent_free(agent); + return 1; + } + + if (g_json) { + cJSON *j = cJSON_CreateObject(); + if (j) { + if (prof.did) cJSON_AddStringToObject(j, "did", prof.did); + if (prof.handle) cJSON_AddStringToObject(j, "handle", prof.handle); + if (prof.display_name) + cJSON_AddStringToObject(j, "displayName", prof.display_name); + if (prof.description) + cJSON_AddStringToObject(j, "description", prof.description); + if (prof.avatar_cid) + cJSON_AddStringToObject(j, "avatar", prof.avatar_cid); + cJSON_AddNumberToObject(j, "followersCount", prof.followers_count); + cJSON_AddNumberToObject(j, "followsCount", prof.follows_count); + cJSON_AddNumberToObject(j, "postsCount", prof.posts_count); + char *out = cJSON_PrintUnformatted(j); + cJSON_Delete(j); + if (out) { + printf("%s\n", out); + free(out); + } + } + wf_agent_profile_free(&prof); + wf_agent_free(agent); + return 0; + } + + printf("%s\n", prof.display_name ? prof.display_name : "(no display name)"); + if (prof.description) { + printf("%s\n", prof.description); + } + wf_agent_profile_free(&prof); + wf_agent_free(agent); + return 0; +} + +int cmd_follow(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + char *subject_did = NULL; + s = resolve_actor_to_did(agent, actor, &subject_did); + if (s != WF_OK || !subject_did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result result = {0}; + s = wf_agent_follow(agent, subject_did, &result); + free(subject_did); + if (s != WF_OK) { + fprintf(stderr, "error: follow failed (status %d)\n", (int)s); + wf_agent_post_result_free(&result); + wf_agent_free(agent); + return 1; + } + + printf("followed: %s\n", result.uri ? result.uri : "(no uri returned)"); + wf_agent_post_result_free(&result); + wf_agent_free(agent); + return 0; +} + +int cmd_unfollow(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + char *subject_did = NULL; + s = resolve_actor_to_did(agent, actor, &subject_did); + if (s != WF_OK || !subject_did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + /* Look up the existing follow URI via the viewer's relationship state. */ + wf_session_data sd = {0}; + wf_agent_get_session_data(agent, &sd); + const char *others[1] = {subject_did}; + wf_response res = {0}; + s = wf_agent_get_relationships(agent, sd.did, others, 1, &res); + wf_agent_session_data_free(&sd); + free(subject_did); + + if (s != WF_OK && s != WF_ERR_HTTP) { + fprintf(stderr, "error: getRelationships failed (status %d)\n", (int)s); + wf_response_free(&res); + wf_agent_free(agent); + return 1; + } + + char *follow_uri = NULL; + if (res.body) { + cJSON *root = cJSON_ParseWithLength(res.body, res.body_len); + if (root) { + cJSON *rels = + cJSON_GetObjectItemCaseSensitive(root, "relationships"); + if (cJSON_IsArray(rels) && cJSON_GetArraySize(rels) > 0) { + cJSON *rel = cJSON_GetArrayItem(rels, 0); + cJSON *following = + cJSON_GetObjectItemCaseSensitive(rel, "following"); + if (cJSON_IsString(following) && following->valuestring) { + follow_uri = strdup(following->valuestring); + } + } + cJSON_Delete(root); + } + } + wf_response_free(&res); + + if (!follow_uri) { + printf("(not currently following %s)\n", actor); + wf_agent_free(agent); + return 0; + } + + s = wf_agent_unfollow(agent, follow_uri); + free(follow_uri); + if (s != WF_OK) { + fprintf(stderr, "error: unfollow failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("unfollowed %s\n", actor); + wf_agent_free(agent); + return 0; +} + +int cmd_like(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + char *cid = NULL; + wf_status s = resolve_post_cid(agent, at_uri, &cid); + if (s != WF_OK || !cid) { + fprintf(stderr, "error: could not resolve CID for %s (status %d)\n", + at_uri, (int)s); + free(cid); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result out = {0}; + s = wf_agent_like(agent, at_uri, cid, &out); + free(cid); + if (s != WF_OK) { + fprintf(stderr, "error: like failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_unlike(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *like_uri = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_status s = wf_agent_unlike(agent, like_uri); + if (s != WF_OK) { + fprintf(stderr, "error: unlike failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("unliked %s\n", like_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_repost(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + char *cid = NULL; + wf_status s = resolve_post_cid(agent, at_uri, &cid); + if (s != WF_OK || !cid) { + fprintf(stderr, "error: could not resolve CID for %s (status %d)\n", + at_uri, (int)s); + free(cid); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result out = {0}; + s = wf_agent_repost(agent, at_uri, cid, &out); + free(cid); + if (s != WF_OK) { + fprintf(stderr, "error: repost failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_delete_repost(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *repost_uri = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_status s = wf_agent_delete_repost(agent, repost_uri); + if (s != WF_OK) { + fprintf(stderr, "error: delete-repost failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("deleted repost %s\n", repost_uri); + wf_agent_free(agent); + return 0; +} + +int cmd_mute(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + char *did = NULL; + wf_status s = resolve_actor_to_did(agent, actor, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + s = wf_agent_mute(agent, did); + free(did); + if (s != WF_OK) { + fprintf(stderr, "error: mute failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("muted %s\n", actor); + wf_agent_free(agent); + return 0; +} + +int cmd_unmute(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + char *did = NULL; + wf_status s = resolve_actor_to_did(agent, actor, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + s = wf_agent_unmute(agent, did); + free(did); + if (s != WF_OK) { + fprintf(stderr, "error: unmute failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("unmuted %s\n", actor); + wf_agent_free(agent); + return 0; +} + +int cmd_block(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + char *did = NULL; + wf_status s = resolve_actor_to_did(agent, actor, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + wf_agent_post_result out = {0}; + s = wf_agent_block(agent, did, &out); + free(did); + if (s != WF_OK) { + fprintf(stderr, "error: block failed (status %d)\n", (int)s); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 1; + } + printf("%s\n", out.uri ? out.uri : "(no uri returned)"); + wf_agent_post_result_free(&out); + wf_agent_free(agent); + return 0; +} + +int cmd_unblock(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *actor = argv[4]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) return 1; + + char *did = NULL; + wf_status s = resolve_actor_to_did(agent, actor, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", + actor, (int)s); + wf_agent_free(agent); + return 1; + } + + wf_response res = {0}; + s = wf_agent_get_blocks(agent, 100, NULL, &res); + if (s != WF_OK && s != WF_ERR_HTTP) { + fprintf(stderr, "error: getBlocks failed (status %d)\n", (int)s); + wf_response_free(&res); + free(did); + wf_agent_free(agent); + return 1; + } + + char *block_uri = NULL; + if (res.body) { + cJSON *root = cJSON_ParseWithLength(res.body, res.body_len); + if (root) { + cJSON *blocks = cJSON_GetObjectItemCaseSensitive(root, "blocks"); + if (cJSON_IsArray(blocks)) { + for (int i = 0; i < cJSON_GetArraySize(blocks); ++i) { + cJSON *b = cJSON_GetArrayItem(blocks, i); + cJSON *subj = + cJSON_GetObjectItemCaseSensitive(b, "subject"); + cJSON *sdid = + subj ? cJSON_GetObjectItemCaseSensitive(subj, "did") + : NULL; + cJSON *uri = cJSON_GetObjectItemCaseSensitive(b, "uri"); + if (cJSON_IsString(sdid) && cJSON_IsString(uri) && + strcmp(sdid->valuestring, did) == 0) { + block_uri = strdup(uri->valuestring); + break; + } + } + } + cJSON_Delete(root); + } + } + wf_response_free(&res); + free(did); + + if (!block_uri) { + printf("(not currently blocking %s)\n", actor); + wf_agent_free(agent); + return 0; + } + + s = wf_agent_unblock(agent, block_uri); + free(block_uri); + if (s != WF_OK) { + fprintf(stderr, "error: unblock failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("unblocked %s\n", actor); + wf_agent_free(agent); + return 0; +} + +int cmd_resolve(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle_or_did = argv[2]; + + if (wf_syntax_did_is_valid(handle_or_did)) { + printf("%s\n", handle_or_did); + return 0; + } + + wf_xrpc_client *client = wf_xrpc_client_new(service); + if (!client) { + fprintf(stderr, "error: failed to create XRPC client\n"); + return 1; + } + + char *did = NULL; + wf_status s = wf_handle_resolve(client, handle_or_did, &did); + if (s != WF_OK || !did) { + fprintf(stderr, "error: could not resolve '%s' (status %d)\n", + handle_or_did, (int)s); + wf_xrpc_client_free(client); + return 1; + } + + printf("%s\n", did); + free(did); + wf_xrpc_client_free(client); + return 0; +} diff --git a/src/cli/cli_social.h b/src/cli/cli_social.h new file mode 100644 index 0000000..97ae99d --- /dev/null +++ b/src/cli/cli_social.h @@ -0,0 +1,45 @@ +#ifndef WOLFRAM_CLI_SOCIAL_H +#define WOLFRAM_CLI_SOCIAL_H + +/* `profile` / `follow` / `unfollow` / `like` / `unlike` / `repost` / + * `delete-repost` / `mute` / `unmute` / `block` / `unblock` / `resolve` + * subcommand handlers, dispatched from main.c's argv switch. Not part of + * any installed API -- this is the CLI demo program, not the SDK. */ + +/* wolfram profile */ +int cmd_profile(int argc, char **argv); + +/* wolfram follow */ +int cmd_follow(int argc, char **argv); + +/* wolfram unfollow */ +int cmd_unfollow(int argc, char **argv); + +/* wolfram like */ +int cmd_like(int argc, char **argv); + +/* wolfram unlike */ +int cmd_unlike(int argc, char **argv); + +/* wolfram repost */ +int cmd_repost(int argc, char **argv); + +/* wolfram delete-repost */ +int cmd_delete_repost(int argc, char **argv); + +/* wolfram mute */ +int cmd_mute(int argc, char **argv); + +/* wolfram unmute */ +int cmd_unmute(int argc, char **argv); + +/* wolfram block */ +int cmd_block(int argc, char **argv); + +/* wolfram unblock */ +int cmd_unblock(int argc, char **argv); + +/* wolfram resolve */ +int cmd_resolve(int argc, char **argv); + +#endif /* WOLFRAM_CLI_SOCIAL_H */ diff --git a/src/cli/cli_stream.c b/src/cli/cli_stream.c new file mode 100644 index 0000000..840e33c --- /dev/null +++ b/src/cli/cli_stream.c @@ -0,0 +1,212 @@ +#include "cli_stream.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/sync_subscribe.h" +#include "wolfram/jetstream.h" + +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t g_stream_stop = 0; +static wf_subscribe_handle **g_stream_handle_ptr = NULL; + +static void stream_on_sigint(int sig) { + (void)sig; + g_stream_stop = 1; + if (g_stream_handle_ptr && *g_stream_handle_ptr) { + wf_subscribe_stop(*g_stream_handle_ptr); + } +} + +static void stream_on_event(const wf_subscribe_event *event, + void *userdata) { + (void)userdata; + switch (event->type) { + case WF_SUBSCRIBE_EVENT_COMMIT: { + const wf_subscribe_commit *c = &event->data.commit; + printf("commit: did=%s seq=%" PRId64 " rev=%s ops=%zu\n", + c->did, event->seq, c->rev, c->ops_count); + for (size_t i = 0; i < c->ops_count; ++i) { + const wf_subscribe_repo_op *op = &c->ops[i]; + printf(" op: action=%s path=%s\n", + op->action, op->path ? op->path : "?"); + } + break; + } + case WF_SUBSCRIBE_EVENT_SYNC: { + const wf_subscribe_sync *s = &event->data.sync; + printf("sync: seq=%" PRId64 " did=%s\n", event->seq, s->did); + break; + } + case WF_SUBSCRIBE_EVENT_IDENTITY: { + const wf_subscribe_identity *id = &event->data.identity; + printf("identity: seq=%" PRId64 " did=%s handle=%s\n", + event->seq, id->did, + id->has_handle ? id->handle : "?"); + break; + } + case WF_SUBSCRIBE_EVENT_ACCOUNT: { + const wf_subscribe_account *acc = &event->data.account; + printf("account: seq=%" PRId64 " did=%s active=%d status=%s\n", + event->seq, acc->did, acc->active, + acc->has_status ? acc->status : "?"); + break; + } + case WF_SUBSCRIBE_EVENT_INFO: { + const wf_subscribe_info *info = &event->data.info; + printf("info: name=%s message=%s\n", info->name, + info->has_message ? info->message : "(none)"); + break; + } + case WF_SUBSCRIBE_EVENT_ERROR: { + printf("error: %s\n", + event->data.error.message ? event->data.error.message : ""); + break; + } + default: + break; + } + fflush(stdout); +} + +static void stream_on_error(wf_status status, const char *msg, + void *userdata) { + (void)userdata; + fprintf(stderr, "error: stream status=%d %s\n", (int)status, + msg ? msg : ""); +} + +int cmd_sync_subscribe(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + + int64_t cursor = 0; + int has_cursor = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) { + cursor = (int64_t)strtoll(argv[++i], NULL, 10); + has_cursor = 1; + } + } + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = stream_on_sigint; + sigaction(SIGINT, &sa, NULL); + + g_stream_stop = 0; + + wf_subscribe_options opts; + memset(&opts, 0, sizeof(opts)); + opts.service = service; + opts.cursor = cursor; + opts.has_cursor = has_cursor; + opts.on_event = stream_on_event; + opts.on_error = stream_on_error; + + printf("subscribing to repo stream at %s (cursor=%s)\n", service, + has_cursor ? "set" : "none"); + + wf_subscribe_handle *handle = NULL; + g_stream_handle_ptr = &handle; + wf_status s = wf_subscribe_start(&opts, &handle); + g_stream_handle_ptr = NULL; + + if (s != WF_OK) { + fprintf(stderr, "error: subscription ended (status %d)\n", (int)s); + return 1; + } + printf("subscription ended cleanly\n"); + return 0; +} + +int cmd_firehose(int argc, char **argv) { + return cmd_sync_subscribe(argc, argv); +} + +int cmd_jetstream(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *endpoint = argv[1]; + + const char *collections[16]; + int n_collections = 0; + const char *dids[16]; + int n_dids = 0; + int64_t cursor = 0; + + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--collections") == 0) { + while (i + 1 < argc && argv[i + 1][0] != '-' && + n_collections < 16) + collections[n_collections++] = argv[++i]; + } else if (strcmp(argv[i], "--dids") == 0) { + while (i + 1 < argc && argv[i + 1][0] != '-' && n_dids < 16) + dids[n_dids++] = argv[++i]; + } else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) { + cursor = (int64_t)strtoll(argv[++i], NULL, 10); + } + } + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = stream_on_sigint; + sigaction(SIGINT, &sa, NULL); + + g_stream_stop = 0; + + wf_jetstream_options opts; + memset(&opts, 0, sizeof(opts)); + opts.endpoint = endpoint; + opts.wanted_collections = collections; + opts.wanted_collections_count = n_collections; + opts.wanted_dids = dids; + opts.wanted_dids_count = n_dids; + opts.cursor = cursor; + + printf("connecting to jetstream at %s (collections=%d, dids=%d, " + "cursor=%" PRId64 ")\n", + endpoint, n_collections, n_dids, cursor); + + wf_jetstream *stream = NULL; + wf_status s = wf_jetstream_connect(&opts, &stream); + if (s != WF_OK) { + fprintf(stderr, "error: jetstream connect failed (status %d)\n", + (int)s); + return 1; + } + + int count = 0; + while (!g_stream_stop) { + wf_jetstream_event event = {0}; + s = wf_jetstream_next(stream, &event); + if (s != WF_OK) { + if (g_stream_stop) break; + fprintf(stderr, "error: jetstream next failed (status %d)\n", + (int)s); + break; + } + count++; + printf("event: kind=%d did=%s\n", + (int)event.kind, event.did ? event.did : "?"); + if (event.json) + printf(" %s\n", event.json); + wf_jetstream_event_free(&event); + fflush(stdout); + } + + wf_jetstream_free(stream); + printf("jetstream ended (%d events)\n", count); + return 0; +} diff --git a/src/cli/cli_stream.h b/src/cli/cli_stream.h new file mode 100644 index 0000000..f14a7ad --- /dev/null +++ b/src/cli/cli_stream.h @@ -0,0 +1,8 @@ +#ifndef WOLFRAM_CLI_STREAM_H +#define WOLFRAM_CLI_STREAM_H + +int cmd_sync_subscribe(int argc, char **argv); +int cmd_firehose(int argc, char **argv); +int cmd_jetstream(int argc, char **argv); + +#endif /* WOLFRAM_CLI_STREAM_H */ diff --git a/src/cli/cli_sync.c b/src/cli/cli_sync.c new file mode 100644 index 0000000..df0b6f9 --- /dev/null +++ b/src/cli/cli_sync.c @@ -0,0 +1,179 @@ +#include "cli_sync.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/sync_typed.h" +#include "wolfram/repo_typed.h" + +#include +#include +#include +#include +#include + +int cmd_sync_get_blob(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *did = argv[2]; + const char *cid = argv[3]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_sync_get_blob(agent, did, cid, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_sync_get_blocks(int argc, char **argv) { + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *did = argv[2]; + const char *const *cids = (const char *const *)(argv + 3); + int n_cids = argc - 3; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_sync_get_blocks(agent, did, cids, n_cids, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_sync_get_record(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *did = argv[2]; + const char *collection = argv[3]; + const char *rkey = argv[4]; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_sync_get_record(agent, did, collection, rkey, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_sync_list_blobs(int argc, char **argv) { + const char *since = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--since") == 0 && i + 1 < argc) + since = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 2) { + usage_stream(stderr); + return 0; + } + int limit = (pi >= 3) ? atoi(pos[2]) : 500; + + wf_agent *agent = wf_agent_new(pos[0]); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_response res = {0}; + wf_status s = + wf_agent_sync_list_blobs(agent, pos[1], limit, NULL, since, &res); + return finish_agent_response(agent, s, &res); +} + +int cmd_import_repo(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *car_path = argv[4]; + + char *car_data = read_text_file(car_path); + if (!car_data) { + fprintf(stderr, "error: could not read CAR file '%s'\n", car_path); + return 1; + } + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + free(car_data); + return 1; + } + + size_t car_len = strlen(car_data); + wf_status s = wf_agent_import_repo_typed(agent, car_data, car_len); + free(car_data); + if (s != WF_OK) { + fprintf(stderr, "error: importRepo failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("repo imported\n"); + wf_agent_free(agent); + return 0; +} + +int cmd_list_missing_blobs(int argc, char **argv) { + const char *cursor = NULL; + const char *pos[3]; + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) + cursor = argv[++i]; + else if (pi < 3) + pos[pi++] = argv[i]; + } + if (pi < 3) { + usage_stream(stderr); + return 0; + } + int limit = 500; + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_repo_missing_blob_list out = {0}; + wf_status s = wf_agent_list_missing_blobs_typed(agent, limit, cursor, &out); + if (s != WF_OK) { + fprintf(stderr, "error: listMissingBlobs failed (status %d)\n", + (int)s); + wf_repo_missing_blob_list_free(&out); + wf_agent_free(agent); + return 1; + } + printf("missing blobs:\n"); + for (size_t i = 0; i < out.count; ++i) { + printf(" cid=%s uri=%s\n", + out.items[i].cid ? out.items[i].cid : "?", + out.items[i].record_uri ? out.items[i].record_uri : "?"); + } + if (out.cursor) + printf("cursor: %s\n", out.cursor); + wf_repo_missing_blob_list_free(&out); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_sync.h b/src/cli/cli_sync.h new file mode 100644 index 0000000..6fefb8c --- /dev/null +++ b/src/cli/cli_sync.h @@ -0,0 +1,13 @@ +#ifndef WOLFRAM_CLI_SYNC_H +#define WOLFRAM_CLI_SYNC_H + +#include + +int cmd_sync_get_blob(int argc, char **argv); +int cmd_sync_get_blocks(int argc, char **argv); +int cmd_sync_get_record(int argc, char **argv); +int cmd_sync_list_blobs(int argc, char **argv); +int cmd_import_repo(int argc, char **argv); +int cmd_list_missing_blobs(int argc, char **argv); + +#endif /* WOLFRAM_CLI_SYNC_H */ diff --git a/src/cli/cli_thread.c b/src/cli/cli_thread.c new file mode 100644 index 0000000..b9e252e --- /dev/null +++ b/src/cli/cli_thread.c @@ -0,0 +1,213 @@ +#include "cli_thread.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/thread_typed.h" +#include "wolfram/notification_typed.h" +#include "wolfram/moderation.h" + +#include +#include +#include +#include +#include + +/* Recursively print a thread node and its replies (indented by depth). */ +static void print_thread_node(const wf_agent_thread_node *node, int depth) { + if (!node) { + return; + } + for (int i = 0; i < depth; ++i) { + fputc(' ', stdout); + fputc(' ', stdout); + } + + if (node->kind == WF_AGENT_THREAD_KIND_POST) { + const char *handle = + node->post.author.handle + ? node->post.author.handle + : (node->post.author.did ? node->post.author.did : "?"); + const char *did = node->post.author.did ? node->post.author.did : "?"; + const char *text = ""; + if (node->post.record) { + cJSON *t = + cJSON_GetObjectItemCaseSensitive(node->post.record, "text"); + if (cJSON_IsString(t) && t->valuestring) { + text = t->valuestring; + } + } + printf("[%s] %s: %s\n", did, handle, text); + } else { + printf("(not found/blocked: %s)\n", node->uri ? node->uri : "?"); + } + + for (size_t i = 0; i < node->replies_count; ++i) { + print_thread_node(&node->replies[i], depth + 1); + } +} + +int cmd_thread(int argc, char **argv) { + if (argc < 5) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + const char *at_uri = argv[4]; + int depth = (argc >= 6) ? atoi(argv[5]) : 6; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + if (g_json) { + wf_response res = {0}; + s = wf_agent_get_post_thread(agent, at_uri, depth, 0, &res); + if (s != WF_OK) + fprintf(stderr, "error: request failed (status %d)\n", (int)s); + else if (res.body && res.body_len > 0) + printf("%s\n", res.body); + else + printf("(empty response, HTTP %ld)\n", res.status); + wf_response_free(&res); + wf_agent_free(agent); + return 0; + } + + wf_agent_thread thread = {0}; + s = wf_agent_get_post_thread_typed(agent, at_uri, depth, &thread); + if (s != WF_OK) { + fprintf(stderr, "error: getPostThread failed (status %d)\n", (int)s); + wf_agent_thread_free(&thread); + wf_agent_free(agent); + return 1; + } + + print_thread_node(&thread.root, 0); + wf_agent_thread_free(&thread); + wf_agent_free(agent); + return 0; +} + +int cmd_notifications(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + + /* notifications update-seen [--seen-at ] + */ + if (strcmp(argv[1], "update-seen") == 0) { + const char *seen_at = NULL; + const char *pos[4]; + int pi = 0; + for (int i = 2; i < argc; ++i) { + if (strcmp(argv[i], "--seen-at") == 0 && i + 1 < argc) { + seen_at = argv[++i]; + } else if (pi < 4) { + pos[pi++] = argv[i]; + } + } + if (pi < 3) { + fprintf(stderr, + "error: usage: wolfram notifications update-seen " + "[--seen-at ] \n"); + return 1; + } + + wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); + if (!agent) return 1; + + wf_status s = wf_agent_update_seen_typed(agent, seen_at); + if (s != WF_OK) { + fprintf(stderr, "error: updateSeen failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + printf("notifications marked seen%s%s\n", seen_at ? " at " : "", + seen_at ? seen_at : ""); + wf_agent_free(agent); + return 0; + } + + /* notifications [limit] */ + if (argc < 4) { + usage_stream(stderr); + return 0; + } + const char *service = argv[1]; + const char *handle = argv[2]; + const char *password = argv[3]; + int limit = (argc >= 5) ? atoi(argv[4]) : 50; + + wf_agent *agent = wf_agent_new(service); + if (!agent) { + fprintf(stderr, "error: failed to create agent\n"); + return 1; + } + + wf_status s = wf_agent_login(agent, handle, password); + if (s != WF_OK) { + fprintf(stderr, "error: login failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + if (g_json) { + wf_response res = {0}; + s = wf_agent_list_notifications(agent, limit, NULL, &res); + if (s != WF_OK) + fprintf(stderr, "error: request failed (status %d)\n", (int)s); + else if (res.body && res.body_len > 0) + printf("%s\n", res.body); + else + printf("(empty response, HTTP %ld)\n", res.status); + wf_response_free(&res); + wf_agent_free(agent); + return 0; + } + + wf_agent_notification_list list = {0}; + s = wf_agent_list_notifications_typed(agent, limit, NULL, &list); + if (s != WF_OK) { + fprintf(stderr, "error: listNotifications failed (status %d)\n", + (int)s); + wf_agent_notification_list_free(&list); + wf_agent_free(agent); + return 1; + } + + for (size_t i = 0; i < list.notification_count; ++i) { + const wf_agent_notification *n = &list.notifications[i]; + const char *author = n->author.handle + ? n->author.handle + : (n->author.did ? n->author.did : "?"); + const char *did = n->author.did ? n->author.did : "?"; + const char *text = ""; + if (n->record) { + cJSON *t = cJSON_GetObjectItemCaseSensitive(n->record, "text"); + if (cJSON_IsString(t) && t->valuestring) { + text = t->valuestring; + } + } + printf("reason=%s author=%s (%s) read=%d\n %s\n", + n->reason ? n->reason : "?", author, did, n->is_read, text); + } + if (list.notification_count == 0) { + printf("(no notifications)\n"); + } + + wf_agent_notification_list_free(&list); + wf_agent_free(agent); + return 0; +} diff --git a/src/cli/cli_thread.h b/src/cli/cli_thread.h new file mode 100644 index 0000000..05e9655 --- /dev/null +++ b/src/cli/cli_thread.h @@ -0,0 +1,7 @@ +#ifndef WOLFRAM_CLI_THREAD_H +#define WOLFRAM_CLI_THREAD_H + +int cmd_thread(int argc, char **argv); +int cmd_notifications(int argc, char **argv); + +#endif /* WOLFRAM_CLI_THREAD_H */ diff --git a/src/cli/cli_video.c b/src/cli/cli_video.c new file mode 100644 index 0000000..06632ff --- /dev/null +++ b/src/cli/cli_video.c @@ -0,0 +1,114 @@ +/* + * cli_video.c — the `video` subcommands (upload, status), split out of main.c + * as their own self-contained concern. + */ + +#include "cli_video.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/video_typed.h" +#include "wolfram/blob.h" + +#include +#include +#include +#include +#include + +int cmd_video(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + + const char *sub = argv[1]; + if (strcmp(sub, "upload") == 0) { + if (argc < 6) { + usage_stream(stderr); + return 0; + } + const char *service = argv[2]; + const char *handle = argv[3]; + const char *password = argv[4]; + const char *path = argv[5]; + + FILE *f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "error: cannot open '%s'\n", path); + return 1; + } + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + fprintf(stderr, "error: cannot seek '%s'\n", path); + return 1; + } + long size = ftell(f); + if (size < 0) { + fclose(f); + fprintf(stderr, "error: cannot stat '%s'\n", path); + return 1; + } + rewind(f); + void *buf = malloc((size_t)size > 0 ? (size_t)size : 1); + if (!buf) { + fclose(f); + fprintf(stderr, "error: out of memory\n"); + return 1; + } + if (fread(buf, 1, (size_t)size, f) != (size_t)size) { + fclose(f); + free(buf); + fprintf(stderr, "error: failed to read '%s'\n", path); + return 1; + } + fclose(f); + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + free(buf); + return 1; + } + + wf_uploaded_blob blob = {0}; + wf_status s = + wf_agent_upload_video(agent, buf, (size_t)size, "video/mp4", &blob); + free(buf); + if (s != WF_OK) { + fprintf(stderr, "error: video upload failed (status %d)\n", (int)s); + wf_agent_free(agent); + return 1; + } + + printf("uploaded: cid=%s mime=%s size=%zu\n", blob.cid ? blob.cid : "?", + blob.mime_type ? blob.mime_type : "?", blob.size); + wf_uploaded_blob_free(&blob); + wf_agent_free(agent); + return 0; + } + + if (strcmp(sub, "status") == 0) { + if (argc < 6) { + usage_stream(stderr); + return 0; + } + const char *service = argv[2]; + const char *handle = argv[3]; + const char *password = argv[4]; + const char *job_id = argv[5]; + + wf_agent *agent = agent_login_or_err(service, handle, password); + if (!agent) { + return 1; + } + + wf_response res = {0}; + wf_status s = wf_agent_get_video_job_status(agent, job_id, &res); + return finish_agent_response(agent, s, &res); + } + + fprintf(stderr, + "error: unknown video subcommand '%s' (try 'upload' or 'status')\n", + sub); + return 1; +} diff --git a/src/cli/cli_video.h b/src/cli/cli_video.h new file mode 100644 index 0000000..283523f --- /dev/null +++ b/src/cli/cli_video.h @@ -0,0 +1,14 @@ +/* + * cli_video.h — the `video` subcommand handler declaration. + * + * Split out of main.c so the video upload/status subcommands live in their + * own translation unit. Not part of any installed API -- this is the CLI + * demo program, not the SDK. + */ + +#ifndef WOLFRAM_CLI_VIDEO_H +#define WOLFRAM_CLI_VIDEO_H + +int cmd_video(int argc, char **argv); + +#endif /* WOLFRAM_CLI_VIDEO_H */ diff --git a/src/cli/main.c b/src/cli/main.c index 7115561..cc671cf 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -61,14 +61,52 @@ #include "wolfram/agent.h" #include "wolfram/blob.h" #include "wolfram/identity.h" +#include "wolfram/identity_typed.h" #include "wolfram/label.h" #include "wolfram/oauth.h" #include "wolfram/server.h" +#include "wolfram/server_typed.h" #include "wolfram/syntax.h" #include "wolfram/xrpc.h" +#include "wolfram/thread_typed.h" +#include "wolfram/feed_typed.h" +#include "wolfram/actor_typed.h" +#include "wolfram/moderation.h" +#include "wolfram/repo_typed.h" +#include "wolfram/feedgen_typed.h" +#include "wolfram/notification_typed.h" +#include "wolfram/moderation_report_typed.h" +#include "wolfram/temp_typed.h" +#include "wolfram/ozone_typed.h" +#include "wolfram/sync_typed.h" +#include "wolfram/jetstream.h" +#include "wolfram/unspecced_typed.h" +#include "wolfram/sync_subscribe.h" +#include "wolfram/graph_typed.h" +#include "wolfram/graph_social_typed.h" +#include "wolfram/graph_write.h" +#include "wolfram/chat_typed.h" +#include "wolfram/actor_prefs_typed.h" +#include "wolfram/admin_typed.h" #include "main_internal.h" #include "cli_oauth.h" +#include "cli_auth.h" +#include "cli_post.h" +#include "cli_thread.h" +#include "cli_social.h" +#include "cli_search.h" +#include "cli_feed.h" +#include "cli_graph.h" +#include "cli_server.h" +#include "cli_identity.h" +#include "cli_sync.h" +#include "cli_prefs.h" +#include "cli_chat.h" +#include "cli_stream.h" +#include "cli_repo.h" +#include "cli_video.h" +#include "cli_ozone.h" #include "wolfram/thread_typed.h" #include "wolfram/feed_typed.h" #include "wolfram/actor_typed.h" @@ -81,7 +119,7 @@ /* Global --json flag: when set, list/get commands print the raw JSON body * instead of human-readable text. Parsed in main() before dispatch. */ -static bool g_json = false; +bool g_json = false; /* ----------------------------------------------------------------- */ /* Usage */ @@ -348,7 +386,7 @@ int usage_exit(void) { /* ----------------------------------------------------------------- */ /* Join argv[first..argc-1] into a single heap string (caller frees). */ -static char *join_args(int argc, char **argv, int first) { +char *join_args(int argc, char **argv, int first) { size_t total = 1; for (int i = first; i < argc; ++i) { size_t part = strlen(argv[i]); @@ -403,8 +441,8 @@ wf_status resolve_actor_to_did(wf_agent *agent, const char *actor, /* Create an agent, login, and return it. Returns NULL on failure (error * already printed to stderr). Caller frees with wf_agent_free. */ -static wf_agent *agent_login_or_err(const char *service, const char *handle, - const char *password) { +wf_agent *agent_login_or_err(const char *service, const char *handle, + const char *password) { wf_agent *agent = wf_agent_new(service); if (!agent) { fprintf(stderr, "error: failed to create agent\n"); @@ -421,7 +459,7 @@ static wf_agent *agent_login_or_err(const char *service, const char *handle, /* Print an agent-level failure to stderr, appending the server's XRPC error * message when one was captured. Returns the failure exit code. */ -static int cli_agent_error(const char *what, wf_status s, wf_agent *agent) { +int cli_agent_error(const char *what, wf_status s, wf_agent *agent) { const char *msg = wf_agent_last_error(agent); if (msg && *msg) { fprintf(stderr, "error: %s failed (status %d): %s\n", what, (int)s, @@ -556,7 +594,7 @@ wf_status resolve_post_for_reply(wf_agent *agent, const char *at_uri, /* Format the current UTC time as an RFC 3339 timestamp (e.g. for record * createdAt fields). Writes at most `len` bytes into `buf`. */ -static void now_rfc3339(char *buf, size_t len) { +void now_rfc3339(char *buf, size_t len) { time_t now = time(NULL); struct tm tm_utc; #ifdef _WIN32 @@ -569,2275 +607,420 @@ static void now_rfc3339(char *buf, size_t len) { } } -/* ----------------------------------------------------------------- */ -/* Subcommands */ -/* ----------------------------------------------------------------- */ - -static int cmd_login(int argc, char **argv) { - if (argc < 4) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; - } - - wf_status s = wf_agent_login(agent, handle, password); +/* Print a raw JSON agent response, freeing resources, returning 0 on success. + * On error prints to stderr and returns 1. */ +int finish_agent_response(wf_agent *agent, wf_status s, wf_response *res) { + int rc = 0; if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + fprintf(stderr, "error: request failed (status %d)\n", (int)s); + rc = 1; + } else if (res->body && res->body_len > 0) { + printf("%s\n", res->body); + } else { + printf("(empty response, HTTP %ld)\n", res->status); } - - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - printf("logged in as %s (%s)\n", sd.handle ? sd.handle : "?", - sd.did ? sd.did : "?"); - wf_agent_session_data_free(&sd); + wf_response_free(res); wf_agent_free(agent); - return 0; + return rc; } -/* wolfram whoami */ -static int cmd_whoami(int argc, char **argv) { - if (argc < 4) { - usage_stream(stderr); - return 0; +/* Read the entire text file `path` into a heap string (NUL terminated). + * Returns NULL on failure. Caller frees. */ +char *read_text_file(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return NULL; } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) return 1; - - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - printf("did: %s\n", sd.did ? sd.did : "?"); - printf("handle: %s\n", sd.handle ? sd.handle : "?"); - wf_agent_session_data_free(&sd); - wf_agent_free(agent); - return 0; -} - -/* wolfram describe-server */ -static int cmd_describe_server(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; + long len = ftell(f); + if (len < 0) { + fclose(f); + return NULL; } - const char *service = argv[1]; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + rewind(f); + char *data = malloc((size_t)len + 1); + if (!data) { + fclose(f); + return NULL; } - - wf_agent_server_description desc = {0}; - wf_status s = wf_agent_describe_server(agent, &desc); - if (s != WF_OK) { - fprintf(stderr, "error: describeServer failed (status %d)\n", (int)s); - wf_agent_server_description_free(&desc); - wf_agent_free(agent); - return 1; - } - printf("did: %s\n", desc.did ? desc.did : "?"); - printf("inviteCodeRequired: %d\n", desc.invite_code_required); - printf("phoneVerificationRequired: %d\n", desc.phone_verification_required); - printf("availableUserDomains (%zu):\n", desc.available_user_domain_count); - for (size_t i = 0; i < desc.available_user_domain_count; ++i) { - printf(" %s\n", desc.available_user_domains[i] - ? desc.available_user_domains[i] - : "?"); - } - printf("privacyPolicy: %s\n", - desc.privacy_policy ? desc.privacy_policy : "?"); - printf("termsOfService: %s\n", - desc.terms_of_service ? desc.terms_of_service : "?"); - printf("contactEmail: %s\n", desc.contact_email ? desc.contact_email : "?"); - wf_agent_server_description_free(&desc); - wf_agent_free(agent); - return 0; + size_t n = fread(data, 1, (size_t)len, f); + fclose(f); + data[n] = '\0'; + return data; } -/* wolfram delete */ -static int cmd_delete(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *at_uri = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) return 1; +/* ----------------------------------------------------------------- */ +/* Subcommands */ +/* ----------------------------------------------------------------- */ - wf_status s = wf_agent_delete_post(agent, at_uri); - if (s != WF_OK) { - cli_agent_error("delete", s, agent); - wf_agent_free(agent); - return 1; +int main(int argc, char **argv) { + if (argc < 2) { + return usage_exit(); } - printf("deleted %s\n", at_uri); - wf_agent_free(agent); - return 0; -} -static int cmd_post(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + /* Strip a leading global --json flag (and any others) so the command and + * its positional arguments remain contiguous at argv[1..]. */ + { + int out = 1; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--json") == 0) { + g_json = true; + continue; + } + argv[out++] = argv[i]; + } + argc = out; } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - char *text = join_args(argc, argv, 4); - if (!text) { - fprintf(stderr, "error: failed to assemble post text\n"); - return 1; - } + const char *cmd = argv[1]; - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - free(text); - return 1; + /* --help / -h / help print usage to stdout and exit 0 — without touching + * the network. Genuinely unknown commands still print to stderr below. */ + if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { + return usage_exit(); } - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - free(text); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "--version") == 0 || strcmp(cmd, "-v") == 0) { + printf("%s\n", WOLFRAM_VERSION_STRING); + return 0; } - wf_agent_post_result result = {0}; - s = wf_agent_post(agent, text, &result); - free(text); - if (s != WF_OK) { - cli_agent_error("post", s, agent); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "help") == 0) { + if (argc >= 3) { + cmd_help_stream(stdout, argv[2]); + } else { + usage_stream(stdout); + } + return 0; } - printf("%s\n", result.uri ? result.uri : "(no uri returned)"); - wf_agent_post_result_free(&result); - wf_agent_free(agent); - return 0; -} - -typedef struct { - int printed; -} timeline_ctx; + int rest = argc - 1; /* arguments after the program name */ -static wf_status timeline_on_page(wf_agent *agent, - const wf_agent_feed_list *feed, - const char *cursor, void *ud) { - (void)agent; - (void)cursor; - timeline_ctx *ctx = (timeline_ctx *)ud; + /* Re-base argv so handlers see as argv[1] etc. */ + char **cargv = argv + 1; - for (size_t i = 0; i < feed->item_count; ++i) { - const wf_agent_post_view *post = &feed->items[i].post; - const char *author = post->author.handle - ? post->author.handle - : (post->author.did ? post->author.did : "?"); - const char *text = ""; - if (post->record) { - cJSON *t = cJSON_GetObjectItemCaseSensitive(post->record, "text"); - if (cJSON_IsString(t) && t->valuestring) { - text = t->valuestring; - } - } - printf("%s: %s\n", author, text); - ctx->printed++; + if (strcmp(cmd, "login") == 0) { + return cmd_login(rest, cargv); } - return WF_OK; -} - -static int cmd_timeline(int argc, char **argv) { - if (argc < 4) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "whoami") == 0) { + return cmd_whoami(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - int max_pages = (argc >= 5) ? atoi(argv[4]) : 0; /* 0 = until exhausted */ - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "describe-server") == 0) { + return cmd_describe_server(rest, cargv); } - - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "post") == 0) { + return cmd_post(rest, cargv); } - - if (g_json) { - wf_response res = {0}; - s = wf_agent_get_timeline(agent, 50, NULL, NULL, &res); - if (s != WF_OK) - fprintf(stderr, "error: request failed (status %d)\n", (int)s); - else if (res.body && res.body_len > 0) - printf("%s\n", res.body); - else - printf("(empty response, HTTP %ld)\n", res.status); - wf_response_free(&res); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "reply") == 0) { + return cmd_reply(rest, cargv); } - - timeline_ctx ctx = {0}; - char *last_cursor = NULL; - s = wf_agent_get_timeline_paged(agent, 10, max_pages, timeline_on_page, - &ctx, &last_cursor); - if (s != WF_OK) { - fprintf(stderr, "error: timeline failed (status %d)\n", (int)s); - free(last_cursor); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "delete") == 0) { + return cmd_delete(rest, cargv); } - - if (ctx.printed == 0) { - printf("(timeline empty)\n"); + if (strcmp(cmd, "like") == 0) { + return cmd_like(rest, cargv); } - free(last_cursor); - wf_agent_free(agent); - return 0; -} - -static int cmd_get_post(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "unlike") == 0) { + return cmd_unlike(rest, cargv); } - const char *service = argv[1]; - const char *uri = argv[2]; - - wf_syntax_aturi parsed = {0}; - if (!wf_syntax_aturi_parse(uri, &parsed) || !parsed.authority || - !parsed.collection || !parsed.record_key) { - fprintf(stderr, "error: invalid at-uri: %s\n", uri); - wf_syntax_aturi_free(&parsed); - return 1; + if (strcmp(cmd, "repost") == 0) { + return cmd_repost(rest, cargv); } - - wf_xrpc_client *client = wf_xrpc_client_new(service); - if (!client) { - fprintf(stderr, "error: failed to create XRPC client\n"); - wf_syntax_aturi_free(&parsed); - return 1; + if (strcmp(cmd, "delete-repost") == 0 || strcmp(cmd, "unrepost") == 0) { + return cmd_delete_repost(rest, cargv); } - - wf_xrpc_param params[] = { - {"repo", parsed.authority}, - {"collection", parsed.collection}, - {"rkey", parsed.record_key}, - }; - - wf_response res = {0}; - wf_status s = wf_xrpc_query_params(client, "com.atproto.repo.getRecord", - params, 3, &res); - wf_syntax_aturi_free(&parsed); - - if (s != WF_OK && s != WF_ERR_HTTP) { - fprintf(stderr, "error: getRecord failed (status %d)\n", (int)s); - wf_response_free(&res); - wf_xrpc_client_free(client); - return 1; + if (strcmp(cmd, "labels") == 0) { + return cmd_labels(rest, cargv); } - - if (res.body && res.body_len > 0) { - printf("%s\n", res.body); - } else { - printf("(empty response, HTTP %ld)\n", res.status); + if (strcmp(cmd, "oauth-login") == 0) { + return cmd_oauth_login(rest, cargv); } - - wf_response_free(&res); - wf_xrpc_client_free(client); - return 0; -} - -static int cmd_profile(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "oauth-callback") == 0) { + return cmd_oauth_callback(rest, cargv); } - const char *service = argv[1]; - const char *actor = argv[2]; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "timeline") == 0) { + return cmd_timeline(rest, cargv); } - - wf_agent_profile prof = {0}; - wf_status s = wf_agent_get_profile(agent, actor, &prof); - if (s != WF_OK) { - fprintf(stderr, "error: getProfile failed (status %d)\n", (int)s); - wf_agent_profile_free(&prof); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-post") == 0) { + return cmd_get_post(rest, cargv); } - - if (g_json) { - cJSON *j = cJSON_CreateObject(); - if (j) { - if (prof.did) cJSON_AddStringToObject(j, "did", prof.did); - if (prof.handle) cJSON_AddStringToObject(j, "handle", prof.handle); - if (prof.display_name) - cJSON_AddStringToObject(j, "displayName", prof.display_name); - if (prof.description) - cJSON_AddStringToObject(j, "description", prof.description); - if (prof.avatar_cid) - cJSON_AddStringToObject(j, "avatar", prof.avatar_cid); - cJSON_AddNumberToObject(j, "followersCount", prof.followers_count); - cJSON_AddNumberToObject(j, "followsCount", prof.follows_count); - cJSON_AddNumberToObject(j, "postsCount", prof.posts_count); - char *out = cJSON_PrintUnformatted(j); - cJSON_Delete(j); - if (out) { - printf("%s\n", out); - free(out); - } - } - wf_agent_profile_free(&prof); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "profile") == 0) { + return cmd_profile(rest, cargv); } - - printf("%s\n", prof.display_name ? prof.display_name : "(no display name)"); - if (prof.description) { - printf("%s\n", prof.description); + if (strcmp(cmd, "follow") == 0) { + return cmd_follow(rest, cargv); } - wf_agent_profile_free(&prof); - wf_agent_free(agent); - return 0; -} - -static int cmd_follow(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "unfollow") == 0) { + return cmd_unfollow(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "resolve") == 0) { + return cmd_resolve(rest, cargv); } - - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "thread") == 0) { + return cmd_thread(rest, cargv); } - - char *subject_did = NULL; - s = resolve_actor_to_did(agent, actor, &subject_did); - if (s != WF_OK || !subject_did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "notifications") == 0) { + return cmd_notifications(rest, cargv); } - - wf_agent_post_result result = {0}; - s = wf_agent_follow(agent, subject_did, &result); - free(subject_did); - if (s != WF_OK) { - fprintf(stderr, "error: follow failed (status %d)\n", (int)s); - wf_agent_post_result_free(&result); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "search") == 0) { + return cmd_search(rest, cargv); } - - printf("followed: %s\n", result.uri ? result.uri : "(no uri returned)"); - wf_agent_post_result_free(&result); - wf_agent_free(agent); - return 0; -} - -static int cmd_unfollow(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "moderation") == 0) { + return cmd_moderation(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "ozone") == 0) { + return cmd_ozone(rest, cargv); } - - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "follows") == 0) { + return cmd_follows(rest, cargv); } - - char *subject_did = NULL; - s = resolve_actor_to_did(agent, actor, &subject_did); - if (s != WF_OK || !subject_did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "followers") == 0) { + return cmd_followers(rest, cargv); } - - /* Look up the existing follow URI via the viewer's relationship state. */ - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - const char *others[1] = {subject_did}; - wf_response res = {0}; - s = wf_agent_get_relationships(agent, sd.did, others, 1, &res); - wf_agent_session_data_free(&sd); - free(subject_did); - - if (s != WF_OK && s != WF_ERR_HTTP) { - fprintf(stderr, "error: getRelationships failed (status %d)\n", (int)s); - wf_response_free(&res); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "blocks") == 0) { + return cmd_blocks(rest, cargv); } - - char *follow_uri = NULL; - if (res.body) { - cJSON *root = cJSON_ParseWithLength(res.body, res.body_len); - if (root) { - cJSON *rels = - cJSON_GetObjectItemCaseSensitive(root, "relationships"); - if (cJSON_IsArray(rels) && cJSON_GetArraySize(rels) > 0) { - cJSON *rel = cJSON_GetArrayItem(rels, 0); - cJSON *following = - cJSON_GetObjectItemCaseSensitive(rel, "following"); - if (cJSON_IsString(following) && following->valuestring) { - follow_uri = strdup(following->valuestring); - } - } - cJSON_Delete(root); - } + if (strcmp(cmd, "mutes") == 0) { + return cmd_mutes(rest, cargv); } - wf_response_free(&res); - - if (!follow_uri) { - printf("(not currently following %s)\n", actor); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "revoke-account-credentials") == 0) { + return cmd_revoke_account_credentials(rest, cargv); } - - s = wf_agent_unfollow(agent, follow_uri); - free(follow_uri); - if (s != WF_OK) { - fprintf(stderr, "error: unfollow failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "mute") == 0) { + return cmd_mute(rest, cargv); } - - printf("unfollowed %s\n", actor); - wf_agent_free(agent); - return 0; -} - -static int cmd_resolve(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "unmute") == 0) { + return cmd_unmute(rest, cargv); } - const char *service = argv[1]; - const char *handle_or_did = argv[2]; - - if (wf_syntax_did_is_valid(handle_or_did)) { - printf("%s\n", handle_or_did); - return 0; + if (strcmp(cmd, "list") == 0) { + return cmd_list(rest, cargv); } - - wf_xrpc_client *client = wf_xrpc_client_new(service); - if (!client) { - fprintf(stderr, "error: failed to create XRPC client\n"); - return 1; + if (strcmp(cmd, "lists") == 0) { + return cmd_lists(rest, cargv); } - - char *did = NULL; - wf_status s = wf_handle_resolve(client, handle_or_did, &did); - if (s != WF_OK || !did) { - fprintf(stderr, "error: could not resolve '%s' (status %d)\n", - handle_or_did, (int)s); - wf_xrpc_client_free(client); - return 1; + if (strcmp(cmd, "get-record") == 0) { + return cmd_get_record(rest, cargv); } - - printf("%s\n", did); - free(did); - wf_xrpc_client_free(client); - return 0; -} - -/* Recursively print a thread node and its replies (indented by depth). */ -static void print_thread_node(const wf_agent_thread_node *node, int depth) { - if (!node) { - return; + if (strcmp(cmd, "video") == 0) { + return cmd_video(rest, cargv); } - for (int i = 0; i < depth; ++i) { - fputc(' ', stdout); - fputc(' ', stdout); + if (strcmp(cmd, "block") == 0) { + return cmd_block(rest, cargv); } - - if (node->kind == WF_AGENT_THREAD_KIND_POST) { - const char *handle = - node->post.author.handle - ? node->post.author.handle - : (node->post.author.did ? node->post.author.did : "?"); - const char *did = node->post.author.did ? node->post.author.did : "?"; - const char *text = ""; - if (node->post.record) { - cJSON *t = - cJSON_GetObjectItemCaseSensitive(node->post.record, "text"); - if (cJSON_IsString(t) && t->valuestring) { - text = t->valuestring; - } - } - printf("[%s] %s: %s\n", did, handle, text); - } else { - printf("(not found/blocked: %s)\n", node->uri ? node->uri : "?"); + if (strcmp(cmd, "unblock") == 0) { + return cmd_unblock(rest, cargv); } - - for (size_t i = 0; i < node->replies_count; ++i) { - print_thread_node(&node->replies[i], depth + 1); + if (strcmp(cmd, "repo") == 0) { + return cmd_repo(rest, cargv); } -} - -static int cmd_thread(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "feed") == 0) { + return cmd_feed(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *at_uri = argv[4]; - int depth = (argc >= 6) ? atoi(argv[5]) : 6; - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + /* --- Feed & discovery --- */ + if (strcmp(cmd, "get-likes") == 0) { + return cmd_get_likes(rest, cargv); } - - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-reposted-by") == 0) { + return cmd_get_reposted_by(rest, cargv); } - - if (g_json) { - wf_response res = {0}; - s = wf_agent_get_post_thread(agent, at_uri, depth, 0, &res); - if (s != WF_OK) - fprintf(stderr, "error: request failed (status %d)\n", (int)s); - else if (res.body && res.body_len > 0) - printf("%s\n", res.body); - else - printf("(empty response, HTTP %ld)\n", res.status); - wf_response_free(&res); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "get-quotes") == 0) { + return cmd_get_quotes(rest, cargv); } - - wf_agent_thread thread = {0}; - s = wf_agent_get_post_thread_typed(agent, at_uri, depth, &thread); - if (s != WF_OK) { - fprintf(stderr, "error: getPostThread failed (status %d)\n", (int)s); - wf_agent_thread_free(&thread); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-actor-likes") == 0) { + return cmd_get_actor_likes(rest, cargv); } - - print_thread_node(&thread.root, 0); - wf_agent_thread_free(&thread); - wf_agent_free(agent); - return 0; -} - -static int cmd_notifications(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "get-actor-feeds") == 0) { + return cmd_get_actor_feeds(rest, cargv); } - - /* notifications update-seen [--seen-at ] - */ - if (strcmp(argv[1], "update-seen") == 0) { - const char *seen_at = NULL; - const char *pos[4]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--seen-at") == 0 && i + 1 < argc) { - seen_at = argv[++i]; - } else if (pi < 4) { - pos[pi++] = argv[i]; - } - } - if (pi < 3) { - fprintf(stderr, - "error: usage: wolfram notifications update-seen " - "[--seen-at ] \n"); - return 1; - } - - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - - wf_status s = wf_agent_update_seen_typed(agent, seen_at); - if (s != WF_OK) { - fprintf(stderr, "error: updateSeen failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - printf("notifications marked seen%s%s\n", seen_at ? " at " : "", - seen_at ? seen_at : ""); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "describe-feed") == 0) { + return cmd_describe_feed(rest, cargv); } - - /* notifications [limit] */ - if (argc < 4) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "get-suggested-feeds") == 0) { + return cmd_get_suggested_feeds(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - int limit = (argc >= 5) ? atoi(argv[4]) : 50; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "get-suggestions") == 0) { + return cmd_get_suggestions(rest, cargv); } - - wf_status s = wf_agent_login(agent, handle, password); - if (s != WF_OK) { - fprintf(stderr, "error: login failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "known-followers") == 0) { + return cmd_known_followers(rest, cargv); } - - if (g_json) { - wf_response res = {0}; - s = wf_agent_list_notifications(agent, limit, NULL, &res); - if (s != WF_OK) - fprintf(stderr, "error: request failed (status %d)\n", (int)s); - else if (res.body && res.body_len > 0) - printf("%s\n", res.body); - else - printf("(empty response, HTTP %ld)\n", res.status); - wf_response_free(&res); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "relationships") == 0) { + return cmd_relationships(rest, cargv); } - - wf_agent_notification_list list = {0}; - s = wf_agent_list_notifications_typed(agent, limit, NULL, &list); - if (s != WF_OK) { - fprintf(stderr, "error: listNotifications failed (status %d)\n", - (int)s); - wf_agent_notification_list_free(&list); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-profiles") == 0) { + return cmd_get_profiles(rest, cargv); + } + if (strcmp(cmd, "unread-count") == 0) { + return cmd_unread_count(rest, cargv); } - for (size_t i = 0; i < list.notification_count; ++i) { - const wf_agent_notification *n = &list.notifications[i]; - const char *author = n->author.handle - ? n->author.handle - : (n->author.did ? n->author.did : "?"); - const char *did = n->author.did ? n->author.did : "?"; - const char *text = ""; - if (n->record) { - cJSON *t = cJSON_GetObjectItemCaseSensitive(n->record, "text"); - if (cJSON_IsString(t) && t->valuestring) { - text = t->valuestring; - } - } - printf("reason=%s author=%s (%s) read=%d\n %s\n", - n->reason ? n->reason : "?", author, did, n->is_read, text); + /* --- Graph & list operations --- */ + if (strcmp(cmd, "mute-list") == 0) { + return cmd_mute_list(rest, cargv); } - if (list.notification_count == 0) { - printf("(no notifications)\n"); + if (strcmp(cmd, "unmute-list") == 0) { + return cmd_unmute_list(rest, cargv); } - - wf_agent_notification_list_free(&list); - wf_agent_free(agent); - return 0; -} - -static int cmd_moderation(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "block-list") == 0) { + return cmd_block_list(rest, cargv); } - - /* moderation report --subject - * --reason [--reason-type ] [--cid ] */ - if (strcmp(argv[1], "report") == 0) { - const char *subject = NULL, *reason = NULL, *reason_type = NULL, - *cid = NULL; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--subject") == 0 && i + 1 < argc) - subject = argv[++i]; - else if (strcmp(argv[i], "--reason") == 0 && i + 1 < argc) - reason = argv[++i]; - else if (strcmp(argv[i], "--reason-type") == 0 && i + 1 < argc) - reason_type = argv[++i]; - else if (strcmp(argv[i], "--cid") == 0 && i + 1 < argc) - cid = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !subject || !reason) { - fprintf(stderr, - "error: usage: wolfram moderation report " - " --subject --reason " - "[--reason-type ] [--cid ]\n"); - return 1; - } - - /* The SDK requires a reasonType NSID. If --reason-type is omitted, the - * --reason text is used as the type (best effort); when --reason-type - * is present, --reason is the optional free-text note. */ - const char *rt = reason_type ? reason_type : reason; - const char *free_reason = reason_type ? reason : NULL; - - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - - const char *subject_did = NULL; - const char *subject_uri = NULL; - const char *subject_cid = NULL; - if (strncmp(subject, "did:", 4) == 0) { - subject_did = subject; - } else { - subject_uri = subject; - subject_cid = cid; - } - - wf_moderation_report_record out = {0}; - wf_status s = - wf_agent_report_typed(agent, subject_did, subject_uri, subject_cid, - rt, free_reason, NULL, NULL, &out); - if (s != WF_OK) { - fprintf(stderr, "error: createReport failed (status %d)\n", (int)s); - wf_moderation_report_record_free(&out); - wf_agent_free(agent); - return 1; - } - printf("report id=%" PRId64 "\n", out.id); - if (out.reason_type) printf("reasonType: %s\n", out.reason_type); - if (out.reported_by) printf("reportedBy: %s\n", out.reported_by); - wf_moderation_report_record_free(&out); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "unblock-list") == 0) { + return cmd_unblock_list(rest, cargv); } - - if (argc < 3) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "mute-thread") == 0) { + return cmd_mute_thread(rest, cargv); } - const char *service = argv[1]; - const char *actor = argv[2]; - /* argv[3] (labeler-did) is accepted but not required by the wrapper. */ - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; + if (strcmp(cmd, "unmute-thread") == 0) { + return cmd_unmute_thread(rest, cargv); } - - char *did = NULL; - wf_status rs = resolve_actor_to_did(agent, actor, &did); - if (rs != WF_OK || !did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)rs); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-list-blocks") == 0) { + return cmd_get_list_blocks(rest, cargv); } - - wf_mod_decision *decision = NULL; - wf_status s = wf_agent_moderate_profile(agent, did, &decision); - free(did); - if (s != WF_OK || !decision) { - fprintf(stderr, "error: moderation failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-list-mutes") == 0) { + return cmd_get_list_mutes(rest, cargv); } - - wf_mod_ui ui = {0}; - s = wf_mod_decision_ui(decision, WF_MOD_CTX_PROFILE_VIEW, &ui); - if (s != WF_OK) { - fprintf(stderr, "error: decision UI failed (status %d)\n", (int)s); - wf_mod_decision_free(decision); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-suggested-follows") == 0) { + return cmd_get_suggested_follows(rest, cargv); } - - printf("moderation for %s: alerts=%zu blurs=%zu informs=%zu\n", - decision->did ? decision->did : actor, ui.alert_count, ui.blur_count, - ui.inform_count); - - wf_mod_ui_free(&ui); - wf_mod_decision_free(decision); - wf_agent_free(agent); - return 0; -} - -/* wolfram search [limit] */ -static int cmd_search(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "starter-pack") == 0) { + return cmd_starter_pack(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *query = argv[4]; - int limit = (argc >= 6) ? atoi(argv[5]) : 25; - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; + /* --- Server & account --- */ + if (strcmp(cmd, "create-account") == 0) { + return cmd_create_account(rest, cargv); } - - wf_agent_actor_list list = {0}; - wf_status s = - wf_agent_search_actors_typed(agent, query, limit, NULL, &list); - if (s != WF_OK) { - fprintf(stderr, "error: search failed (status %d)\n", (int)s); - wf_agent_actor_list_free(&list); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "app-password") == 0) { + return cmd_app_password(rest, cargv); } - - for (size_t i = 0; i < list.actor_count; ++i) { - const wf_agent_profile_view *a = &list.actors[i]; - printf("%s (%s)\n", a->handle ? a->handle : "?", a->did ? a->did : "?"); - if (a->display_name) { - printf(" %s\n", a->display_name); - } + if (strcmp(cmd, "invite-codes") == 0) { + return cmd_invite_codes(rest, cargv); } - if (list.actor_count == 0) { - printf("(no results)\n"); + if (strcmp(cmd, "activate") == 0) { + return cmd_activate(rest, cargv); } - - wf_agent_actor_list_free(&list); - wf_agent_free(agent); - return 0; -} - -/* wolfram mute */ -static int cmd_mute(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "deactivate") == 0) { + return cmd_deactivate(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; + if (strcmp(cmd, "check-status") == 0) { + return cmd_check_status(rest, cargv); } - - char *did = NULL; - wf_status s = resolve_actor_to_did(agent, actor, &did); - if (s != WF_OK || !did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "email") == 0) { + return cmd_email(rest, cargv); } - - s = wf_agent_mute(agent, did); - free(did); - if (s != WF_OK) { - fprintf(stderr, "error: mute failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "password-reset") == 0) { + return cmd_password_reset(rest, cargv); } - - printf("muted %s\n", actor); - wf_agent_free(agent); - return 0; -} - -/* wolfram unmute */ -static int cmd_unmute(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "reserve-signing-key") == 0) { + return cmd_reserve_signing_key(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; + if (strcmp(cmd, "get-service-auth") == 0) { + return cmd_get_service_auth(rest, cargv); } - - char *did = NULL; - wf_status s = resolve_actor_to_did(agent, actor, &did); - if (s != WF_OK || !did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "request-account-delete") == 0) { + return cmd_request_account_delete(rest, cargv); } - - s = wf_agent_unmute(agent, did); - free(did); - if (s != WF_OK) { - fprintf(stderr, "error: unmute failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "request-email-update") == 0) { + return cmd_request_email_update(rest, cargv); } - - printf("unmuted %s\n", actor); - wf_agent_free(agent); - return 0; -} - -/* ----------------------------------------------------------------- */ -/* New subcommands */ -/* ----------------------------------------------------------------- */ - -static int cmd_like(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "request-email-confirmation") == 0) { + return cmd_request_email_confirmation(rest, cargv); } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *at_uri = argv[4]; - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; + /* --- Identity --- */ + if (strcmp(cmd, "resolve-did") == 0) { + return cmd_resolve_did(rest, cargv); } - - char *cid = NULL; - wf_status s = resolve_post_cid(agent, at_uri, &cid); - if (s != WF_OK || !cid) { - fprintf(stderr, "error: could not resolve CID for %s (status %d)\n", - at_uri, (int)s); - free(cid); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "check-handle") == 0) { + return cmd_check_handle(rest, cargv); } - - wf_agent_post_result out = {0}; - s = wf_agent_like(agent, at_uri, cid, &out); - free(cid); - if (s != WF_OK) { - fprintf(stderr, "error: like failed (status %d)\n", (int)s); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 1; + if (strcmp(cmd, "get-recommended-did-credentials") == 0) { + return cmd_get_recommended_did_credentials(rest, cargv); + } + if (strcmp(cmd, "rotate-handle") == 0) { + return cmd_rotate_handle(rest, cargv); + } + if (strcmp(cmd, "plc") == 0) { + return cmd_plc(rest, cargv); } - printf("%s\n", out.uri ? out.uri : "(no uri returned)"); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 0; -} - -static int cmd_unlike(int argc, char **argv) { - if (argc < 5) { + /* --- Sync & blob --- */ + if (strcmp(cmd, "sync") == 0) { + if (rest >= 2) { + const char *sub = cargv[1]; + if (strcmp(sub, "get-blob") == 0) + return cmd_sync_get_blob(rest - 1, cargv + 1); + if (strcmp(sub, "get-blocks") == 0) + return cmd_sync_get_blocks(rest - 1, cargv + 1); + if (strcmp(sub, "get-record") == 0) + return cmd_sync_get_record(rest - 1, cargv + 1); + if (strcmp(sub, "list-blobs") == 0) + return cmd_sync_list_blobs(rest - 1, cargv + 1); + if (strcmp(sub, "subscribe") == 0) + return cmd_sync_subscribe(rest - 1, cargv + 1); + } + fprintf(stderr, "error: unknown sync subcommand\n"); usage_stream(stderr); return 0; } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *like_uri = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; + if (strcmp(cmd, "import-repo") == 0) { + return cmd_import_repo(rest, cargv); + } + if (strcmp(cmd, "list-missing-blobs") == 0) { + return cmd_list_missing_blobs(rest, cargv); } - wf_status s = wf_agent_unlike(agent, like_uri); - if (s != WF_OK) { - fprintf(stderr, "error: unlike failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - - printf("unliked %s\n", like_uri); - wf_agent_free(agent); - return 0; -} - -static int cmd_repost(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *at_uri = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - char *cid = NULL; - wf_status s = resolve_post_cid(agent, at_uri, &cid); - if (s != WF_OK || !cid) { - fprintf(stderr, "error: could not resolve CID for %s (status %d)\n", - at_uri, (int)s); - free(cid); - wf_agent_free(agent); - return 1; - } - - wf_agent_post_result out = {0}; - s = wf_agent_repost(agent, at_uri, cid, &out); - free(cid); - if (s != WF_OK) { - fprintf(stderr, "error: repost failed (status %d)\n", (int)s); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 1; - } - - printf("%s\n", out.uri ? out.uri : "(no uri returned)"); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 0; -} - -static int cmd_delete_repost(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *repost_uri = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_status s = wf_agent_delete_repost(agent, repost_uri); - if (s != WF_OK) { - fprintf(stderr, "error: delete-repost failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - - printf("deleted repost %s\n", repost_uri); - wf_agent_free(agent); - return 0; -} - -static int cmd_reply(int argc, char **argv) { - if (argc < 6) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *parent_uri = argv[4]; - - char *text = join_args(argc, argv, 5); - if (!text) { - fprintf(stderr, "error: failed to assemble reply text\n"); - return 1; - } - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - free(text); - return 1; - } - - char *parent_cid = NULL, *root_uri = NULL, *root_cid = NULL; - wf_status s = resolve_post_for_reply(agent, parent_uri, &parent_cid, - &root_uri, &root_cid); - if (s != WF_OK || !parent_cid || !root_uri || !root_cid) { - fprintf(stderr, - "error: could not resolve reply refs for %s (status %d)\n", - parent_uri, (int)s); - free(parent_cid); - free(root_uri); - free(root_cid); - free(text); - wf_agent_free(agent); - return 1; - } - - char created_at[32]; - now_rfc3339(created_at, sizeof(created_at)); - - cJSON *rec = cJSON_CreateObject(); - cJSON_AddStringToObject(rec, "$type", "app.bsky.feed.post"); - cJSON_AddStringToObject(rec, "text", text); - cJSON_AddStringToObject(rec, "createdAt", created_at); - - cJSON *reply = cJSON_CreateObject(); - cJSON *root = cJSON_CreateObject(); - cJSON_AddStringToObject(root, "uri", root_uri); - cJSON_AddStringToObject(root, "cid", root_cid); - cJSON *parent = cJSON_CreateObject(); - cJSON_AddStringToObject(parent, "uri", parent_uri); - cJSON_AddStringToObject(parent, "cid", parent_cid); - cJSON_AddItemToObject(reply, "root", root); - cJSON_AddItemToObject(reply, "parent", parent); - cJSON_AddItemToObject(rec, "reply", reply); - - char *record_json = cJSON_PrintUnformatted(rec); - cJSON_Delete(rec); - free(parent_cid); - free(root_uri); - free(root_cid); - free(text); - if (!record_json) { - fprintf(stderr, "error: failed to serialize reply record\n"); - wf_agent_free(agent); - return 1; - } - - wf_agent_post_result out = {0}; - s = wf_agent_create_record(agent, "app.bsky.feed.post", record_json, &out); - free(record_json); - if (s != WF_OK) { - cli_agent_error("reply", s, agent); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 1; - } - - printf("%s\n", out.uri ? out.uri : "(no uri returned)"); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 0; -} - -/* Labels streaming — subscribe to com.atproto.label.subscribeLabels and - * print each arriving label. The subscription blocks until interrupted (Ctrl-C - * / SIGINT), a time bound elapses, or a small event cap is reached; it is - * stopped cleanly via the handle pointer trick (mirrors - * examples/subscribe_labels.c). */ -#define CLI_LABEL_MAX_EVENTS 100 - -static volatile sig_atomic_t g_label_stop = 0; -static int g_label_count = 0; -static time_t g_label_start = 0; -static int g_label_seconds = 30; -static wf_label_subscribe_handle **g_label_handle_ptr = NULL; - -static void label_on_sigint(int sig) { - (void)sig; - g_label_stop = 1; - if (g_label_handle_ptr && *g_label_handle_ptr) { - wf_label_subscribe_stop(*g_label_handle_ptr); - } -} - -static void label_on_label(const wf_label *label, void *userdata) { - (void)userdata; - g_label_count++; - printf("label: uri=%s cid=%s val=%s src=%s exp=%s neg=%d\n", - label->uri ? label->uri : "", label->cid ? label->cid : "", - label->val ? label->val : "", label->src ? label->src : "", - label->exp ? label->exp : "", label->neg); - fflush(stdout); - if (g_label_stop || - (g_label_seconds > 0 && - (time(NULL) - g_label_start) >= g_label_seconds) || - (g_label_handle_ptr && *g_label_handle_ptr && - g_label_count >= CLI_LABEL_MAX_EVENTS)) { - if (g_label_handle_ptr && *g_label_handle_ptr) { - wf_label_subscribe_stop(*g_label_handle_ptr); - } - } -} - -static void label_on_info(const wf_label_info *info, void *userdata) { - (void)userdata; - printf("info: name=%s message=%s\n", info->name ? info->name : "", - info->message ? info->message : "(none)"); - fflush(stdout); -} - -static void label_on_error(wf_status status, const char *msg, void *userdata) { - (void)userdata; - fprintf(stderr, "error: label stream status=%d %s\n", (int)status, - msg ? msg : ""); -} - -/* wolfram labels subscribe [--cursor N] [--seconds N] */ -static int cmd_labels(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - - const char *sub = argv[1]; - if (strcmp(sub, "subscribe") != 0) { - fprintf(stderr, - "error: unknown labels subcommand '%s' (try 'subscribe')\n", - sub); - return 1; - } - - const char *service = argv[2]; - - int64_t cursor = 0; - int has_cursor = 0; - for (int i = 3; i < argc; ++i) { - if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) { - cursor = (int64_t)strtoll(argv[++i], NULL, 10); - has_cursor = 1; - } else if (strcmp(argv[i], "--seconds") == 0 && i + 1 < argc) { - g_label_seconds = atoi(argv[++i]); - } else { - fprintf(stderr, "error: unexpected labels argument '%s'\n", - argv[i]); - return 1; - } - } - - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = label_on_sigint; - sigaction(SIGINT, &sa, NULL); - - g_label_stop = 0; - g_label_count = 0; - g_label_start = time(NULL); - - wf_label_subscribe_options opts; - memset(&opts, 0, sizeof(opts)); - opts.service = service; - opts.cursor = cursor; - opts.has_cursor = has_cursor; - opts.reconnect_delay_ms = 1000; - opts.on_label = label_on_label; - opts.on_neg = label_on_label; - opts.on_info = label_on_info; - opts.on_error = label_on_error; - - printf( - "subscribing to label stream at %s (cursor=%s, max %d events, %ds)\n", - service, has_cursor ? argv[2] : "none", CLI_LABEL_MAX_EVENTS, - g_label_seconds); - - wf_label_subscribe_handle *handle = NULL; - g_label_handle_ptr = &handle; - wf_status s = wf_label_subscribe_start(&opts, &handle); - g_label_handle_ptr = NULL; - - if (s != WF_OK) { - fprintf(stderr, "error: label subscription ended (status %d)\n", - (int)s); - return 1; - } - printf("label subscription ended cleanly (%d labels)\n", g_label_count); - return 0; -} - -/* Print a raw JSON agent response, freeing resources, returning 0 on success. - * On error prints to stderr and returns 1. */ -int finish_agent_response(wf_agent *agent, wf_status s, wf_response *res) { - int rc = 0; - if (s != WF_OK) { - fprintf(stderr, "error: request failed (status %d)\n", (int)s); - rc = 1; - } else if (res->body && res->body_len > 0) { - printf("%s\n", res->body); - } else { - printf("(empty response, HTTP %ld)\n", res->status); - } - wf_response_free(res); - wf_agent_free(agent); - return rc; -} - -/* Read the entire text file `path` into a heap string (NUL terminated). - * Returns NULL on failure. Caller frees. */ -char *read_text_file(const char *path) { - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return NULL; - } - long len = ftell(f); - if (len < 0) { - fclose(f); - return NULL; - } - rewind(f); - char *data = malloc((size_t)len + 1); - if (!data) { - fclose(f); - return NULL; - } - size_t n = fread(data, 1, (size_t)len, f); - fclose(f); - data[n] = '\0'; - return data; -} - -/* wolfram follows [limit] */ -static int cmd_follows(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *actor = argv[2]; - int limit = (argc >= 4) ? atoi(argv[3]) : 50; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_follows(agent, actor, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram followers [limit] */ -static int cmd_followers(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *actor = argv[2]; - int limit = (argc >= 4) ? atoi(argv[3]) : 50; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_followers(agent, actor, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram blocks [limit] */ -static int cmd_blocks(int argc, char **argv) { - if (argc < 4) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - int limit = (argc >= 5) ? atoi(argv[4]) : 50; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_blocks(agent, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram mutes [limit] */ -static int cmd_mutes(int argc, char **argv) { - if (argc < 4) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - int limit = (argc >= 5) ? atoi(argv[4]) : 50; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_mutes(agent, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram revoke-account-credentials */ -static int cmd_revoke_account_credentials(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *account = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_status s = wf_agent_revoke_account_credentials(agent, account); - if (s != WF_OK) { - fprintf(stderr, "error: revokeAccountCredentials failed (status %d)\n", - (int)s); - wf_agent_free(agent); - return 1; - } - printf("revoked credentials for %s\n", account); - wf_agent_free(agent); - return 0; -} - -/* wolfram list [limit] */ -static int cmd_list(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *list_uri = argv[2]; - int limit = (argc >= 4) ? atoi(argv[3]) : 50; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_list(agent, list_uri, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram lists [limit] */ -static int cmd_lists(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *actor = argv[2]; - int limit = (argc >= 4) ? atoi(argv[3]) : 50; - - wf_agent *agent = wf_agent_new(service); - if (!agent) { - fprintf(stderr, "error: failed to create agent\n"); - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_lists(agent, actor, limit, NULL, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram get-record */ -static int cmd_get_record(int argc, char **argv) { - if (argc < 6) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *collection = argv[4]; - const char *rkey = argv[5]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_record(agent, collection, rkey, &res); - return finish_agent_response(agent, s, &res); -} - -/* wolfram video upload - * wolfram video status */ -static int cmd_video(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - - const char *sub = argv[1]; - if (strcmp(sub, "upload") == 0) { - if (argc < 6) { - usage_stream(stderr); - return 0; - } - const char *service = argv[2]; - const char *handle = argv[3]; - const char *password = argv[4]; - const char *path = argv[5]; - - FILE *f = fopen(path, "rb"); - if (!f) { - fprintf(stderr, "error: cannot open '%s'\n", path); - return 1; - } - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - fprintf(stderr, "error: cannot seek '%s'\n", path); - return 1; - } - long size = ftell(f); - if (size < 0) { - fclose(f); - fprintf(stderr, "error: cannot stat '%s'\n", path); - return 1; - } - rewind(f); - void *buf = malloc((size_t)size > 0 ? (size_t)size : 1); - if (!buf) { - fclose(f); - fprintf(stderr, "error: out of memory\n"); - return 1; - } - if (fread(buf, 1, (size_t)size, f) != (size_t)size) { - fclose(f); - free(buf); - fprintf(stderr, "error: failed to read '%s'\n", path); - return 1; - } - fclose(f); - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - free(buf); - return 1; - } - - wf_uploaded_blob blob = {0}; - wf_status s = - wf_agent_upload_video(agent, buf, (size_t)size, "video/mp4", &blob); - free(buf); - if (s != WF_OK) { - fprintf(stderr, "error: video upload failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - - printf("uploaded: cid=%s mime=%s size=%zu\n", blob.cid ? blob.cid : "?", - blob.mime_type ? blob.mime_type : "?", blob.size); - wf_uploaded_blob_free(&blob); - wf_agent_free(agent); - return 0; - } - - if (strcmp(sub, "status") == 0) { - if (argc < 6) { - usage_stream(stderr); - return 0; - } - const char *service = argv[2]; - const char *handle = argv[3]; - const char *password = argv[4]; - const char *job_id = argv[5]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) { - return 1; - } - - wf_response res = {0}; - wf_status s = wf_agent_get_video_job_status(agent, job_id, &res); - return finish_agent_response(agent, s, &res); - } - - fprintf(stderr, - "error: unknown video subcommand '%s' (try 'upload' or 'status')\n", - sub); - return 1; -} - -/* wolfram block */ -static int cmd_block(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) return 1; - - char *did = NULL; - wf_status s = resolve_actor_to_did(agent, actor, &did); - if (s != WF_OK || !did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; + /* --- Preferences & push --- */ + if (strcmp(cmd, "preferences") == 0) { + return cmd_preferences(rest, cargv); } - - wf_agent_post_result out = {0}; - s = wf_agent_block(agent, did, &out); - free(did); - if (s != WF_OK) { - fprintf(stderr, "error: block failed (status %d)\n", (int)s); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 1; - } - printf("%s\n", out.uri ? out.uri : "(no uri returned)"); - wf_agent_post_result_free(&out); - wf_agent_free(agent); - return 0; -} - -/* wolfram unblock */ -static int cmd_unblock(int argc, char **argv) { - if (argc < 5) { - usage_stream(stderr); - return 0; - } - const char *service = argv[1]; - const char *handle = argv[2]; - const char *password = argv[3]; - const char *actor = argv[4]; - - wf_agent *agent = agent_login_or_err(service, handle, password); - if (!agent) return 1; - - char *did = NULL; - wf_status s = resolve_actor_to_did(agent, actor, &did); - if (s != WF_OK || !did) { - fprintf(stderr, "error: could not resolve actor '%s' (status %d)\n", - actor, (int)s); - wf_agent_free(agent); - return 1; - } - - wf_response res = {0}; - s = wf_agent_get_blocks(agent, 100, NULL, &res); - if (s != WF_OK && s != WF_ERR_HTTP) { - fprintf(stderr, "error: getBlocks failed (status %d)\n", (int)s); - wf_response_free(&res); - free(did); - wf_agent_free(agent); - return 1; - } - - char *block_uri = NULL; - if (res.body) { - cJSON *root = cJSON_ParseWithLength(res.body, res.body_len); - if (root) { - cJSON *blocks = cJSON_GetObjectItemCaseSensitive(root, "blocks"); - if (cJSON_IsArray(blocks)) { - for (int i = 0; i < cJSON_GetArraySize(blocks); ++i) { - cJSON *b = cJSON_GetArrayItem(blocks, i); - cJSON *subj = - cJSON_GetObjectItemCaseSensitive(b, "subject"); - cJSON *sdid = - subj ? cJSON_GetObjectItemCaseSensitive(subj, "did") - : NULL; - cJSON *uri = cJSON_GetObjectItemCaseSensitive(b, "uri"); - if (cJSON_IsString(sdid) && cJSON_IsString(uri) && - strcmp(sdid->valuestring, did) == 0) { - block_uri = strdup(uri->valuestring); - break; - } - } - } - cJSON_Delete(root); - } - } - wf_response_free(&res); - free(did); - - if (!block_uri) { - printf("(not currently blocking %s)\n", actor); - wf_agent_free(agent); - return 0; - } - - s = wf_agent_unblock(agent, block_uri); - free(block_uri); - if (s != WF_OK) { - fprintf(stderr, "error: unblock failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - printf("unblocked %s\n", actor); - wf_agent_free(agent); - return 0; -} - -/* wolfram repo ... (put-record / delete-record / list-records / - * describe) */ -static int cmd_repo(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; - } - const char *sub = argv[1]; - - /* wolfram repo put-record --collection - * --rkey --json */ - if (strcmp(sub, "put-record") == 0) { - const char *collection = NULL, *rkey = NULL, *json = NULL; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) - collection = argv[++i]; - else if (strcmp(argv[i], "--rkey") == 0 && i + 1 < argc) - rkey = argv[++i]; - else if (strcmp(argv[i], "--json") == 0 && i + 1 < argc) - json = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !collection || !rkey || !json) { - fprintf(stderr, - "error: usage: wolfram repo put-record " - " --collection --rkey " - "--json \n"); - return 1; - } - const char *record_json = json; - char *file_buf = NULL; - if (json[0] != '{') { - file_buf = read_text_file(json); - if (!file_buf) { - fprintf(stderr, "error: cannot read record file '%s'\n", json); - return 1; - } - record_json = file_buf; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) { - free(file_buf); - return 1; - } - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - const char *repo = sd.did ? sd.did : pos[1]; - wf_repo_write_record_result out = {0}; - wf_status s = wf_agent_put_record_typed( - agent, repo, collection, rkey, -1, record_json, NULL, NULL, &out); - free(file_buf); - wf_agent_session_data_free(&sd); - if (s != WF_OK) { - fprintf(stderr, "error: putRecord failed (status %d)\n", (int)s); - wf_repo_write_record_result_free(&out); - wf_agent_free(agent); - return 1; - } - printf("%s\n", out.uri ? out.uri : "(no uri returned)"); - wf_repo_write_record_result_free(&out); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "register-push") == 0) { + return cmd_register_push(rest, cargv); } - - /* wolfram repo delete-record --collection - * - * --rkey */ - if (strcmp(sub, "delete-record") == 0) { - const char *collection = NULL, *rkey = NULL; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) - collection = argv[++i]; - else if (strcmp(argv[i], "--rkey") == 0 && i + 1 < argc) - rkey = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !collection || !rkey) { - fprintf(stderr, - "error: usage: wolfram repo delete-record " - " --collection --rkey \n"); - return 1; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - const char *repo = sd.did ? sd.did : pos[1]; - wf_status s = wf_agent_delete_record_typed(agent, repo, collection, - rkey, NULL, NULL); - wf_agent_session_data_free(&sd); - if (s != WF_OK) { - fprintf(stderr, "error: deleteRecord failed (status %d)\n", (int)s); - wf_agent_free(agent); - return 1; - } - printf("deleted %s/%s/%s\n", repo, collection, rkey); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "unregister-push") == 0) { + return cmd_unregister_push(rest, cargv); } - - /* wolfram repo list-records --collection - * - * [--limit N] [--cursor C] */ - if (strcmp(sub, "list-records") == 0) { - const char *collection = NULL, *cursor = NULL; - int limit = 50; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--collection") == 0 && i + 1 < argc) - collection = argv[++i]; - else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) - limit = atoi(argv[++i]); - else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) - cursor = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !collection) { - fprintf(stderr, - "error: usage: wolfram repo list-records " - " --collection [--limit N] " - "[--cursor C]\n"); - return 1; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - wf_session_data sd = {0}; - wf_agent_get_session_data(agent, &sd); - const char *repo = sd.did ? sd.did : pos[1]; - if (g_json) { - wf_response res = {0}; - wf_status s = - wf_agent_list_records(agent, collection, limit, cursor, &res); - wf_agent_session_data_free(&sd); - return finish_agent_response(agent, s, &res); - } - wf_repo_record_list list = {0}; - wf_status s = wf_agent_list_records_typed(agent, repo, collection, - limit, cursor, 0, &list); - wf_agent_session_data_free(&sd); - if (s != WF_OK) { - fprintf(stderr, "error: listRecords failed (status %d)\n", (int)s); - wf_repo_record_list_free(&list); - wf_agent_free(agent); - return 1; - } - for (size_t i = 0; i < list.count; ++i) { - const wf_repo_record *r = &list.items[i]; - printf("%s (cid=%s)\n", r->uri ? r->uri : "?", - r->has_cid && r->cid ? r->cid : "?"); - } - if (list.cursor) printf("cursor: %s\n", list.cursor); - if (list.count == 0) printf("(no records)\n"); - wf_repo_record_list_free(&list); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "update-profile") == 0) { + return cmd_update_profile(rest, cargv); } - - /* wolfram repo describe --repo - * */ - if (strcmp(sub, "describe") == 0) { - const char *repo = NULL; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--repo") == 0 && i + 1 < argc) - repo = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !repo) { - fprintf(stderr, "error: usage: wolfram repo describe " - " --repo \n"); - return 1; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - if (g_json) { - wf_response res = {0}; - wf_status s = wf_agent_describe_repo(agent, repo, &res); - return finish_agent_response(agent, s, &res); - } - wf_repo_description desc = {0}; - wf_status s = wf_agent_describe_repo_typed(agent, repo, &desc); - if (s != WF_OK) { - fprintf(stderr, "error: describeRepo failed (status %d)\n", (int)s); - wf_repo_description_free(&desc); - wf_agent_free(agent); - return 1; - } - printf("handle: %s\n", desc.handle ? desc.handle : "?"); - printf("did: %s\n", desc.did ? desc.did : "?"); - printf("handleIsCorrect: %d\n", desc.handle_is_correct); - printf("collections (%zu):\n", desc.collection_count); - for (size_t i = 0; i < desc.collection_count; ++i) { - printf(" %s\n", desc.collections[i] ? desc.collections[i] : "?"); - } - wf_repo_description_free(&desc); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "put-actor-status") == 0) { + return cmd_put_actor_status(rest, cargv); } - - fprintf(stderr, - "error: unknown repo subcommand '%s' " - "(try put-record/delete-record/list-records/describe)\n", - sub); - return 1; -} - -/* wolfram feed ... (get / author) */ -static int cmd_feed(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; + if (strcmp(cmd, "upload-blob") == 0) { + return cmd_upload_blob(rest, cargv); } - const char *sub = argv[1]; - - /* wolfram feed get --feed - * [--limit N] [--cursor C] */ - if (strcmp(sub, "get") == 0) { - const char *feed = NULL, *cursor = NULL; - int limit = 50; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--feed") == 0 && i + 1 < argc) - feed = argv[++i]; - else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) - limit = atoi(argv[++i]); - else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) - cursor = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !feed) { - fprintf( - stderr, - "error: usage: wolfram feed get " - " --feed [--limit N] [--cursor C]\n"); - return 1; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - if (g_json) { - wf_response res = {0}; - wf_status s = wf_agent_get_feed(agent, feed, limit, cursor, &res); - return finish_agent_response(agent, s, &res); - } - wf_agent_feed_view_list list = {0}; - wf_status s = - wf_agent_get_feed_typed(agent, feed, limit, cursor, &list); - if (s != WF_OK) { - fprintf(stderr, "error: getFeed failed (status %d)\n", (int)s); - wf_agent_feed_view_list_free(&list); - wf_agent_free(agent); - return 1; - } - for (size_t i = 0; i < list.item_count; ++i) { - const wf_agent_post_view *post = &list.items[i].post; - const char *author = - post->author.handle - ? post->author.handle - : (post->author.did ? post->author.did : "?"); - const char *text = ""; - if (post->record) { - cJSON *t = - cJSON_GetObjectItemCaseSensitive(post->record, "text"); - if (cJSON_IsString(t) && t->valuestring) text = t->valuestring; - } - printf("%s: %s\n", author, text); - } - if (list.cursor) printf("cursor: %s\n", list.cursor); - if (list.item_count == 0) printf("(empty feed)\n"); - wf_agent_feed_view_list_free(&list); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "apply-writes") == 0) { + return cmd_apply_writes(rest, cargv); } - - /* wolfram feed author --actor - * [--limit N] [--cursor C] */ - if (strcmp(sub, "author") == 0) { - const char *actor = NULL, *cursor = NULL; - int limit = 50; - const char *pos[3]; - int pi = 0; - for (int i = 2; i < argc; ++i) { - if (strcmp(argv[i], "--actor") == 0 && i + 1 < argc) - actor = argv[++i]; - else if (strcmp(argv[i], "--limit") == 0 && i + 1 < argc) - limit = atoi(argv[++i]); - else if (strcmp(argv[i], "--cursor") == 0 && i + 1 < argc) - cursor = argv[++i]; - else if (pi < 3) - pos[pi++] = argv[i]; - } - if (pi < 3 || !actor) { - fprintf(stderr, - "error: usage: wolfram feed author " - " --actor [--limit N] [--cursor " - "C]\n"); - return 1; - } - wf_agent *agent = agent_login_or_err(pos[0], pos[1], pos[2]); - if (!agent) return 1; - if (g_json) { - wf_response res = {0}; - wf_status s = wf_agent_get_author_feed(agent, actor, limit, cursor, - NULL, false, &res); - return finish_agent_response(agent, s, &res); - } - wf_agent_feed_list list = {0}; - wf_status s = wf_agent_get_author_feed_typed(agent, actor, limit, - cursor, NULL, &list); - if (s != WF_OK) { - fprintf(stderr, "error: getAuthorFeed failed (status %d)\n", - (int)s); - wf_agent_feed_list_free(&list); - wf_agent_free(agent); - return 1; - } - for (size_t i = 0; i < list.item_count; ++i) { - const wf_agent_post_view *post = &list.items[i].post; - const char *author = - post->author.handle - ? post->author.handle - : (post->author.did ? post->author.did : "?"); - const char *text = ""; - if (post->record) { - cJSON *t = - cJSON_GetObjectItemCaseSensitive(post->record, "text"); - if (cJSON_IsString(t) && t->valuestring) text = t->valuestring; - } - printf("%s: %s\n", author, text); - } - if (list.cursor) printf("cursor: %s\n", list.cursor); - if (list.item_count == 0) printf("(empty feed)\n"); - wf_agent_feed_list_free(&list); - wf_agent_free(agent); - return 0; + if (strcmp(cmd, "send-interactions") == 0) { + return cmd_send_interactions(rest, cargv); } - fprintf(stderr, "error: unknown feed subcommand '%s' (try get/author)\n", - sub); - return 1; -} - -/* ----------------------------------------------------------------- */ -/* Dispatch */ -/* ----------------------------------------------------------------- */ - -int main(int argc, char **argv) { - if (argc < 2) { - return usage_exit(); + /* --- Chat --- */ + if (strcmp(cmd, "convos") == 0) { + return cmd_convos(rest, cargv); } - - /* Strip a leading global --json flag (and any others) so the command and - * its positional arguments remain contiguous at argv[1..]. */ - { - int out = 1; - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--json") == 0) { - g_json = true; - continue; - } - argv[out++] = argv[i]; - } - argc = out; + if (strcmp(cmd, "messages") == 0) { + return cmd_messages(rest, cargv); } - - const char *cmd = argv[1]; - - /* --help / -h / help print usage to stdout and exit 0 — without touching - * the network. Genuinely unknown commands still print to stderr below. */ - if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { - return usage_exit(); + if (strcmp(cmd, "chat") == 0) { + return cmd_chat(rest, cargv); } - - if (strcmp(cmd, "--version") == 0 || strcmp(cmd, "-v") == 0) { - printf("%s\n", WOLFRAM_VERSION_STRING); - return 0; + if (strcmp(cmd, "groups") == 0) { + return cmd_groups(rest, cargv); } - - if (strcmp(cmd, "help") == 0) { - if (argc >= 3) { - cmd_help_stream(stdout, argv[2]); - } else { - usage_stream(stdout); - } - return 0; + if (strcmp(cmd, "reactions") == 0) { + return cmd_reactions(rest, cargv); } - int rest = argc - 1; /* arguments after the program name */ - - /* Re-base argv so handlers see as argv[1] etc. */ - char **cargv = argv + 1; - - if (strcmp(cmd, "login") == 0) { - return cmd_login(rest, cargv); - } - if (strcmp(cmd, "whoami") == 0) { - return cmd_whoami(rest, cargv); - } - if (strcmp(cmd, "describe-server") == 0) { - return cmd_describe_server(rest, cargv); - } - if (strcmp(cmd, "post") == 0) { - return cmd_post(rest, cargv); - } - if (strcmp(cmd, "reply") == 0) { - return cmd_reply(rest, cargv); - } - if (strcmp(cmd, "delete") == 0) { - return cmd_delete(rest, cargv); - } - if (strcmp(cmd, "like") == 0) { - return cmd_like(rest, cargv); - } - if (strcmp(cmd, "unlike") == 0) { - return cmd_unlike(rest, cargv); - } - if (strcmp(cmd, "repost") == 0) { - return cmd_repost(rest, cargv); - } - if (strcmp(cmd, "delete-repost") == 0 || strcmp(cmd, "unrepost") == 0) { - return cmd_delete_repost(rest, cargv); - } - if (strcmp(cmd, "labels") == 0) { - return cmd_labels(rest, cargv); - } - if (strcmp(cmd, "oauth-login") == 0) { - return cmd_oauth_login(rest, cargv); - } - if (strcmp(cmd, "oauth-callback") == 0) { - return cmd_oauth_callback(rest, cargv); - } - if (strcmp(cmd, "timeline") == 0) { - return cmd_timeline(rest, cargv); - } - if (strcmp(cmd, "get-post") == 0) { - return cmd_get_post(rest, cargv); - } - if (strcmp(cmd, "profile") == 0) { - return cmd_profile(rest, cargv); - } - if (strcmp(cmd, "follow") == 0) { - return cmd_follow(rest, cargv); - } - if (strcmp(cmd, "unfollow") == 0) { - return cmd_unfollow(rest, cargv); - } - if (strcmp(cmd, "resolve") == 0) { - return cmd_resolve(rest, cargv); - } - if (strcmp(cmd, "thread") == 0) { - return cmd_thread(rest, cargv); - } - if (strcmp(cmd, "notifications") == 0) { - return cmd_notifications(rest, cargv); - } - if (strcmp(cmd, "search") == 0) { - return cmd_search(rest, cargv); - } - if (strcmp(cmd, "moderation") == 0) { - return cmd_moderation(rest, cargv); - } - if (strcmp(cmd, "follows") == 0) { - return cmd_follows(rest, cargv); - } - if (strcmp(cmd, "followers") == 0) { - return cmd_followers(rest, cargv); - } - if (strcmp(cmd, "blocks") == 0) { - return cmd_blocks(rest, cargv); - } - if (strcmp(cmd, "mutes") == 0) { - return cmd_mutes(rest, cargv); + /* --- Sync/Jetstream/Firehose --- */ + if (strcmp(cmd, "jetstream") == 0) { + return cmd_jetstream(rest, cargv); } - if (strcmp(cmd, "revoke-account-credentials") == 0) { - return cmd_revoke_account_credentials(rest, cargv); - } - if (strcmp(cmd, "mute") == 0) { - return cmd_mute(rest, cargv); - } - if (strcmp(cmd, "unmute") == 0) { - return cmd_unmute(rest, cargv); - } - if (strcmp(cmd, "list") == 0) { - return cmd_list(rest, cargv); - } - if (strcmp(cmd, "lists") == 0) { - return cmd_lists(rest, cargv); - } - if (strcmp(cmd, "get-record") == 0) { - return cmd_get_record(rest, cargv); - } - if (strcmp(cmd, "video") == 0) { - return cmd_video(rest, cargv); - } - if (strcmp(cmd, "block") == 0) { - return cmd_block(rest, cargv); - } - if (strcmp(cmd, "unblock") == 0) { - return cmd_unblock(rest, cargv); - } - if (strcmp(cmd, "repo") == 0) { - return cmd_repo(rest, cargv); - } - if (strcmp(cmd, "feed") == 0) { - return cmd_feed(rest, cargv); + if (strcmp(cmd, "firehose") == 0) { + return cmd_firehose(rest, cargv); } fprintf(stderr, "error: unknown command '%s'\n\n", cmd); diff --git a/src/cli/main_internal.h b/src/cli/main_internal.h index 1ce047b..2b37c77 100644 --- a/src/cli/main_internal.h +++ b/src/cli/main_internal.h @@ -9,6 +9,7 @@ #include "wolfram/agent.h" #include +#include /* Print the full `wolfram ...` usage summary to `out`. */ void usage_stream(FILE *out); @@ -42,4 +43,24 @@ int finish_agent_response(wf_agent *agent, wf_status s, wf_response *res); * Returns NULL on failure. Caller frees. */ char *read_text_file(const char *path); +/* Global --json flag: when set, list/get commands print the raw JSON body + * instead of human-readable text. */ +extern bool g_json; + +/* Create an agent, login, and return it. Returns NULL on failure (error + * already printed to stderr). Caller frees with wf_agent_free. */ +wf_agent *agent_login_or_err(const char *service, const char *handle, + const char *password); + +/* Print an agent-level failure to stderr, appending the server's XRPC error + * message when one was captured. Returns the failure exit code. */ +int cli_agent_error(const char *what, wf_status s, wf_agent *agent); + +/* Format the current UTC time as an RFC 3339 timestamp (e.g. for record + * createdAt fields). Writes at most `len` bytes into `buf`. */ +void now_rfc3339(char *buf, size_t len); + +/* Join argv[first..argc-1] into a single heap string (caller frees). */ +char *join_args(int argc, char **argv, int first); + #endif /* WOLFRAM_CLI_MAIN_INTERNAL_H */