Add API's for marking a feed as blocked

This commit is contained in:
Jeremy Lakeman 2017-06-05 12:39:07 +09:30
parent f83b15d251
commit 508e95436a
11 changed files with 192 additions and 116 deletions

View File

@ -85,6 +85,10 @@ public abstract class AbstractId {
this.binary = binary;
}
protected boolean isEquivalent(Object other){
return other.getClass() == this.getClass();
}
@Override
public boolean equals(Object other) {
// must be the exact same type with the same binary contents to be considered equal
@ -92,7 +96,7 @@ public abstract class AbstractId {
return false;
if (other==this)
return true;
if (other.getClass() == this.getClass()) {
if (isEquivalent(other)) {
AbstractId oBinary = (AbstractId) other;
for (int i = 0; i < this.binary.length; i++)
if (this.binary[i] != oBinary.binary[i])

View File

@ -220,12 +220,8 @@ public class ServalDClient implements ServalDHttpConnectionFactory {
return list;
}
public int meshmbFollow(Subscriber id, SigningKey peer) throws ServalDInterfaceException, IOException {
return MeshMBCommon.follow(this, id, peer);
}
public int meshmbIgnore(Subscriber id, SigningKey peer) throws ServalDInterfaceException, IOException {
return MeshMBCommon.ignore(this, id, peer);
public int meshmbAlterSubscription(Subscriber id, MeshMBCommon.SubscriptionAction action, SigningKey peer) throws ServalDInterfaceException, IOException {
return MeshMBCommon.alterSubscription(this, id, action, peer);
}
public MeshMBSubscriptionList meshmbSubscriptions(Subscriber identity) throws IOException, ServalDInterfaceException {

View File

@ -29,4 +29,9 @@ public class SigningKey extends AbstractId {
public String getMimeType() {
return "serval-mesh/id";
}
@Override
protected boolean isEquivalent(Object other){
return SigningKey.class.isInstance(other);
}
}

View File

@ -17,6 +17,12 @@ public class MeshMBCommon {
public static final String SERVICE = "MeshMB1";
public enum SubscriptionAction{
Follow,
Ignore,
Block
}
public static int sendMessage(ServalDHttpConnectionFactory connector, SigningKey id, String text) throws IOException, ServalDInterfaceException {
HttpURLConnection conn = connector.newServalDHttpConnection("/restful/meshmb/" + id.toHex() + "/sendmessage");
PostHelper helper = new PostHelper(conn);
@ -28,15 +34,8 @@ public class MeshMBCommon {
return conn.getResponseCode();
}
public static int ignore(ServalDHttpConnectionFactory connector, Subscriber id, SigningKey peer) throws ServalDInterfaceException, IOException {
HttpURLConnection conn = connector.newServalDHttpConnection("/restful/meshmb/" + id.signingKey.toHex() + "/ignore/" + peer.toHex());
conn.setRequestMethod("POST");
conn.connect();
return conn.getResponseCode();
}
public static int follow(ServalDHttpConnectionFactory connector, Subscriber id, SigningKey peer) throws ServalDInterfaceException, IOException {
HttpURLConnection conn = connector.newServalDHttpConnection("/restful/meshmb/" + id.signingKey.toHex() + "/follow/" + peer.toHex());
public static int alterSubscription(ServalDHttpConnectionFactory connector, Subscriber id, SubscriptionAction action, SigningKey peer) throws ServalDInterfaceException, IOException {
HttpURLConnection conn = connector.newServalDHttpConnection("/restful/meshmb/" + id.signingKey.toHex() + "/"+action.toString().toLowerCase()+"/" + peer.toHex());
conn.setRequestMethod("POST");
conn.connect();
return conn.getResponseCode();

View File

@ -1,6 +1,7 @@
package org.servalproject.servaldna.meshmb;
import org.servalproject.json.JSONTableScanner;
import org.servalproject.json.JSONTokeniser;
import org.servalproject.servaldna.AbstractJsonList;
import org.servalproject.servaldna.ServalDHttpConnectionFactory;
import org.servalproject.servaldna.ServalDInterfaceException;
@ -23,9 +24,10 @@ public class MeshMBSubscriptionList extends AbstractJsonList<MeshMBSubscription,
super(httpConnector, new JSONTableScanner()
.addColumn("id", SigningKey.class)
.addColumn("author", SubscriberId.class)
.addColumn("name", String.class)
.addColumn("blocked", Boolean.class)
.addColumn("name", String.class, JSONTokeniser.Narrow.ALLOW_NULL)
.addColumn("timestamp", Long.class)
.addColumn("last_message", String.class)
.addColumn("last_message", String.class, JSONTokeniser.Narrow.ALLOW_NULL)
);
this.identity = identity;
}
@ -40,7 +42,7 @@ public class MeshMBSubscriptionList extends AbstractJsonList<MeshMBSubscription,
new Subscriber((SubscriberId)row.get("author"),
(SigningKey) row.get("id"),
true),
false,
(Boolean) row.get("blocked"),
(String) row.get("name"),
(Long) row.get("timestamp"),
(String) row.get("last_message")

View File

@ -22,6 +22,8 @@ struct feed_metadata{
uint64_t size;
};
#define FLAG_BLOCKED (1<<0)
struct meshmb_feeds{
struct tree_root root;
keyring_identity *id;
@ -29,8 +31,8 @@ struct meshmb_feeds{
sign_keypair_t ack_bundle_keypair;
rhizome_manifest *ack_manifest;
struct rhizome_write ack_writer;
bool_t dirty;
uint8_t generation;
bool_t dirty:1;
};
struct meshmb_activity_iterator *meshmb_activity_open(struct meshmb_feeds *feeds){
@ -74,6 +76,8 @@ static int activity_next_ack(struct meshmb_activity_iterator *i){
}else{
struct feed_metadata *metadata;
if (tree_find(&i->feeds->root, (void**)&metadata, ack.binary, ack.binary_length, NULL, NULL)==TREE_FOUND){
if (metadata->details.blocked)
continue;
bundle_id = &metadata->bundle_id;
i->metadata = metadata;
}else{
@ -240,6 +244,8 @@ static int activity_ack(struct meshmb_feeds *feeds, struct message_ply_ack *ack)
static int update_stats(struct meshmb_feeds *feeds, struct feed_metadata *metadata, struct message_ply_read *reader)
{
if (metadata->details.blocked)
return 0;
if (!metadata->details.ply.found){
// get the current size from the db
sqlite_retry_state retry = SQLITE_RETRY_STATE_DEFAULT;
@ -329,7 +335,7 @@ static int update_stats(struct meshmb_feeds *feeds, struct feed_metadata *metada
return 1;
}
// TODO, might be quicker to fetch all meshmb bundles and test if they are in the feed list
// TODO, might sometimes be quicker to fetch all meshmb bundles and test if they are in the feed list
static int update_stats_tree(void **record, void *context)
{
struct feed_metadata *metadata = (struct feed_metadata *)*record;
@ -346,7 +352,8 @@ int meshmb_bundle_update(struct meshmb_feeds *feeds, rhizome_manifest *m, struct
{
struct feed_metadata *metadata;
if (strcmp(m->service, RHIZOME_SERVICE_MESHMB) == 0
&& tree_find(&feeds->root, (void**)&metadata, m->keypair.public_key.binary, sizeof m->keypair.public_key.binary, NULL, NULL)==TREE_FOUND){
&& tree_find(&feeds->root, (void**)&metadata, m->keypair.public_key.binary, sizeof m->keypair.public_key.binary, NULL, NULL)==TREE_FOUND
&& !metadata->details.blocked){
metadata->details.ply.found = 1;
if (metadata->details.ply.size != m->filesize){
@ -384,7 +391,9 @@ static int write_metadata(void **record, void *context)
len += sizeof (rhizome_bid_t);
bcopy(metadata->details.ply.author.binary, &buffer[len], sizeof (sid_t));
len += sizeof (sid_t);
buffer[len++]=0;// flags?
uint8_t flags = (metadata->details.blocked ? FLAG_BLOCKED : 0);
buffer[len++]=flags;
if (!metadata->details.blocked){
len+=pack_uint(&buffer[len], metadata->size);
len+=pack_uint(&buffer[len], metadata->size - metadata->last_message_offset);
len+=pack_uint(&buffer[len], metadata->size - metadata->last_seen);
@ -399,6 +408,7 @@ static int write_metadata(void **record, void *context)
else
buffer[len]=0;
len+=msg_len;
}
assert(len < sizeof buffer);
DEBUGF(meshmb, "Write %zu bytes of metadata for %s/%s",
len,
@ -525,10 +535,12 @@ static int read_metadata(struct meshmb_feeds *feeds, struct rhizome_read *read)
break;
uint64_t delta=0;
uint64_t size;
uint64_t last_message_offset;
uint64_t last_seen;
uint64_t timestamp;
uint64_t size=0;
uint64_t last_message_offset=0;
uint64_t last_seen=0;
uint64_t timestamp=0;
const char *name=NULL;
const char *msg=NULL;
int unpacked;
const rhizome_bid_t *bid = (const rhizome_bid_t *)&buffer[0];
@ -541,8 +553,9 @@ static int read_metadata(struct meshmb_feeds *feeds, struct rhizome_read *read)
if (offset >= (unsigned)bytes)
goto error;
//uint8_t flags = buffer[offset++];
offset++;
uint8_t flags = buffer[offset++];
if (!(flags & FLAG_BLOCKED)){
if (offset >= (unsigned)bytes)
goto error;
@ -564,17 +577,18 @@ static int read_metadata(struct meshmb_feeds *feeds, struct rhizome_read *read)
goto error;
offset += unpacked;
const char *name = (const char *)&buffer[offset];
name = (const char *)&buffer[offset];
while(buffer[offset++]){
if (offset >= (unsigned)bytes)
goto error;
}
const char *msg = (const char *)&buffer[offset];
msg = (const char *)&buffer[offset];
while(buffer[offset++]){
if (offset >= (unsigned)bytes)
goto error;
}
}
DEBUGF(meshmb, "Seeking backwards %"PRIu64", %u, %zu", read->offset, offset, bytes);
read->offset = (read->offset - bytes) + offset;
@ -583,6 +597,7 @@ static int read_metadata(struct meshmb_feeds *feeds, struct rhizome_read *read)
if (tree_find(&feeds->root, (void**)&result, bid->binary, sizeof *bid, alloc_feed, feeds)<0)
return WHY("Failed to allocate metadata");
result->details.blocked = (flags & FLAG_BLOCKED) ? 1 : 0;
result->last_message_offset = last_message_offset;
result->last_seen = last_seen;
result->size = size;
@ -675,8 +690,6 @@ int meshmb_follow(struct meshmb_feeds *feeds, rhizome_bid_t *bid)
struct feed_metadata *metadata;
DEBUGF(meshmb, "Attempting to follow %s", alloca_tohex_rhizome_bid_t(*bid));
// TODO load the manifest and check the service!
if (tree_find(&feeds->root, (void**)&metadata, bid->binary, sizeof *bid, alloc_feed, feeds)!=TREE_FOUND)
return WHYF("Failed to follow feed");
@ -687,6 +700,30 @@ int meshmb_follow(struct meshmb_feeds *feeds, rhizome_bid_t *bid)
return 0;
}
int meshmb_block(struct meshmb_feeds *feeds, rhizome_bid_t *bid)
{
struct feed_metadata *metadata;
DEBUGF(meshmb, "Attempting to block %s", alloca_tohex_rhizome_bid_t(*bid));
if (tree_find(&feeds->root, (void**)&metadata, bid->binary, sizeof *bid, alloc_feed, feeds)!=TREE_FOUND)
return WHYF("Failed to block feed");
if (metadata->details.name){
free((void*)metadata->details.name);
metadata->details.name = NULL;
}
if (metadata->details.last_message){
free((void*)metadata->details.last_message);
metadata->details.last_message = NULL;
}
if (is_all_matching(metadata->details.ply.author.binary, sizeof metadata->details.ply.author, 0))
crypto_sign_to_sid(bid, &metadata->details.ply.author);
if (!metadata->details.blocked)
feeds->dirty = 1;
metadata->details.blocked = 1;
metadata->details.timestamp = 0;
return 0;
}
int meshmb_ignore(struct meshmb_feeds *feeds, rhizome_bid_t *bid)
{
DEBUGF(meshmb, "Attempting to ignore %s", alloca_tohex_rhizome_bid_t(*bid));

View File

@ -14,6 +14,7 @@ struct meshmb_feed_details{
const char *name;
const char *last_message;
time_s_t timestamp;
bool_t blocked:1;
};
// threaded feed iterator state
@ -41,6 +42,7 @@ int meshmb_flush(struct meshmb_feeds *feeds);
// set / clear follow flag for this feed
int meshmb_follow(struct meshmb_feeds *feeds, rhizome_bid_t *bid);
int meshmb_ignore(struct meshmb_feeds *feeds, rhizome_bid_t *bid);
int meshmb_block(struct meshmb_feeds *feeds, rhizome_bid_t *bid);
// enumerate feeds, starting from restart_from
typedef int (*meshmb_callback) (struct meshmb_feed_details *details, void *context);

View File

@ -212,8 +212,8 @@ static int app_meshmb_find(const struct cli_parsed *parsed, struct cli_context *
}
DEFINE_CMD(app_meshmb_follow, 0,
"Start or stop following a broadcast feed",
"meshmb", "follow|ignore" KEYRING_PIN_OPTIONS, "<id>", "<peer>");
"Follow, block or ignore a broadcast feed",
"meshmb", "follow|ignore|block" KEYRING_PIN_OPTIONS, "<id>", "<peer>");
static int app_meshmb_follow(const struct cli_parsed *parsed, struct cli_context *UNUSED(context))
{
const char *peerhex;
@ -221,6 +221,7 @@ static int app_meshmb_follow(const struct cli_parsed *parsed, struct cli_context
return -1;
int follow = cli_arg(parsed, "follow", NULL, NULL, NULL) == 0;
int block = cli_arg(parsed, "block", NULL, NULL, NULL) == 0;
identity_t peer;
if (str_to_identity_t(&peer, peerhex) == -1)
@ -230,7 +231,9 @@ static int app_meshmb_follow(const struct cli_parsed *parsed, struct cli_context
int ret = -1;
if (feeds){
if (follow){
if (block){
ret = meshmb_block(feeds, &peer);
}else if (follow){
ret = meshmb_follow(feeds, &peer);
}else{
ret = meshmb_ignore(feeds, &peer);
@ -263,6 +266,7 @@ static int list_callback(struct meshmb_feed_details *details, void *context)
cli_put_long(enum_context->context, enum_context->rowcount, ":");
cli_put_string(enum_context->context, alloca_tohex_rhizome_bid_t(details->ply.bundle_id), ":");
cli_put_string(enum_context->context, alloca_tohex_sid_t(details->ply.author), ":");
cli_put_string(enum_context->context, details->blocked ? "true" : "false", ":");
cli_put_string(enum_context->context, details->name, ":");
cli_put_long(enum_context->context, details->timestamp ? (long)(gettime() - details->timestamp) : (long)-1, ":");
cli_put_string(enum_context->context, details->last_message, "\n");
@ -270,7 +274,7 @@ static int list_callback(struct meshmb_feed_details *details, void *context)
}
DEFINE_CMD(app_meshmb_list, 0,
"List the feeds that you are currently following",
"List the feeds that you are currently following or blocking",
"meshmb", "list", "following" KEYRING_PIN_OPTIONS, "<id>");
static int app_meshmb_list(const struct cli_parsed *parsed, struct cli_context *context)
{
@ -282,6 +286,7 @@ static int app_meshmb_list(const struct cli_parsed *parsed, struct cli_context *
"_id",
"id",
"author",
"blocked",
"name",
"age",
"last_message"

View File

@ -20,6 +20,10 @@ struct meshmb_session{
struct meshmb_feeds *feeds;
};
#define FLAG_FOLLOW (1)
#define FLAG_IGNORE (2)
#define FLAG_BLOCK (3)
static struct meshmb_session *sessions = NULL;
static struct meshmb_session *open_session(const identity_t *identity){
@ -484,40 +488,25 @@ static int restful_meshmb_newsince_find(httpd_request *r, const char *remainder)
*/
static int restful_meshmb_follow(httpd_request *r, const char *remainder)
static int restful_meshmb_follow_ignore(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
assert(r->finalise_union == NULL);
struct meshmb_session *session = open_session(&r->bid);
int ret;
int ret=-1;
if (session
&& meshmb_follow(session->feeds, &r->u.meshmb_feeds.bundle_id)!=-1
&& meshmb_flush(session->feeds)!=-1){
http_request_simple_response(&r->http, 201, "TODO, detailed response");
ret = 201;
}else{
http_request_simple_response(&r->http, 500, "TODO, detailed response");
ret = 500;
if (session){
switch(r->ui64){
case FLAG_FOLLOW: ret = meshmb_follow(session->feeds, &r->u.meshmb_feeds.bundle_id); break;
case FLAG_IGNORE: ret = meshmb_ignore(session->feeds, &r->u.meshmb_feeds.bundle_id); break;
case FLAG_BLOCK: ret = meshmb_block (session->feeds, &r->u.meshmb_feeds.bundle_id); break;
default:
FATAL("Unexpected value");
}
if (session)
close_session(session);
return ret;
}
static int restful_meshmb_ignore(httpd_request *r, const char *remainder)
{
if (*remainder)
return 404;
assert(r->finalise_union == NULL);
struct meshmb_session *session = open_session(&r->bid);
int ret;
if (session
&& meshmb_ignore(session->feeds, &r->u.meshmb_feeds.bundle_id)!=-1
if (ret!=-1
&& meshmb_flush(session->feeds)!=-1){
http_request_simple_response(&r->http, 201, "TODO, detailed response");
ret = 201;
@ -545,7 +534,7 @@ static int restful_feedlist_enum(struct meshmb_feed_details *details, void *cont
strbuf_json_hex(state->buffer, details->ply.bundle_id.binary, sizeof details->ply.bundle_id.binary);
strbuf_puts(state->buffer, ",");
strbuf_json_hex(state->buffer, details->ply.author.binary, sizeof details->ply.author.binary);
strbuf_puts(state->buffer, ",");
strbuf_puts(state->buffer, details->blocked ? ",true," : ",false,");
strbuf_json_string(state->buffer, details->name);
strbuf_puts(state->buffer, ",");
strbuf_sprintf(state->buffer, "%d", details->timestamp);
@ -569,6 +558,7 @@ static int restful_meshmb_feedlist_json_content_chunk(struct http_request *hr, s
const char *headers[] = {
"id",
"author",
"blocked",
"name",
"timestamp",
"last_message"
@ -918,13 +908,21 @@ static int restful_meshmb_(httpd_request *r, const char *remainder)
remainder = "";
} else if(str_startswith(remainder, "/follow/", &end)
&& strn_to_identity_t(&r->u.meshmb_feeds.bundle_id, end, &end) != -1) {
handler = restful_meshmb_follow;
handler = restful_meshmb_follow_ignore;
verb = HTTP_VERB_POST;
r->ui64 = FLAG_FOLLOW;
remainder = "";
} else if(str_startswith(remainder, "/ignore/", &end)
&& strn_to_identity_t(&r->u.meshmb_feeds.bundle_id, end, &end) != -1) {
handler = restful_meshmb_ignore;
handler = restful_meshmb_follow_ignore;
verb = HTTP_VERB_POST;
r->ui64 = FLAG_IGNORE;
remainder = "";
} else if(str_startswith(remainder, "/block/", &end)
&& strn_to_identity_t(&r->u.meshmb_feeds.bundle_id, end, &end) != -1) {
handler = restful_meshmb_follow_ignore;
verb = HTTP_VERB_POST;
r->ui64 = FLAG_BLOCK;
remainder = "";
}
/*

View File

@ -85,16 +85,16 @@ setup_meshmbFollow() {
test_meshmbFollow() {
executeOk_servald meshmb follow $IDA1 $IDA2
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=0 ":$IDA3:$SIDA3:Feed C:[0-9]\+:Message 3\$"
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:false:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=0 ":$IDA3:$SIDA3:false:Feed C:[0-9]\+:Message 3\$"
executeOk_servald meshmb follow $IDA1 $IDA3
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:Feed C:[0-9]\+:Message 3\$"
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:false:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:false:Feed C:[0-9]\+:Message 3\$"
executeOk_servald meshmb ignore $IDA1 $IDA2
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=0 ":$IDA2:$SIDA2:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:Feed C:[0-9]\+:Message 3\$"
assertStdoutGrep --matches=0 ":$IDA2:$SIDA2:false:Feed B:[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:false:Feed C:[0-9]\+:Message 3\$"
}
doc_meshmbThreading="Thread incoming message feeds"
@ -143,4 +143,32 @@ test_meshmbThreading() {
assertStdoutGrep --matches=1 "5:$IDA5:$SIDA5:Feed E:[0-9]\+:[0-9]\+:Message 6\$"
}
doc_meshmbBlock="Record blocked feeds"
setup_meshmbBlock() {
setup_identities 4
executeOk_servald keyring set did $SIDA1 "" "Feed A"
executeOk_servald keyring set did $SIDA2 "" "Feed B"
executeOk_servald keyring set did $SIDA3 "" "Feed C"
executeOk_servald keyring set did $SIDA4 "" "Feed D"
executeOk_servald meshmb send $IDA2 "Message 1"
executeOk_servald meshmb send $IDA3 "Message 2"
executeOk_servald meshmb send $IDA4 "Message 3"
executeOk_servald meshmb follow $IDA1 $IDA2
executeOk_servald meshmb follow $IDA1 $IDA3
}
test_meshmbBlock() {
executeOk_servald meshmb block $IDA1 $IDA3
executeOk_servald meshmb block $IDA1 $IDA4
executeOk_servald meshmb activity $IDA1
tfw_cat --stdout
assertStdoutGrep --matches=1 "0:$IDA2:$SIDA2:Feed B:[0-9]\+:[0-9]\+:Message 1\$"
# what was followed, disappears
assertStdoutGrep --matches=0 ":$IDA3:$SIDA3:"
executeOk_servald meshmb list following $IDA1
tfw_cat --stdout
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:false:Feed B:[0-9]\+:Message 1\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:true::-1:\$"
assertStdoutGrep --matches=1 ":$IDA4:$SIDA4:true::-1:\$"
}
runTests "$@"

View File

@ -110,8 +110,8 @@ test_MeshMBRestFollow() {
--request POST \
"http://$addr_localhost:$PORTA/restful/meshmb/$IDA1/follow/$IDA2"
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=0 ":$IDA3:$SIDA3::[0-9]\+:Message 3\$"
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:false::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=0 ":$IDA3:$SIDA3:false::[0-9]\+:Message 3\$"
executeOk curl \
--silent --fail --show-error \
--output follow.json \
@ -119,8 +119,8 @@ test_MeshMBRestFollow() {
--request POST \
"http://$addr_localhost:$PORTA/restful/meshmb/$IDA1/follow/$IDA3"
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3::[0-9]\+:Message 3\$"
assertStdoutGrep --matches=1 ":$IDA2:$SIDA2:false::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:false::[0-9]\+:Message 3\$"
executeOk curl \
--silent --fail --show-error \
--output follow.json \
@ -128,8 +128,8 @@ test_MeshMBRestFollow() {
--request POST \
"http://$addr_localhost:$PORTA/restful/meshmb/$IDA1/ignore/$IDA2"
executeOk_servald meshmb list following $IDA1
assertStdoutGrep --matches=0 ":$IDA2:$SIDA2::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3::[0-9]\+:Message 3\$"
assertStdoutGrep --matches=0 ":$IDA2:$SIDA2:false::[0-9]\+:Message 2\$"
assertStdoutGrep --matches=1 ":$IDA3:$SIDA3:false::[0-9]\+:Message 3\$"
}
doc_MeshMBRestFeeds="Restful list subscribed feeds"