diff --git a/httpd.h b/httpd.h index 4b3f5952..021c023c 100644 --- a/httpd.h +++ b/httpd.h @@ -43,6 +43,7 @@ struct form_buf_malloc { }; struct httpd_request; +struct meshmb_session; int form_buf_malloc_init(struct form_buf_malloc *, size_t size_limit); int form_buf_malloc_accumulate(struct httpd_request *, const char *partname, struct form_buf_malloc *, const char *, size_t); @@ -219,6 +220,8 @@ typedef struct httpd_request struct { rhizome_bid_t bundle_id; + struct meshmb_session *session; + uint8_t generation; enum list_phase phase; size_t rowcount; } meshmb_feeds; diff --git a/meshmb.c b/meshmb.c index cf9330e6..e293f500 100644 --- a/meshmb.c +++ b/meshmb.c @@ -27,6 +27,7 @@ struct meshmb_feeds{ keyring_identity *id; sign_keypair_t bundle_keypair; bool_t dirty; + uint8_t generation; }; // only remember this many bytes of ply names & last messages @@ -175,7 +176,7 @@ int meshmb_flush(struct meshmb_feeds *feeds) { if (!feeds->dirty){ DEBUGF(meshmb, "Ignoring flush, not dirty"); - return 0; + return feeds->generation; } rhizome_manifest *mout = NULL; @@ -210,14 +211,14 @@ int meshmb_flush(struct meshmb_feeds *feeds) rhizome_manifest_set_filesize(m, write.file_length); struct rhizome_bundle_result result = rhizome_manifest_finalise(m, &mout, 1); if (result.status == RHIZOME_BUNDLE_STATUS_NEW){ - ret = 0; + ret = ++feeds->generation; feeds->dirty = 0; } rhizome_bundle_result_free(&result); } } } - if (ret!=0) + if (ret==-1) rhizome_fail_write(&write); break; } diff --git a/meshmb.h b/meshmb.h index a9888a4f..48bc6a63 100644 --- a/meshmb.h +++ b/meshmb.h @@ -25,6 +25,7 @@ int meshmb_open(keyring_identity *id, struct meshmb_feeds **feeds); void meshmb_close(struct meshmb_feeds *feeds); // re-write metadata if required +// returns -1 on failure, or a generation number that is incremented only if something has changed. int meshmb_flush(struct meshmb_feeds *feeds); // set / clear follow flag for this feed diff --git a/meshmb_restful.c b/meshmb_restful.c index a46652c3..c4a38cf3 100644 --- a/meshmb_restful.c +++ b/meshmb_restful.c @@ -494,6 +494,131 @@ static int restful_meshmb_ignore(httpd_request *r, const char *remainder) return ret; } +struct enum_state{ + httpd_request *request; + strbuf buffer; +}; + +static int restful_feedlist_enum(struct meshmb_feed_details *details, void *context){ + struct enum_state *state = context; + size_t checkpoint = strbuf_len(state->buffer); + + if (state->request->u.meshmb_feeds.rowcount!=0) + strbuf_putc(state->buffer, ','); + strbuf_puts(state->buffer, "\n["); + strbuf_json_hex(state->buffer, details->bundle_id.binary, sizeof details->bundle_id.binary); + strbuf_puts(state->buffer, ","); + strbuf_json_string(state->buffer, details->name); + strbuf_puts(state->buffer, ","); + strbuf_sprintf(state->buffer, "%d", details->timestamp); + strbuf_puts(state->buffer, ","); + strbuf_json_string(state->buffer, details->last_message); + strbuf_puts(state->buffer, "]"); + + if (strbuf_overrun(state->buffer)){ + strbuf_trunc(state->buffer, checkpoint); + return 1; + }else{ + ++state->request->u.meshmb_feeds.rowcount; + state->request->u.meshmb_feeds.bundle_id = details->bundle_id; + return 0; + } +} + +static int restful_meshmb_feedlist_json_content_chunk(struct http_request *hr, strbuf b) +{ + httpd_request *r = (httpd_request *) hr; + const char *headers[] = { + "id", + "name", + "timestamp", + "last_message" + }; + + DEBUGF(httpd, "Phase %d", r->u.meshmb_feeds.phase); + + switch (r->u.meshmb_feeds.phase) { + case LIST_HEADER: + strbuf_puts(b, "{\n\"header\":["); + unsigned i; + for (i = 0; i != NELS(headers); ++i) { + if (i) + strbuf_putc(b, ','); + strbuf_json_string(b, headers[i]); + } + strbuf_puts(b, "],\n\"rows\":["); + if (!strbuf_overrun(b)) + r->u.meshmb_feeds.phase = LIST_ROWS; + return 1; + + case LIST_ROWS: + case LIST_FIRST: + { + struct enum_state state={ + .request = r, + .buffer = b + }; + if (meshmb_enum(r->u.meshmb_feeds.session->feeds, &r->u.meshmb_feeds.bundle_id, restful_feedlist_enum, &state)!=0) + return 0; + } + // fallthrough + r->u.meshmb_feeds.phase = LIST_END; + case LIST_END: + strbuf_puts(b, "\n]\n}\n"); + if (!strbuf_overrun(b)) + r->u.plylist.phase = LIST_DONE; + + // fall through... + case LIST_DONE: + return 0; + } +} + +static int restful_meshmb_feedlist_json_content(struct http_request *hr, unsigned char *buf, size_t bufsz, struct http_content_generator_result *result) +{ + return generate_http_content_from_strbuf_chunks(hr, (char *)buf, bufsz, result, restful_meshmb_feedlist_json_content_chunk); +} + +static void feedlist_on_rhizome_add(httpd_request *r, rhizome_manifest *m) +{ + struct message_ply_read reader; + bzero(&reader, sizeof(reader)); + meshmb_bundle_update(r->u.meshmb_feeds.session->feeds, m, &reader); + message_ply_read_close(&reader); + + int gen = meshmb_flush(r->u.meshmb_feeds.session->feeds); + if (gen>=0 && gen != r->u.meshmb_feeds.generation) + http_request_resume_response(&r->http); +} + +static void feedlist_finalise(httpd_request *r) +{ + close_session(r->u.meshmb_feeds.session); +} + +static int restful_meshmb_feedlist(httpd_request *r, const char *remainder) +{ + if (*remainder) + return 404; + + struct meshmb_session *session = open_session(&r->bid); + if (!session){ + http_request_simple_response(&r->http, 500, "TODO, detailed response"); + return 500; + } + + assert(r->finalise_union == NULL); + r->finalise_union = feedlist_finalise; + r->trigger_rhizome_bundle_added = feedlist_on_rhizome_add; + r->u.meshmb_feeds.phase = LIST_HEADER; + r->u.meshmb_feeds.session = session; + r->u.meshmb_feeds.generation = meshmb_flush(session->feeds); + bzero(&r->u.meshmb_feeds.bundle_id, sizeof r->u.meshmb_feeds.bundle_id); + + http_request_response_generated(&r->http, 200, CONTENT_TYPE_JSON, restful_meshmb_feedlist_json_content); + return 1; +} + DECLARE_HANDLER("/restful/meshmb/", restful_meshmb_); static int restful_meshmb_(httpd_request *r, const char *remainder) { @@ -518,6 +643,10 @@ static int restful_meshmb_(httpd_request *r, const char *remainder) handler = restful_meshmb_list; remainder = ""; r->ui64 = 0; + } else if (strcmp(remainder, "/feedlist.json") == 0) { + handler = restful_meshmb_feedlist; + remainder = ""; + r->ui64 = 0; } else if ( str_startswith(remainder, "/newsince/", &end) && strn_to_position_token(end, &r->ui64, &end) && strcmp(end, "messagelist.json") == 0) { diff --git a/tests/meshmbrestful b/tests/meshmbrestful index 03dc7d0e..c5f939be 100755 --- a/tests/meshmbrestful +++ b/tests/meshmbrestful @@ -137,5 +137,42 @@ test_MeshMBRestFollow() { assertStdoutGrep --matches=1 ":$IDA3::[0-9]\+:Message 3\$" } +doc_MeshMBRestFeeds="Restful list subscribed feeds" +setup_MeshMBRestFeeds() { + IDENTITY_COUNT=3 + setup + executeOk_servald meshmb send $IDA1 "Message 1" + executeOk_servald meshmb send $IDA2 "Message 2" + executeOk_servald meshmb send $IDA3 "Message 3" + executeOk_servald meshmb follow $IDA1 $IDA2 + executeOk_servald meshmb follow $IDA1 $IDA3 +} +test_MeshMBRestFeeds() { + executeOk curl \ + -H "Expect:" \ + --silent --fail --show-error \ + --output feedlist.json \ + --dump-header http.headers \ + --basic --user harry:potter \ + "http://$addr_localhost:$PORTA/restful/meshmb/$IDA1/feedlist.json" + tfw_cat http.headers feedlist.json + tfw_preserve feedlist.json + assert [ "$(jq '.rows | length' feedlist.json)" = 2 ] + transform_list_json feedlist.json list.json + tfw_preserve list.json + assertJq list.json \ + "contains([ + { id: \"$IDA2\", + last_message: \"Message 2\" + } + ])" + assertJq list.json \ + "contains([ + { id: \"$IDA3\", + last_message: \"Message 3\" + } + ])" +} + runTests "$@"