Add C SDK (rb.h + rb.c) with API download endpoint and namespace settings links.
This commit is contained in:
parent
f661938820
commit
7575f74837
6 changed files with 677 additions and 2 deletions
|
|
@ -91,7 +91,8 @@ c.delete_node(node_id) # permanent, moderator only
|
|||
|
||||
### Key details
|
||||
|
||||
- **Client source**: `remarkbox/api/remarkbox_client.py` (stdlib only, no pip)
|
||||
- **Python client**: `remarkbox/api/remarkbox_client.py` (stdlib only, no pip)
|
||||
- **C client**: `remarkbox/api/rb.c` (compile: `gcc rb.c -o rb -lcurl`)
|
||||
- **API docs**: `docs/api.md`
|
||||
- **Identity**: Authenticated as `timehexon@unturf.com` (display name: `timehexon`)
|
||||
- **Journey thread**: `9f970183-ffaf-11f0-b565-040140774501` on `meta.remarkbox.com` -- update this after finishing work
|
||||
|
|
@ -115,6 +116,7 @@ c.delete_node(node_id) # permanent, moderator only
|
|||
| GET | `/api/v1/user/profile` | Get profile |
|
||||
| PATCH | `/api/v1/user/profile` | Update display name |
|
||||
| GET | `/api/v1/clients/python` | Download Python client |
|
||||
| GET | `/api/v1/clients/c` | Download C client (rb.c) |
|
||||
| GET | `/api/v1/admin/namespaces` | List all namespaces (superuser only) |
|
||||
| GET | `/api/v1/admin/recent-nodes?days=7` | Recent nodes network-wide (superuser only) |
|
||||
|
||||
|
|
@ -248,7 +250,7 @@ Follow Russell Ballestrini's capability-driven presentation practice
|
|||
(russell.ballestrini.net/capability-driven-presentation/). A page need not look
|
||||
identical across all browsers. Accommodate what the user's browser can do:
|
||||
|
||||
1. **Single canonical URI** — one URL serves the content.
|
||||
1. **Single canonical URI** — one URI serves the content.
|
||||
2. **Consistent content** — regardless of viewer capabilities.
|
||||
3. **Graceful enhancement/degradation** — use available capabilities to enhance presentation.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ def includeme(config):
|
|||
config.add_route("api-auth-verify", "/api/v1/auth/verify")
|
||||
config.add_route("api-user-profile", "/api/v1/user/profile")
|
||||
config.add_route("api-client-python", "/api/v1/clients/python")
|
||||
config.add_route("api-client-c", "/api/v1/clients/c")
|
||||
config.add_route("api-webmention", "/api/v1/webmention")
|
||||
config.add_route("api-admin-namespaces", "/api/v1/admin/namespaces")
|
||||
config.add_route("api-admin-recent-nodes", "/api/v1/admin/recent-nodes")
|
||||
|
|
|
|||
156
remarkbox/api/rb.c
Normal file
156
remarkbox/api/rb.c
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* rb.c — Remarkbox CLI client (uses rb.h library)
|
||||
*
|
||||
* Compile:
|
||||
* gcc rb.c -o rb -lcurl
|
||||
*
|
||||
* Download both files:
|
||||
* curl -s https://REMARKBOX/api/v1/clients/c -o rb.c
|
||||
* curl -s https://REMARKBOX/api/v1/clients/c?file=rb.h -o rb.h
|
||||
* gcc rb.c -o rb -lcurl
|
||||
*
|
||||
* Usage:
|
||||
* rb [uri] <command> [args...]
|
||||
*
|
||||
* If uri is omitted, uses REMARKBOX_URL environment variable.
|
||||
* Session cookies stored at REMARKBOX_COOKIES or ~/.config/remarkbox/cookies.txt
|
||||
*
|
||||
* License: Same as Remarkbox
|
||||
*/
|
||||
|
||||
#define RB_IMPLEMENTATION
|
||||
#include "rb.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CLI_VERSION RB_LIB_VERSION
|
||||
|
||||
static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"rb %s — Remarkbox CLI client\n"
|
||||
"\n"
|
||||
"Usage: rb [uri] <command> [args...]\n"
|
||||
"\n"
|
||||
"If uri is omitted, uses REMARKBOX_URL environment variable.\n"
|
||||
"Session cookies are stored at REMARKBOX_COOKIES or\n"
|
||||
"~/.config/remarkbox/cookies.txt\n"
|
||||
"\n"
|
||||
"Commands:\n"
|
||||
" version Deployed version\n"
|
||||
" threads <namespace> List threads\n"
|
||||
" thread <node_id> Get thread with replies\n"
|
||||
" search <namespace> <query> Search threads\n"
|
||||
" node <node_id> Get a single node\n"
|
||||
" post <namespace> <title> <data> [name] Create thread\n"
|
||||
" reply <node_id> <data> [name] Reply to node\n"
|
||||
" edit <node_id> <data> Edit a node\n"
|
||||
" disable <node_id> Disable a node\n"
|
||||
" enable <node_id> Enable a node\n"
|
||||
" approve <node_id> Approve a node\n"
|
||||
" lock <node_id> Lock a thread\n"
|
||||
" unlock <node_id> Unlock a thread\n"
|
||||
" delete <node_id> Delete a node\n"
|
||||
" login <email> Request OTP\n"
|
||||
" verify <email> <otp> Verify OTP\n"
|
||||
" profile Show current user\n"
|
||||
"\n"
|
||||
"Examples:\n"
|
||||
" rb https://my.remarkbox.com threads meta.remarkbox.com\n"
|
||||
" rb threads meta.remarkbox.com # uses REMARKBOX_URL\n"
|
||||
" rb login user@example.com\n"
|
||||
" rb verify user@example.com 123456\n"
|
||||
" rb post meta.remarkbox.com \"Title\" \"Body\" MyBot\n"
|
||||
"\n"
|
||||
"Compile: gcc rb.c -o rb -lcurl\n"
|
||||
"\n", CLI_VERSION);
|
||||
}
|
||||
|
||||
/* Run a command, print the response, return exit code. */
|
||||
static int run(rb_response_t r)
|
||||
{
|
||||
if (r.body && r.body_len > 0)
|
||||
rb_json_pretty(r.body);
|
||||
int rc = r.ok ? 0 : 1;
|
||||
rb_response_free(&r);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const char *base_uri;
|
||||
int cmd_start;
|
||||
|
||||
if (argc < 2) { usage(); return 1; }
|
||||
|
||||
/* Determine if first arg is a URI or a command */
|
||||
if (strncmp(argv[1], "http://", 7) == 0 || strncmp(argv[1], "https://", 8) == 0) {
|
||||
base_uri = argv[1];
|
||||
cmd_start = 2;
|
||||
} else {
|
||||
base_uri = getenv("REMARKBOX_URL");
|
||||
if (!base_uri || !*base_uri) {
|
||||
fprintf(stderr, "rb: no URI provided and REMARKBOX_URL not set\n");
|
||||
return 1;
|
||||
}
|
||||
cmd_start = 1;
|
||||
}
|
||||
|
||||
if (cmd_start >= argc) { usage(); return 1; }
|
||||
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
|
||||
rb_client_t *c = rb_client_new(base_uri, NULL);
|
||||
const char *cmd = argv[cmd_start];
|
||||
int nargs = argc - cmd_start - 1;
|
||||
char **args = argv + cmd_start + 1;
|
||||
int rc = 1;
|
||||
|
||||
if (strcmp(cmd, "version") == 0) {
|
||||
rc = run(rb_version(c));
|
||||
} else if (strcmp(cmd, "threads") == 0 && nargs >= 1) {
|
||||
rc = run(rb_list_threads(c, args[0]));
|
||||
} else if (strcmp(cmd, "thread") == 0 && nargs >= 1) {
|
||||
rc = run(rb_get_thread(c, args[0]));
|
||||
} else if (strcmp(cmd, "search") == 0 && nargs >= 2) {
|
||||
rc = run(rb_search_threads(c, args[0], args[1]));
|
||||
} else if (strcmp(cmd, "node") == 0 && nargs >= 1) {
|
||||
rc = run(rb_get_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "post") == 0 && nargs >= 3) {
|
||||
rc = run(rb_create_thread(c, args[0], args[1], args[2],
|
||||
nargs >= 4 ? args[3] : NULL));
|
||||
} else if (strcmp(cmd, "reply") == 0 && nargs >= 2) {
|
||||
rc = run(rb_reply(c, args[0], args[1], nargs >= 3 ? args[2] : NULL));
|
||||
} else if (strcmp(cmd, "edit") == 0 && nargs >= 2) {
|
||||
rc = run(rb_edit_node(c, args[0], args[1]));
|
||||
} else if (strcmp(cmd, "disable") == 0 && nargs >= 1) {
|
||||
rc = run(rb_disable_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "enable") == 0 && nargs >= 1) {
|
||||
rc = run(rb_enable_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "approve") == 0 && nargs >= 1) {
|
||||
rc = run(rb_approve_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "lock") == 0 && nargs >= 1) {
|
||||
rc = run(rb_lock_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "unlock") == 0 && nargs >= 1) {
|
||||
rc = run(rb_unlock_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "delete") == 0 && nargs >= 1) {
|
||||
rc = run(rb_delete_node(c, args[0]));
|
||||
} else if (strcmp(cmd, "login") == 0 && nargs >= 1) {
|
||||
rc = run(rb_login(c, args[0]));
|
||||
} else if (strcmp(cmd, "verify") == 0 && nargs >= 2) {
|
||||
rc = run(rb_verify(c, args[0], args[1]));
|
||||
} else if (strcmp(cmd, "profile") == 0) {
|
||||
rc = run(rb_get_profile(c));
|
||||
} else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 ||
|
||||
strcmp(cmd, "-h") == 0) {
|
||||
usage(); rc = 0;
|
||||
} else {
|
||||
fprintf(stderr, "rb: unknown command '%s'\n\n", cmd);
|
||||
usage(); rc = 1;
|
||||
}
|
||||
|
||||
rb_client_free(c);
|
||||
curl_global_cleanup();
|
||||
return rc;
|
||||
}
|
||||
485
remarkbox/api/rb.h
Normal file
485
remarkbox/api/rb.h
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
/*
|
||||
* rb.h — Remarkbox C library (single-header, libcurl only)
|
||||
*
|
||||
* Download:
|
||||
* curl -s https://REMARKBOX/api/v1/clients/c?file=rb.h -o rb.h
|
||||
*
|
||||
* Usage as a library:
|
||||
* // In exactly ONE .c file, before including:
|
||||
* #define RB_IMPLEMENTATION
|
||||
* #include "rb.h"
|
||||
*
|
||||
* // In all other .c files, just:
|
||||
* #include "rb.h"
|
||||
*
|
||||
* Quick start:
|
||||
* rb_client_t *c = rb_client_new("https://my.remarkbox.com", NULL);
|
||||
* rb_response_t r = rb_version(c);
|
||||
* if (r.ok) printf("%s\n", r.body);
|
||||
* rb_response_free(&r);
|
||||
* rb_client_free(c);
|
||||
*
|
||||
* Requires: libcurl (link with -lcurl)
|
||||
* License: Same as Remarkbox
|
||||
*/
|
||||
|
||||
#ifndef RB_H
|
||||
#define RB_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define RB_LIB_VERSION "0.1.0"
|
||||
#define RB_MAX_URI 2048
|
||||
#define RB_MAX_BODY 1048576 /* 1 MB response limit */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
char *base_uri; /* e.g. "https://my.remarkbox.com" */
|
||||
char *cookie_path; /* path to Netscape cookie jar */
|
||||
} rb_client_t;
|
||||
|
||||
typedef struct {
|
||||
int ok; /* 1 if HTTP 2xx, 0 otherwise */
|
||||
long status; /* HTTP status code */
|
||||
char *body; /* raw JSON response (caller must rb_response_free) */
|
||||
size_t body_len;
|
||||
} rb_response_t;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Client lifecycle
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Create a client. cookie_path may be NULL for default
|
||||
(~/.config/remarkbox/cookies.txt or REMARKBOX_COOKIES). */
|
||||
rb_client_t *rb_client_new(const char *base_uri, const char *cookie_path);
|
||||
|
||||
/* Free a client. */
|
||||
void rb_client_free(rb_client_t *c);
|
||||
|
||||
/* Free response body. */
|
||||
void rb_response_free(rb_response_t *r);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* API methods — each returns rb_response_t with raw JSON in body
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Version */
|
||||
rb_response_t rb_version(rb_client_t *c);
|
||||
|
||||
/* Threads */
|
||||
rb_response_t rb_list_threads(rb_client_t *c, const char *ns);
|
||||
rb_response_t rb_get_thread(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_search_threads(rb_client_t *c, const char *ns, const char *query);
|
||||
rb_response_t rb_create_thread(rb_client_t *c, const char *ns, const char *title,
|
||||
const char *data, const char *anon_name);
|
||||
|
||||
/* Replies */
|
||||
rb_response_t rb_reply(rb_client_t *c, const char *node_id, const char *data,
|
||||
const char *anon_name);
|
||||
|
||||
/* Nodes */
|
||||
rb_response_t rb_get_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_edit_node(rb_client_t *c, const char *node_id, const char *data);
|
||||
rb_response_t rb_disable_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_enable_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_approve_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_lock_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_unlock_node(rb_client_t *c, const char *node_id);
|
||||
rb_response_t rb_delete_node(rb_client_t *c, const char *node_id);
|
||||
|
||||
/* Auth */
|
||||
rb_response_t rb_login(rb_client_t *c, const char *email);
|
||||
rb_response_t rb_verify(rb_client_t *c, const char *email, const char *otp);
|
||||
|
||||
/* Profile */
|
||||
rb_response_t rb_get_profile(rb_client_t *c);
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Utility
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Pretty-print JSON to stdout. */
|
||||
void rb_json_pretty(const char *json);
|
||||
|
||||
/* Escape a string for use inside a JSON string value.
|
||||
Writes to out (must be at least out_size bytes). */
|
||||
void rb_json_escape(const char *s, char *out, size_t out_size);
|
||||
|
||||
|
||||
/* =========================================================================
|
||||
* IMPLEMENTATION — include this in exactly one .c file
|
||||
* ========================================================================= */
|
||||
|
||||
#ifdef RB_IMPLEMENTATION
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Internal: response buffer
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
struct rb__buf {
|
||||
char *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
};
|
||||
|
||||
static void rb__buf_init(struct rb__buf *b)
|
||||
{
|
||||
b->cap = 4096;
|
||||
b->data = (char *)malloc(b->cap);
|
||||
b->len = 0;
|
||||
if (b->data) b->data[0] = '\0';
|
||||
}
|
||||
|
||||
static size_t rb__write_cb(void *ptr, size_t size, size_t nmemb, void *userdata)
|
||||
{
|
||||
struct rb__buf *b = (struct rb__buf *)userdata;
|
||||
size_t bytes = size * nmemb;
|
||||
|
||||
if (b->len + bytes + 1 > RB_MAX_BODY)
|
||||
bytes = RB_MAX_BODY - b->len - 1;
|
||||
|
||||
if (b->len + bytes + 1 > b->cap) {
|
||||
size_t newcap = b->cap * 2;
|
||||
while (newcap < b->len + bytes + 1) newcap *= 2;
|
||||
if (newcap > RB_MAX_BODY) newcap = RB_MAX_BODY;
|
||||
char *tmp = (char *)realloc(b->data, newcap);
|
||||
if (!tmp) return 0;
|
||||
b->data = tmp;
|
||||
b->cap = newcap;
|
||||
}
|
||||
|
||||
memcpy(b->data + b->len, ptr, bytes);
|
||||
b->len += bytes;
|
||||
b->data[b->len] = '\0';
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Internal: default cookie path
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static const char *rb__default_cookie_path(void)
|
||||
{
|
||||
const char *env = getenv("REMARKBOX_COOKIES");
|
||||
if (env && *env) return env;
|
||||
|
||||
static char path[1024];
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = ".";
|
||||
snprintf(path, sizeof(path), "%s/.config/remarkbox/cookies.txt", home);
|
||||
return path;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Client lifecycle
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
rb_client_t *rb_client_new(const char *base_uri, const char *cookie_path)
|
||||
{
|
||||
rb_client_t *c = (rb_client_t *)calloc(1, sizeof(rb_client_t));
|
||||
if (!c) return NULL;
|
||||
|
||||
/* Strip trailing slash */
|
||||
size_t len = strlen(base_uri);
|
||||
while (len > 0 && base_uri[len - 1] == '/') len--;
|
||||
c->base_uri = (char *)malloc(len + 1);
|
||||
if (c->base_uri) { memcpy(c->base_uri, base_uri, len); c->base_uri[len] = '\0'; }
|
||||
|
||||
const char *cp = cookie_path ? cookie_path : rb__default_cookie_path();
|
||||
c->cookie_path = (char *)malloc(strlen(cp) + 1);
|
||||
if (c->cookie_path) strcpy(c->cookie_path, cp);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void rb_client_free(rb_client_t *c)
|
||||
{
|
||||
if (!c) return;
|
||||
free(c->base_uri);
|
||||
free(c->cookie_path);
|
||||
free(c);
|
||||
}
|
||||
|
||||
void rb_response_free(rb_response_t *r)
|
||||
{
|
||||
free(r->body);
|
||||
r->body = NULL;
|
||||
r->body_len = 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Internal: HTTP request
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static rb_response_t rb__request(rb_client_t *c, const char *method,
|
||||
const char *api_path, const char *json_body,
|
||||
const char *extra_header)
|
||||
{
|
||||
rb_response_t result = {0, 0, NULL, 0};
|
||||
char uri[RB_MAX_URI];
|
||||
struct rb__buf resp;
|
||||
|
||||
snprintf(uri, sizeof(uri), "%s%s", c->base_uri, api_path);
|
||||
rb__buf_init(&resp);
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
result.body = (char *)malloc(32);
|
||||
if (result.body) { strcpy(result.body, "{\"error\":\"curl init failed\"}"); result.body_len = strlen(result.body); }
|
||||
return result;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, uri);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, rb__write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, c->cookie_path);
|
||||
curl_easy_setopt(curl, CURLOPT_COOKIEJAR, c->cookie_path);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "rb/" RB_LIB_VERSION);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/json");
|
||||
if (json_body) {
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
||||
}
|
||||
if (extra_header)
|
||||
headers = curl_slist_append(headers, extra_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(resp.data);
|
||||
result.body = (char *)malloc(128);
|
||||
if (result.body) {
|
||||
snprintf(result.body, 128, "{\"error\":\"%s\"}", curl_easy_strerror(res));
|
||||
result.body_len = strlen(result.body);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ok = (result.status >= 200 && result.status < 400);
|
||||
result.body = resp.data; /* caller owns this now */
|
||||
result.body_len = resp.len;
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Utility
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
void rb_json_escape(const char *s, char *out, size_t out_size)
|
||||
{
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; s[i] && j + 6 < out_size; i++) {
|
||||
switch (s[i]) {
|
||||
case '"': out[j++] = '\\'; out[j++] = '"'; break;
|
||||
case '\\': out[j++] = '\\'; out[j++] = '\\'; break;
|
||||
case '\n': out[j++] = '\\'; out[j++] = 'n'; break;
|
||||
case '\r': out[j++] = '\\'; out[j++] = 'r'; break;
|
||||
case '\t': out[j++] = '\\'; out[j++] = 't'; break;
|
||||
default:
|
||||
if ((unsigned char)s[i] < 0x20)
|
||||
j += snprintf(out + j, out_size - j, "\\u%04x", (unsigned char)s[i]);
|
||||
else
|
||||
out[j++] = s[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
out[j] = '\0';
|
||||
}
|
||||
|
||||
void rb_json_pretty(const char *s)
|
||||
{
|
||||
int indent = 0, in_string = 0, escaped = 0;
|
||||
for (; *s; s++) {
|
||||
if (escaped) { putchar(*s); escaped = 0; continue; }
|
||||
if (*s == '\\' && in_string) { putchar(*s); escaped = 1; continue; }
|
||||
if (*s == '"') { in_string = !in_string; putchar(*s); continue; }
|
||||
if (in_string) { putchar(*s); continue; }
|
||||
switch (*s) {
|
||||
case '{': case '[':
|
||||
putchar(*s); putchar('\n'); indent += 2;
|
||||
for (int i = 0; i < indent; i++) putchar(' ');
|
||||
break;
|
||||
case '}': case ']':
|
||||
putchar('\n'); indent -= 2; if (indent < 0) indent = 0;
|
||||
for (int i = 0; i < indent; i++) putchar(' ');
|
||||
putchar(*s); break;
|
||||
case ',':
|
||||
putchar(*s); putchar('\n');
|
||||
for (int i = 0; i < indent; i++) putchar(' ');
|
||||
break;
|
||||
case ':': putchar(*s); putchar(' '); break;
|
||||
default:
|
||||
if (*s != ' ' && *s != '\t' && *s != '\n' && *s != '\r')
|
||||
putchar(*s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Internal: URI encoding helper
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static void rb__uri_encode(const char *s, char *out, size_t out_size)
|
||||
{
|
||||
CURL *curl = curl_easy_init();
|
||||
if (curl) {
|
||||
char *enc = curl_easy_escape(curl, s, 0);
|
||||
if (enc) { snprintf(out, out_size, "%s", enc); curl_free(enc); }
|
||||
else { snprintf(out, out_size, "%s", s); }
|
||||
curl_easy_cleanup(curl);
|
||||
} else {
|
||||
snprintf(out, out_size, "%s", s);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* API methods
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
rb_response_t rb_version(rb_client_t *c)
|
||||
{
|
||||
return rb__request(c, "GET", "/api/v1/version", NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_list_threads(rb_client_t *c, const char *ns)
|
||||
{
|
||||
char path[RB_MAX_URI], enc[1024];
|
||||
rb__uri_encode(ns, enc, sizeof(enc));
|
||||
snprintf(path, sizeof(path), "/api/v1/threads?namespace=%s", enc);
|
||||
return rb__request(c, "GET", path, NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_get_thread(rb_client_t *c, const char *node_id)
|
||||
{
|
||||
char path[RB_MAX_URI];
|
||||
snprintf(path, sizeof(path), "/api/v1/threads/%s", node_id);
|
||||
return rb__request(c, "GET", path, NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_search_threads(rb_client_t *c, const char *ns, const char *query)
|
||||
{
|
||||
char path[RB_MAX_URI], enc_ns[512], enc_q[512];
|
||||
rb__uri_encode(ns, enc_ns, sizeof(enc_ns));
|
||||
rb__uri_encode(query, enc_q, sizeof(enc_q));
|
||||
snprintf(path, sizeof(path), "/api/v1/threads/search?namespace=%s&q=%s",
|
||||
enc_ns, enc_q);
|
||||
return rb__request(c, "GET", path, NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_create_thread(rb_client_t *c, const char *ns, const char *title,
|
||||
const char *data, const char *anon_name)
|
||||
{
|
||||
char body[RB_MAX_BODY];
|
||||
char ens[2048], etitle[2048], edata[524288], ename[512];
|
||||
rb_json_escape(ns, ens, sizeof(ens));
|
||||
rb_json_escape(title, etitle, sizeof(etitle));
|
||||
rb_json_escape(data, edata, sizeof(edata));
|
||||
if (anon_name) {
|
||||
rb_json_escape(anon_name, ename, sizeof(ename));
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"namespace\":\"%s\",\"title\":\"%s\",\"data\":\"%s\","
|
||||
"\"anonymous_name\":\"%s\"}", ens, etitle, edata, ename);
|
||||
} else {
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"namespace\":\"%s\",\"title\":\"%s\",\"data\":\"%s\"}",
|
||||
ens, etitle, edata);
|
||||
}
|
||||
return rb__request(c, "POST", "/api/v1/threads", body, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_reply(rb_client_t *c, const char *node_id, const char *data,
|
||||
const char *anon_name)
|
||||
{
|
||||
char path[RB_MAX_URI], body[RB_MAX_BODY];
|
||||
char edata[524288], ename[512];
|
||||
rb_json_escape(data, edata, sizeof(edata));
|
||||
snprintf(path, sizeof(path), "/api/v1/threads/%s/replies", node_id);
|
||||
if (anon_name) {
|
||||
rb_json_escape(anon_name, ename, sizeof(ename));
|
||||
snprintf(body, sizeof(body),
|
||||
"{\"data\":\"%s\",\"anonymous_name\":\"%s\"}", edata, ename);
|
||||
} else {
|
||||
snprintf(body, sizeof(body), "{\"data\":\"%s\"}", edata);
|
||||
}
|
||||
return rb__request(c, "POST", path, body, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_get_node(rb_client_t *c, const char *node_id)
|
||||
{
|
||||
char path[RB_MAX_URI];
|
||||
snprintf(path, sizeof(path), "/api/v1/nodes/%s", node_id);
|
||||
return rb__request(c, "GET", path, NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_edit_node(rb_client_t *c, const char *node_id, const char *data)
|
||||
{
|
||||
char path[RB_MAX_URI], body[RB_MAX_BODY], edata[524288];
|
||||
rb_json_escape(data, edata, sizeof(edata));
|
||||
snprintf(path, sizeof(path), "/api/v1/nodes/%s", node_id);
|
||||
snprintf(body, sizeof(body), "{\"data\":\"%s\"}", edata);
|
||||
return rb__request(c, "PATCH", path, body, NULL);
|
||||
}
|
||||
|
||||
static rb_response_t rb__mod_flag(rb_client_t *c, const char *node_id,
|
||||
const char *field, const char *value)
|
||||
{
|
||||
char path[RB_MAX_URI], body[256];
|
||||
snprintf(path, sizeof(path), "/api/v1/nodes/%s", node_id);
|
||||
snprintf(body, sizeof(body), "{\"%s\":%s}", field, value);
|
||||
return rb__request(c, "PATCH", path, body, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_disable_node(rb_client_t *c, const char *id) { return rb__mod_flag(c, id, "disabled", "true"); }
|
||||
rb_response_t rb_enable_node(rb_client_t *c, const char *id) { return rb__mod_flag(c, id, "disabled", "false"); }
|
||||
rb_response_t rb_approve_node(rb_client_t *c, const char *id) { return rb__mod_flag(c, id, "approved", "true"); }
|
||||
rb_response_t rb_lock_node(rb_client_t *c, const char *id) { return rb__mod_flag(c, id, "locked", "true"); }
|
||||
rb_response_t rb_unlock_node(rb_client_t *c, const char *id) { return rb__mod_flag(c, id, "locked", "false"); }
|
||||
|
||||
rb_response_t rb_delete_node(rb_client_t *c, const char *node_id)
|
||||
{
|
||||
char path[RB_MAX_URI];
|
||||
snprintf(path, sizeof(path), "/api/v1/nodes/%s", node_id);
|
||||
return rb__request(c, "DELETE", path, NULL, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_login(rb_client_t *c, const char *email)
|
||||
{
|
||||
char body[512], eemail[256];
|
||||
rb_json_escape(email, eemail, sizeof(eemail));
|
||||
snprintf(body, sizeof(body), "{\"email\":\"%s\"}", eemail);
|
||||
return rb__request(c, "POST", "/api/v1/auth/login", body, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_verify(rb_client_t *c, const char *email, const char *otp)
|
||||
{
|
||||
char body[512], eemail[256], eotp[64];
|
||||
rb_json_escape(email, eemail, sizeof(eemail));
|
||||
rb_json_escape(otp, eotp, sizeof(eotp));
|
||||
snprintf(body, sizeof(body), "{\"email\":\"%s\",\"otp\":\"%s\"}", eemail, eotp);
|
||||
return rb__request(c, "POST", "/api/v1/auth/verify", body, NULL);
|
||||
}
|
||||
|
||||
rb_response_t rb_get_profile(rb_client_t *c)
|
||||
{
|
||||
return rb__request(c, "GET", "/api/v1/user/profile", NULL, NULL);
|
||||
}
|
||||
|
||||
#endif /* RB_IMPLEMENTATION */
|
||||
#endif /* RB_H */
|
||||
|
|
@ -1094,6 +1094,29 @@ def api_client_python(request):
|
|||
)
|
||||
|
||||
|
||||
@view_config(
|
||||
route_name="api-client-c",
|
||||
request_method="GET",
|
||||
require_csrf=False,
|
||||
)
|
||||
def api_client_c(request):
|
||||
"""Serve the C client for download.
|
||||
|
||||
?file=rb.h returns the header library instead of the CLI.
|
||||
"""
|
||||
filename = request.params.get("file", "rb.c")
|
||||
if filename not in ("rb.c", "rb.h"):
|
||||
filename = "rb.c"
|
||||
path = os.path.join(_client_dir, filename)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
return Response(
|
||||
body=content,
|
||||
content_type="text/plain",
|
||||
charset="utf-8",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Admin (superuser only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -275,6 +275,14 @@ Embed this snippet into your web page to enable comments:
|
|||
</ul>
|
||||
|
||||
|
||||
<label>API Clients</label>
|
||||
Programmatic access to this Namespace's threads and comments:
|
||||
<ul>
|
||||
<li><a href="/api/v1/clients/python" target="_blank">Python client</a> — <code>remarkbox_client.py</code> (stdlib only, no pip)</li>
|
||||
<li><a href="/api/v1/clients/c" target="_blank">C client</a> — <code>rb.c</code> + <code><a href="/api/v1/clients/c?file=rb.h" target="_blank">rb.h</a></code> (libcurl only, <code>gcc rb.c -o rb -lcurl</code>)</li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<label>Statistics</a></label>
|
||||
This <a href="/ns/{{ request.namespace.name }}/stats.json" target="_blank">stats.json</a> endpoint
|
||||
returns various statistics about the threads in this Namespace.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue