diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 8 | ||||
-rw-r--r-- | TODO.md | 8 | ||||
-rw-r--r-- | activitypub.c | 42 | ||||
-rw-r--r-- | data.c | 778 | ||||
-rw-r--r-- | doc/snac.1 | 5 | ||||
-rw-r--r-- | doc/snac.8 | 26 | ||||
-rw-r--r-- | html.c | 20 | ||||
-rw-r--r-- | httpd.c | 2 | ||||
-rw-r--r-- | main.c | 27 | ||||
-rw-r--r-- | snac.h | 30 | ||||
-rw-r--r-- | upgrade.c | 131 | ||||
-rw-r--r-- | utils.c | 5 | ||||
-rw-r--r-- | xs.h | 5 | ||||
-rw-r--r-- | xs_glob.h | 2 | ||||
-rw-r--r-- | xs_io.h | 2 | ||||
-rw-r--r-- | xs_regex.h | 4 | ||||
-rw-r--r-- | xs_set.h | 17 | ||||
-rw-r--r-- | xs_version.h | 2 |
19 files changed, 888 insertions, 227 deletions
@@ -7,6 +7,7 @@ A simple, minimalistic ActivityPub instance - Lightweight, minimal dependencies - Extensive support of ActivityPub operations, e.g. write public notes, follow users, be followed, reply to the notes of others, admire wonderful content (like or boost), write private messages... - Simple but effective web interface +- Multiuser - Easily-accessed MUTE button to silence morons - Tested interoperability with related software - No database needed diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a0706c8..1d8a3ea 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,13 @@ # Release Notes +## 2.13 + +A big disk layout rework, to make it more efficient when timelines get very big. Please take note that you must run `snac upgrade` when you install this version over an already existing one. + +Fixed HTML loose close tag. + +Fixed bug when closing sendmail pipe. + ## 2.12 Fixed some bugs triggered when a GET query does not have an `Accept:` header. @@ -2,7 +2,11 @@ ## Open -Dropping on input those messages that have their parent hidden is not a good idea, as children of *these* dropped messages will pass unharmed. +Add an ?skip=NNN parameter to the admin page, to see older timeline. + +Add support for Edit + Note. + +Add domain/subdomain flexibility according to https://codeberg.org/grunfink/snac2/issues/3 Add support for uploading the avatar, instead of needing an URL to an image. As a kludgy workaround, you can post something with an attached image, copy the auto-generated URL and use it. You can even delete the post, as attached images are never deleted (I said it was kludgy). @@ -177,3 +181,5 @@ Add a purge timeout also for the local timeline (2022-11-12T08:32:56+0100). Add a switch for sensitive posts (2022-11-16T12:17:50+0100). Add an RSS to the local timeline (2022-11-18T11:43:54+0100). + +Dropping on input those messages that have their parent hidden is not a good idea, as children of *these* dropped messages will pass unharmed (2022-11-28T11:34:56+0100). diff --git a/activitypub.c b/activitypub.c index 5f26f73..7f63310 100644 --- a/activitypub.c +++ b/activitypub.c @@ -9,6 +9,7 @@ #include "xs_openssl.h" #include "xs_regex.h" #include "xs_time.h" +#include "xs_set.h" #include "snac.h" @@ -169,11 +170,13 @@ int send_to_actor(snac *snac, char *actor, char *msg, d_char **payload, int *p_s d_char *recipient_list(snac *snac, char *msg, int expand_public) /* returns the list of recipients for a message */ { - d_char *list = xs_list_new(); char *to = xs_dict_get(msg, "to"); char *cc = xs_dict_get(msg, "cc"); + xs_set rcpts; int n; + xs_set_init(&rcpts); + char *lists[] = { to, cc, NULL }; for (n = 0; lists[n]; n++) { char *l = lists[n]; @@ -192,45 +195,41 @@ d_char *recipient_list(snac *snac, char *msg, int expand_public) if (expand_public && strcmp(v, public_address) == 0) { /* iterate the followers and add them */ xs *fwers = follower_list(snac); - char *fw; + char *actor; char *p = fwers; - while (xs_list_iter(&p, &fw)) { - char *actor = xs_dict_get(fw, "actor"); - - if (xs_list_in(list, actor) == -1) - list = xs_list_append(list, actor); - } + while (xs_list_iter(&p, &actor)) + xs_set_add(&rcpts, actor); } else - if (xs_list_in(list, v) == -1) - list = xs_list_append(list, v); + xs_set_add(&rcpts, v); } } - return list; + return xs_set_result(&rcpts); } d_char *inbox_list(snac *snac, char *msg) /* returns the list of inboxes that are recipients of this message */ { - d_char *list = xs_list_new(); - xs *rcpts = recipient_list(snac, msg, 1); + xs *rcpts = recipient_list(snac, msg, 1); + xs_set inboxes; char *p, *v; + xs_set_init(&inboxes); + p = rcpts; while (xs_list_iter(&p, &v)) { xs *inbox; if ((inbox = get_actor_inbox(snac, v)) != NULL) { /* add the inbox if it's not already there */ - if (xs_list_in(list, inbox) == -1) - list = xs_list_append(list, inbox); + xs_set_add(&inboxes, inbox); } } - return list; + return xs_set_result(&inboxes); } @@ -824,7 +823,7 @@ int process_message(snac *snac, char *msg, char *req) timeline_add(snac, xs_dict_get(f_msg, "id"), f_msg, NULL, NULL); - follower_add(snac, actor, f_msg); + follower_add(snac, actor); snac_log(snac, xs_fmt("New follower %s", actor)); do_notify = 1; @@ -918,6 +917,7 @@ int process_message(snac *snac, char *msg, char *req) if (strcmp(type, "Update") == 0) { if (strcmp(utype, "Person") == 0) { actor_add(snac, actor, xs_dict_get(msg, "object")); + snac_log(snac, xs_fmt("updated actor %s", actor)); } else @@ -1017,10 +1017,10 @@ void process_queue(snac *snac) FILE *f; int ok = 0; - f = popen("/usr/sbin/sendmail -t", "w"); - if (f) { + if ((f = popen("/usr/sbin/sendmail -t", "w")) != NULL) { fprintf(f, "%s\n", msg); - if (pclose(f) != EOF) //this is a pipe stream not just a file + + if (pclose(f) != -1) ok = 1; } @@ -1089,7 +1089,7 @@ int activitypub_get_handler(d_char *req, char *q_path, if (p_path == NULL) { /* if there was no component after the user, it's an actor request */ msg = msg_actor(&snac); - *ctype = "application/ld+json"; + *ctype = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""; } else if (strcmp(p_path, "outbox") == 0) { @@ -6,6 +6,7 @@ #include "xs_json.h" #include "xs_openssl.h" #include "xs_glob.h" +#include "xs_set.h" #include "snac.h" @@ -14,12 +15,12 @@ #include <sys/file.h> #include <fcntl.h> -double db_layout = 2.1; +double db_layout = 2.5; int db_upgrade(d_char **error); -int srv_open(char *basedir) +int srv_open(char *basedir, int auto_upgrade) /* opens a server */ { int ret = 0; @@ -69,7 +70,14 @@ int srv_open(char *basedir) error = xs_fmt("DEBUG level set to %d from environment", dbglevel); } - ret = db_upgrade(&error); + if (auto_upgrade) + ret = db_upgrade(&error); + else { + if (xs_number_get(xs_dict_get(srv_config, "layout")) < db_layout) + error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first"); + else + ret = 1; + } } } @@ -178,24 +186,215 @@ d_char *user_list(void) } -double mtime(char *fn) -/* returns the mtime of a file or directory, or 0.0 */ +double mtime_nl(const char *fn, int *n_link) +/* returns the mtime and number of links of a file or directory, or 0.0 */ { struct stat st; double r = 0.0; + int n = 0; - if (fn && stat(fn, &st) != -1) - r = (double)st.st_mtim.tv_sec; + if (fn && stat(fn, &st) != -1) { + r = (double) st.st_mtim.tv_sec; + n = st.st_nlink; + } + + if (n_link) + *n_link = n; return r; } -/** object database 2.1+ **/ +/** database 2.1+ **/ + +/** indexes **/ + +int index_add_md5(const char *fn, const char *md5) +/* adds an md5 to an index */ +{ + int status = 201; /* Created */ + FILE *f; + + if ((f = fopen(fn, "a")) != NULL) { + flock(fileno(f), LOCK_EX); + + /* ensure the position is at the end after getting the lock */ + fseek(f, 0, SEEK_END); + + fprintf(f, "%s\n", md5); + fclose(f); + } + else + status = 500; + + return status; +} + + +int index_add(const char *fn, const char *id) +/* adds an id to an index */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return index_add_md5(fn, md5); +} + + +int index_del(const char *fn, const char *md5) +/* deletes an md5 from an index */ +{ + int status = 404; + FILE *i, *o; + + if ((i = fopen(fn, "r")) != NULL) { + flock(fileno(i), LOCK_EX); + + xs *nfn = xs_fmt("%s.new", fn); + char line[256]; + + if ((o = fopen(nfn, "w")) != NULL) { + while (fgets(line, sizeof(line), i) != NULL) { + line[32] = '\0'; + if (memcmp(line, md5, 32) != 0) + fprintf(o, "%s\n", line); + } + + fclose(o); + + xs *ofn = xs_fmt("%s.bak", fn); + + link(fn, ofn); + rename(nfn, fn); + } + else + status = 500; + + fclose(i); + } + else + status = 500; + + return status; +} + + +int index_in_md5(const char *fn, const char *md5) +/* checks if the md5 is already in the index */ +{ + FILE *f; + int ret = 0; + + if ((f = fopen(fn, "r")) != NULL) { + flock(fileno(f), LOCK_SH); + + char line[256]; + + while (!ret && fgets(line, sizeof(line), f) != NULL) { + line[32] = '\0'; + + if (strcmp(line, md5) == 0) + ret = 1; + } + + fclose(f); + } + + return ret; +} + + +int index_in(const char *fn, const char *id) +/* checks if the object id is already in the index */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return index_in_md5(fn, md5); +} + + +int index_first(const char *fn, char *line, int size) +/* reads the first entry of an index */ +{ + FILE *f; + int ret = 0; + + if ((f = fopen(fn, "r")) != NULL) { + flock(fileno(f), LOCK_SH); + + if (fgets(line, size, f) != NULL) { + line[32] = '\0'; + ret = 1; + } + + fclose(f); + } + + return ret; +} + + +d_char *index_list(const char *fn, int max) +/* returns an index as a list */ +{ + d_char *list = NULL; + FILE *f; + int n = 0; + + if ((f = fopen(fn, "r")) != NULL) { + flock(fileno(f), LOCK_SH); + + char line[256]; + list = xs_list_new(); + + while (n < max && fgets(line, sizeof(line), f) != NULL) { + line[32] = '\0'; + list = xs_list_append(list, line); + n++; + } + + fclose(f); + } + + return list; +} + + +d_char *index_list_desc(const char *fn, int max) +/* returns an index as a list, in reverse order */ +{ + d_char *list = NULL; + FILE *f; + int n = 0; + + if ((f = fopen(fn, "r")) != NULL) { + flock(fileno(f), LOCK_SH); + + char line[256]; + list = xs_list_new(); + + /* move to the end minus one entry */ + if (!fseek(f, 0, SEEK_END) && !fseek(f, -33, SEEK_CUR)) { + while (n < max && fgets(line, sizeof(line), f) != NULL) { + line[32] = '\0'; + list = xs_list_append(list, line); + n++; + + /* move backwards 2 entries */ + if (fseek(f, -66, SEEK_CUR) == -1) + break; + } + } + + fclose(f); + } + + return list; +} + + +/** objects **/ d_char *_object_fn_by_md5(const char *md5) { - xs *bfn = xs_fmt("%s/object/%c%c/", srv_basedir, md5[0], md5[1]); + xs *bfn = xs_fmt("%s/object/%c%c", srv_basedir, md5[0], md5[1]); mkdir(bfn, 0755); @@ -203,19 +402,18 @@ d_char *_object_fn_by_md5(const char *md5) } -d_char *_object_fn_by_id(const char *id) +d_char *_object_fn(const char *id) { xs *md5 = xs_md5_hex(id, strlen(id)); - return _object_fn_by_md5(md5); } -int object_get(const char *id, d_char **obj, const char *type) -/* returns a loaded object, optionally of the requested type */ +int object_get_by_md5(const char *md5, d_char **obj, const char *type) +/* returns a stored object, optionally of the requested type */ { int status = 404; - xs *fn = _object_fn_by_id(id); + xs *fn = _object_fn_by_md5(md5); FILE *f; if ((f = fopen(fn, "r")) != NULL) { @@ -247,13 +445,27 @@ int object_get(const char *id, d_char **obj, const char *type) } -int object_add(const char *id, d_char *obj) +int object_get(const char *id, d_char **obj, const char *type) +/* returns a stored object, optionally of the requested type */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return object_get_by_md5(md5, obj, type); +} + + +int _object_add(const char *id, d_char *obj, int ow) /* stores an object */ { int status = 201; /* Created */ - xs *fn = _object_fn_by_id(id); + xs *fn = _object_fn(id); FILE *f; + if (!ow && mtime(fn) > 0.0) { + /* object already here */ + srv_debug(0, xs_fmt("object_add object already here %s", id)); + return 204; /* No content */ + } + if ((f = fopen(fn, "w")) != NULL) { flock(fileno(f), LOCK_EX); @@ -261,100 +473,241 @@ int object_add(const char *id, d_char *obj) fwrite(j, strlen(j), 1, f); fclose(f); + + /* does this object has a parent? */ + char *in_reply_to = xs_dict_get(obj, "inReplyTo"); + + if (!xs_is_null(in_reply_to) && *in_reply_to) { + /* update the children index of the parent */ + xs *c_idx = _object_fn(in_reply_to); + + c_idx = xs_replace_i(c_idx, ".json", "_c.idx"); + index_add(c_idx, id); + + srv_debug(0, xs_fmt("object_add added child %s to %s", id, c_idx)); + + /* create a one-element index with the parent */ + xs *p_idx = xs_replace(fn, ".json", "_p.idx"); + index_add(p_idx, in_reply_to); + + srv_debug(0, xs_fmt("object_add added parent %s to %s", in_reply_to, p_idx)); + } } else status = 500; + srv_debug(0, xs_fmt("object_add %s %s %d", id, fn, status)); + return status; } -d_char *_follower_fn(snac *snac, char *actor) +int object_add(const char *id, d_char *obj) +/* stores an object */ { - xs *md5 = xs_md5_hex(actor, strlen(actor)); - return xs_fmt("%s/followers/%s.json", snac->basedir, md5); + return _object_add(id, obj, 0); } -int follower_add(snac *snac, char *actor, char *msg) -/* adds a follower */ +int object_add_ow(const char *id, d_char *obj) +/* stores an object (overwriting allowed) */ { - int ret = 201; /* created */ - xs *fn = _follower_fn(snac, actor); - FILE *f; + return _object_add(id, obj, 1); +} - if ((f = fopen(fn, "w")) != NULL) { - xs *j = xs_json_dumps_pp(msg, 4); - fwrite(j, 1, strlen(j), f); - fclose(f); +int object_del_by_md5(const char *md5) +/* deletes an object by its md5 */ +{ + int status = 404; + xs *fn = _object_fn_by_md5(md5); + + if (fn != NULL && unlink(fn) != -1) { + status = 200; + + /* also delete associated indexes */ + xs *spec = xs_dup(fn); + spec = xs_replace_i(spec, ".json", "*.idx"); + xs *files = xs_glob(spec, 0, 0); + char *p, *v; + + p = files; + while (xs_list_iter(&p, &v)) { + srv_debug(0, xs_fmt("object_del index %s", v)); + unlink(v); + } } - else - ret = 500; - snac_debug(snac, 2, xs_fmt("follower_add %s %s", actor, fn)); + srv_debug(0, xs_fmt("object_del %s %d", fn, status)); + + return status; +} + + +int object_del(const char *id) +/* deletes an object */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + return object_del_by_md5(md5); +} + + +int object_del_if_unref(const char *id) +/* deletes an object if its n_links < 2 */ +{ + xs *fn = _object_fn(id); + int n_links; + int ret = 0; + + if (mtime_nl(fn, &n_links) > 0.0 && n_links < 2) + ret = object_del(id); return ret; } -int follower_del(snac *snac, char *actor) -/* deletes a follower */ +d_char *object_children(const char *id) +/* returns the list of an object's children */ +{ + xs *fn = _object_fn(id); + + fn = xs_replace_i(fn, ".json", "_c.idx"); + + return index_list(fn, XS_ALL); +} + + +int object_admire(const char *id, const char *actor, int like) +/* actor likes or announces this object */ { int status = 200; - xs *fn = _follower_fn(snac, actor); + xs *fn = _object_fn(id); - if (fn != NULL) - unlink(fn); - else - status = 404; + fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx"); - snac_debug(snac, 2, xs_fmt("follower_del %s %s", actor, fn)); + if (!index_in(fn, actor)) { + status = index_add(fn, actor); + + srv_debug(0, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn)); + } return status; } -int follower_check(snac *snac, char *actor) -/* checks if someone is a follower */ +int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del) +/* adds or deletes from a user cache */ { - xs *fn = _follower_fn(snac, actor); + xs *ofn = _object_fn(id); + xs *l = xs_split(ofn, "/"); + xs *cfn = xs_fmt("%s/%s/%s", snac->basedir, cachedir, xs_list_get(l, -1)); + xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir); + int ret; + + if (del) { + if ((ret = unlink(cfn)) != -1) + index_del(idx, id); + } + else { + index_add(idx, id); + ret = link(ofn, cfn); + } - return !!(mtime(fn) != 0.0); + return ret; +} + + +int object_user_cache_add(snac *snac, const char *id, const char *cachedir) +/* caches an object into a user cache */ +{ + return _object_user_cache(snac, id, cachedir, 0); +} + + +int object_user_cache_del(snac *snac, const char *id, const char *cachedir) +/* deletes an object from a user cache */ +{ + return _object_user_cache(snac, id, cachedir, 1); +} + + +int object_user_cache_in(snac *snac, const char *id, const char *cachedir) +/* checks if an object is stored in a cache */ +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + xs *cfn = xs_fmt("%s/%s/%s.json", snac->basedir, cachedir, md5); + + return !!(mtime(cfn) != 0.0); +} + + +d_char *object_user_cache_list(snac *snac, const char *cachedir, int max) +/* returns the objects in a cache as a list */ +{ + xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir); + return index_list(idx, max); +} + + +/** specialized functions **/ + +/** followers **/ + +int follower_add(snac *snac, const char *actor) +/* adds a follower */ +{ + int ret = object_user_cache_add(snac, actor, "followers"); + + snac_debug(snac, 2, xs_fmt("follower_add %s %s", actor)); + + return ret == -1 ? 500 : 200; +} + + +int follower_del(snac *snac, const char *actor) +/* deletes a follower */ +{ + int ret = object_user_cache_del(snac, actor, "followers"); + + snac_debug(snac, 2, xs_fmt("follower_del %s %s", actor)); + + return ret == -1 ? 404 : 200; +} + + +int follower_check(snac *snac, const char *actor) +/* checks if someone is a follower */ +{ + return object_user_cache_in(snac, actor, "followers"); } d_char *follower_list(snac *snac) /* returns the list of followers */ { - xs *spec = xs_fmt("%s/followers/" "*.json", snac->basedir); - xs *glist = xs_glob(spec, 0, 0); + xs *list = object_user_cache_list(snac, "followers", XS_ALL); + d_char *fwers = xs_list_new(); char *p, *v; - d_char *list = xs_list_new(); - /* iterate the list of files */ - p = glist; + /* resolve the list of md5 to be a list of actors */ + p = list; while (xs_list_iter(&p, &v)) { - FILE *f; - - /* load the follower data */ - if ((f = fopen(v, "r")) != NULL) { - xs *j = xs_readall(f); - fclose(f); + xs *a_obj = NULL; - if (j != NULL) { - xs *o = xs_json_loads(j); + if (valid_status(object_get_by_md5(v, &a_obj, NULL))) { + char *actor = xs_dict_get(a_obj, "id"); - if (o != NULL) - list = xs_list_append(list, o); - } + if (!xs_is_null(actor)) + fwers = xs_list_append(fwers, actor); } } - return list; + return fwers; } +/** timeline **/ + double timeline_mtime(snac *snac) { xs *fn = xs_fmt("%s/timeline", snac->basedir); @@ -437,6 +790,13 @@ int timeline_del(snac *snac, char *id) ret = 200; } + /* delete from the user's caches */ + object_user_cache_del(snac, id, "public"); + object_user_cache_del(snac, id, "private"); + + /* try to delete the object if it's not used elsewhere */ + object_del_if_unref(id); + return ret; } @@ -511,22 +871,11 @@ int _timeline_write(snac *snac, char *id, char *msg, char *parent, char *referre if (pfn != NULL && (f = fopen(pfn, "r")) != NULL) { xs *j; - char *v; j = xs_readall(f); fclose(f); p_msg = xs_json_loads(j); - - if ((v = xs_dict_get(p_msg, "_snac")) != NULL) { - /* is parent hidden? */ - if ((v = xs_dict_get(v, "hidden")) && xs_type(v) == XSTYPE_TRUE) { - snac_debug(snac, 1, - xs_fmt("_timeline_write dropping due to hidden parent %s (%s)", id, parent)); - - return 0; - } - } } } @@ -648,6 +997,16 @@ int _timeline_write(snac *snac, char *id, char *msg, char *parent, char *referre } +void timeline_update_indexes(snac *snac, const char *id) +/* updates the indexes */ +{ + object_user_cache_add(snac, id, "private"); + + if (xs_startswith(id, snac->actor)) + object_user_cache_add(snac, id, "public"); +} + + int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer) /* adds a message to the timeline */ { @@ -680,13 +1039,61 @@ int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer msg = xs_dict_set(msg, "_snac", md); - if ((ret = _timeline_write(snac, id, msg, parent, referrer))) + if ((ret = _timeline_write(snac, id, msg, parent, referrer))) { snac_debug(snac, 1, xs_fmt("timeline_add %s", id)); + object_add(id, o_msg); + timeline_update_indexes(snac, id); + } + return ret; } +d_char *timeline_top_level(snac *snac, d_char *list) +/* returns the top level md5 entries from this index */ +{ + d_char *tl = xs_list_new(); + xs_set seen; + char *p, *v; + + xs_set_init(&seen); + + p = list; + while (xs_list_iter(&p, &v)) { + char line[256] = ""; + + strcpy(line, v); + + for (;;) { + char line2[256]; + xs *fn = _object_fn_by_md5(line); + fn = xs_replace_i(fn, ".json", "_p.idx"); + + /* if it doesn't have a parent, use this */ + if (index_first(fn, line2, sizeof(line2)) == 0) + break; + + xs *pfn = _object_fn_by_md5(line2); + + /* well, there is a parent... but if it's not there, use this */ + if (mtime(pfn) == 0.0) + break; + + /* it's here! try again with its own parent */ + strcpy(line, line2); + } + + if (xs_set_add(&seen, line) == 1) + tl = xs_list_append(tl, line); + } + + xs_set_free(&seen); + + return tl; +} + + void timeline_admire(snac *snac, char *id, char *admirer, int like) /* updates a timeline entry with a new admiration */ { @@ -734,52 +1141,16 @@ void timeline_admire(snac *snac, char *id, char *admirer, int like) } else snac_log(snac, xs_fmt("timeline_admire ignored for unknown object %s", id)); -} - - -int timeline_hide(snac *snac, char *id, int hide) -/* hides/unhides a timeline entry */ -{ - int ret = 0; - xs *fn = _timeline_find_fn(snac, id); - FILE *f; - - if (fn != NULL && (f = fopen(fn, "r")) != NULL) { - xs *s1 = xs_readall(f); - xs *msg = xs_json_loads(s1); - xs *meta = xs_dup(xs_dict_get(msg, "_snac")); - xs *hdn = xs_val_new(hide ? XSTYPE_TRUE : XSTYPE_FALSE); - char *p, *v; - - fclose(f); - - /* if it's already in this hidden state, we're done */ - if ((v = xs_dict_get(meta, "hidden")) && xs_type(v) == xs_type(hdn)) - return ret; - - meta = xs_dict_set(meta, "hidden", hdn); - msg = xs_dict_set(msg, "_snac", meta); - - if ((f = fopen(fn, "w")) != NULL) { - xs *j1 = xs_json_dumps_pp(msg, 4); - - fwrite(j1, strlen(j1), 1, f); - fclose(f); - snac_debug(snac, 1, xs_fmt("timeline_hide %d %s", hide, id)); + object_admire(id, admirer, like); +} - /* now hide the children */ - p = xs_dict_get(meta, "children"); - while (xs_list_iter(&p, &v)) - timeline_hide(snac, v, hide); - ret = 1; - } - } - - return ret; -} +/** following **/ +/* this needs special treatment and cannot use the object db as is, + with a link to a cached author, because we need the Follow object + in case we need to unfollow (Undo + original Follow) */ d_char *_following_fn(snac *snac, char *actor) { @@ -811,7 +1182,7 @@ int following_add(snac *snac, char *actor, char *msg) int following_del(snac *snac, char *actor) -/* someone is no longer following us */ +/* we're not following this actor any longer */ { xs *fn = _following_fn(snac, actor); @@ -824,7 +1195,7 @@ int following_del(snac *snac, char *actor) int following_check(snac *snac, char *actor) -/* checks if someone is following us */ +/* checks if we are following this actor */ { xs *fn = _following_fn(snac, actor); @@ -877,8 +1248,12 @@ d_char *following_list(snac *snac) if (o != NULL) { char *type = xs_dict_get(o, "type"); - if (!xs_is_null(type) && strcmp(type, "Accept") == 0) - list = xs_list_append(list, o); + if (!xs_is_null(type) && strcmp(type, "Accept") == 0) { + char *actor = xs_dict_get(o, "actor"); + + if (!xs_is_null(actor)) + list = xs_list_append(list, actor); + } } } } @@ -891,7 +1266,7 @@ d_char *following_list(snac *snac) d_char *_muted_fn(snac *snac, char *actor) { xs *md5 = xs_md5_hex(actor, strlen(actor)); - return xs_fmt("%s/muted/%s.json", snac->basedir, md5); + return xs_fmt("%s/muted/%s", snac->basedir, md5); } @@ -930,65 +1305,89 @@ int is_muted(snac *snac, char *actor) } -d_char *_actor_fn(snac *snac, char *actor) -/* returns the file name for an actor */ +d_char *_hidden_fn(snac *snac, const char *id) { - xs *md5 = xs_md5_hex(actor, strlen(actor)); - return xs_fmt("%s/actors/%s.json", snac->basedir, md5); + xs *md5 = xs_md5_hex(id, strlen(id)); + return xs_fmt("%s/hidden/%s", snac->basedir, md5); } -int actor_add(snac *snac, char *actor, char *msg) -/* adds an actor */ +void hide(snac *snac, const char *id) +/* hides a message tree */ { - int ret = 201; /* created */ - xs *fn = _actor_fn(snac, actor); + xs *fn = _hidden_fn(snac, id); FILE *f; if ((f = fopen(fn, "w")) != NULL) { - xs *j = xs_json_dumps_pp(msg, 4); - - fwrite(j, 1, strlen(j), f); + fprintf(f, "%s\n", id); fclose(f); + + snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn)); + + /* hide all the children */ + xs *chld = object_children(id); + char *p, *v; + + p = chld; + while (xs_list_iter(&p, &v)) { + xs *co = NULL; + + /* resolve to get the id */ + if (valid_status(object_get_by_md5(v, &co, NULL))) { + if ((v = xs_dict_get(co, "id")) != NULL) + hide(snac, v); + } + } } - else - ret = 500; +} - snac_debug(snac, 2, xs_fmt("actor_add %s %s", actor, fn)); -// object_add(actor, msg); +int is_hidden(snac *snac, const char *id) +/* check is id is hidden */ +{ + xs *fn = _hidden_fn(snac, id); - return ret; + return !!(mtime(fn) != 0.0); +} + + +int actor_add(snac *snac, const char *actor, d_char *msg) +/* adds an actor */ +{ + return object_add_ow(actor, msg); } -int actor_get(snac *snac, char *actor, d_char **data) +int actor_get(snac *snac, const char *actor, d_char **data) /* returns an already downloaded actor */ { - xs *fn = _actor_fn(snac, actor); - double t; - double max_time; - int status; - FILE *f; + int status = 200; + char *d; if (strcmp(actor, snac->actor) == 0) { + /* this actor */ if (data) *data = msg_actor(snac); - return 200; + return status; } - t = mtime(fn); + /* read the object */ + if (!valid_status(status = object_get(actor, &d, NULL))) + return status; - /* no mtime? there is nothing here */ - if (t == 0.0) - return 404; + if (data) + *data = d; + + xs *fn = _object_fn(actor); + double max_time; /* maximum time for the actor data to be considered stale */ max_time = 3600.0 * 36.0; - if (t + max_time < (double) time(NULL)) { + if (mtime(fn) + max_time < (double) time(NULL)) { /* actor data exists but also stinks */ + FILE *f; if ((f = fopen(fn, "a")) != NULL) { /* write a blank at the end to 'touch' the file */ @@ -998,22 +1397,6 @@ int actor_get(snac *snac, char *actor, d_char **data) status = 205; /* "205: Reset Content" "110: Response Is Stale" */ } - else { - /* it's still valid */ - status = 200; - } - - if (data) { - if ((f = fopen(fn, "r")) != NULL) { - xs *j = xs_readall(f); - - fclose(f); - - *data = xs_json_loads(j); - } - else - status = 500; - } return status; } @@ -1033,7 +1416,7 @@ int static_get(snac *snac, const char *id, d_char **data, int *size) FILE *f; int status = 404; - *size = 0xfffffff; + *size = XS_ALL; if ((f = fopen(fn, "rb")) != NULL) { *data = xs_read(f, size); @@ -1121,6 +1504,8 @@ d_char *history_list(snac *snac) } +/** the queue **/ + static int _enqueue_put(char *fn, char *msg) /* writes safely to the queue */ { @@ -1267,38 +1652,81 @@ d_char *dequeue(snac *snac, char *fn) } +/** the purge **/ + +static void _purge_file(const char *fn, time_t mt) +/* purge fn if it's older than days */ +{ + if (mtime(fn) < mt) { + /* older than the minimum time: delete it */ + unlink(fn); + srv_debug(1, xs_fmt("purged %s", fn)); + } +} + + static void _purge_subdir(snac *snac, const char *subdir, int days) /* purges all files in subdir older than days */ { if (days) { time_t mt = time(NULL) - days * 24 * 3600; - xs *spec = xs_fmt("%s/%s/" "*.json", snac->basedir, subdir); + xs *spec = xs_fmt("%s/%s/" "*", snac->basedir, subdir); xs *list = xs_glob(spec, 0, 0); char *p, *v; p = list; - while (xs_list_iter(&p, &v)) { - if (mtime(v) < mt) { - /* older than the minimum time: delete it */ - unlink(v); - snac_debug(snac, 1, xs_fmt("purged %s", v)); + while (xs_list_iter(&p, &v)) + _purge_file(v, mt); + } +} + + +void purge_server(void) +/* purge global server data */ +{ + int tpd = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days")); + xs *spec = xs_fmt("%s/object/??", srv_basedir); + xs *dirs = xs_glob(spec, 0, 0); + char *p, *v; + + time_t mt = time(NULL) - tpd * 24 * 3600; + + p = dirs; + while (xs_list_iter(&p, &v)) { + xs *spec2 = xs_fmt("%s/" "*.json", v); + xs *files = xs_glob(spec2, 0, 0); + char *p2, *v2; + + p2 = files; + while (xs_list_iter(&p2, &v2)) { + int n_link; + + /* old and with no hard links? */ + if (mtime_nl(v2, &n_link) < mt && n_link < 2) { + xs *s1 = xs_replace(v2, ".json", ""); + xs *l = xs_split(s1, "/"); + char *md5 = xs_list_get(l, -1); + + object_del_by_md5(md5); } } } } -void purge(snac *snac) -/* do the purge */ +void purge_user(snac *snac) +/* do the purge for this user */ { int days; days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days")); _purge_subdir(snac, "timeline", days); - _purge_subdir(snac, "actors", days); + _purge_subdir(snac, "hidden", days); + _purge_subdir(snac, "private", days); days = xs_number_get(xs_dict_get(srv_config, "local_purge_days")); _purge_subdir(snac, "local", days); + _purge_subdir(snac, "public", days); } @@ -1312,8 +1740,10 @@ void purge_all(void) p = list; while (xs_list_iter(&p, &uid)) { if (user_open(&snac, uid)) { - purge(&snac); + purge_user(&snac); user_free(&snac); } } + + purge_server(); } @@ -145,6 +145,11 @@ Initializes the database. This is an interactive command; necessary information will be prompted for. The .Ar basedir directory must not exist. +.It Cm upgrade Ar basedir +Upgrades the database disk layout after installing a new version. +Only necessary if +.Nm +complains and demands it. .It Cm purge Ar basedir Purges old data from the timeline of all users. .It Cm adduser Ar basedir Op uid @@ -14,6 +14,19 @@ This is the admin manual. For user operation, see .Xr snac 1 . For file and data formats, see .Xr snac 5 . +.Ss Special cares about your snac you must know beforehand +.Nm +makes heavy use of hard links and link reference counts for its work, so +don't even think of using it on a filesystem that doesn't support this +feature. Most UNIX-like operating systems (Linux, the BSDs, the old DEC +Ultrix machine in your grandfather basement, probably MacOS) support hard +links on their native filesystems. Don't do fancy things like moving the +subdirectories to different filesystems. Also, if you move your +.Nm +installation to another server, do it with a tool that respect hard +link counts. Remember: +.Nm +is a very UNIXy program that loves hard links. .Ss Building and Installation A C compiler must be installed in the system, as well as the development headers and libraries for OpenSSL and curl. To build @@ -69,6 +82,19 @@ startup scripts and configuration data in the directory. For other operating systems, please read the appropriate documentation on how to install a daemon as a non-root service. +.Ss Upgrading to a new version +Sometimes, the database disk layout changes between versions. If there +is such a change, +.Nm +will refuse to run and require an upgrade. Do this by running +.Bd -literal -offset indent +snac upgrade $HOME/snac-data +.Ed +Take special care to execute this upgrade operation without any +.Nm +processes serving on the same folder. You can break everything. I know +this because Tyler knows this. +.Pp .Ss Server Setup .Pp An http server with TLS and proxying support must already be @@ -196,14 +196,14 @@ d_char *html_user_header(snac *snac, d_char *s, int local) "<a href=\"%s.rss\">%s</a> - " "<a href=\"%s/admin\" rel=\"nofollow\">%s</a></nav>\n", snac->actor, L("RSS"), - snac->actor, L("admin")); + snac->actor, L("private")); else s1 = xs_fmt( "<a href=\"%s\">%s</a> - " "<a href=\"%s/admin\">%s</a> - " "<a href=\"%s/people\">%s</a></nav>\n", snac->actor, L("public"), - snac->actor, L("admin"), + snac->actor, L("private"), snac->actor, L("people")); s = xs_str_cat(s, s1); @@ -497,7 +497,7 @@ d_char *html_entry(snac *snac, d_char *os, char *msg, xs_set *seen, int local, i xs *s = xs_str_new(NULL); /* top wrap */ - if ((v = xs_dict_get(meta, "hidden")) && xs_type(v) == XSTYPE_TRUE) + if (is_hidden(snac, id)) s = xs_str_cat(s, "<div style=\"display: none\">\n"); else s = xs_str_cat(s, "<div>\n"); @@ -840,13 +840,12 @@ d_char *html_people_list(snac *snac, d_char *os, d_char *list, const char *heade { xs *s = xs_str_new(NULL); xs *h = xs_fmt("<h2>%s</h2>\n", header); - char *p, *v; + char *p, *actor_id; s = xs_str_cat(s, h); p = list; - while (xs_list_iter(&p, &v)) { - char *actor_id = xs_dict_get(v, "actor"); + while (xs_list_iter(&p, &actor_id)) { xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); xs *actor = NULL; @@ -1008,7 +1007,7 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char * status = 200; } else { - xs *list = local_list(&snac, 0xfffffff); + xs *list = local_list(&snac, XS_ALL); *body = html_timeline(&snac, list, 1); *b_size = strlen(*body); @@ -1034,7 +1033,7 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char * else { snac_debug(&snac, 1, xs_fmt("building timeline")); - xs *list = timeline_list(&snac, 0xfffffff); + xs *list = timeline_list(&snac, XS_ALL); *body = html_timeline(&snac, list, 0); *b_size = strlen(*body); @@ -1300,7 +1299,7 @@ int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, } else if (strcmp(action, L("Hide")) == 0) { - timeline_hide(&snac, id, 1); + hide(&snac, id); } else if (strcmp(action, L("Follow")) == 0) { @@ -1341,6 +1340,9 @@ int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size, post(&snac, msg); + /* FIXME: also post this Tombstone to people + that Announce'd it */ + snac_log(&snac, xs_fmt("posted tombstone for %s", id)); } @@ -222,7 +222,7 @@ static void *queue_thread(void *arg) time_t purge_time; /* first purge time */ - purge_time = time(NULL) + 15 * 60; + purge_time = time(NULL) + 10 * 60; srv_log(xs_fmt("queue thread start")); @@ -16,6 +16,7 @@ int usage(void) printf("Commands:\n"); printf("\n"); printf("init [{basedir}] Initializes the database\n"); + printf("upgrade {basedir} Upgrade to a new version\n"); printf("adduser {basedir} [{uid}] Adds a new user\n"); printf("httpd {basedir} Starts the HTTPD daemon\n"); printf("purge {basedir} Purges old data\n"); @@ -76,6 +77,19 @@ int main(int argc, char *argv[]) return initdb(basedir); } + if (strcmp(cmd, "upgrade") == 0) { + int ret; + + /* database upgrade */ + if ((basedir = GET_ARGV()) == NULL) + return usage(); + + if ((ret = srv_open(basedir, 1)) == 1) + srv_log(xs_dup("OK")); + + return ret; + } + if (strcmp(cmd, "markdown") == 0) { /* undocumented, for testing only */ xs *c = xs_readall(stdin); @@ -88,7 +102,7 @@ int main(int argc, char *argv[]) if ((basedir = GET_ARGV()) == NULL) return usage(); - if (!srv_open(basedir)) { + if (!srv_open(basedir, 0)) { srv_log(xs_fmt("error opening database at %s", basedir)); return 1; } @@ -142,12 +156,21 @@ int main(int argc, char *argv[]) } if (strcmp(cmd, "timeline") == 0) { - xs *list = local_list(&snac, 0xfffffff); +#if 0 + xs *list = local_list(&snac, XS_ALL); xs *body = html_timeline(&snac, list, 1); printf("%s\n", body); user_free(&snac); srv_free(); +#endif + + xs *idx = xs_fmt("%s/private.idx", snac.basedir); + xs *list = index_list_desc(idx, 256); + xs *tl = timeline_top_level(&snac, list); + + xs *j = xs_json_dumps_pp(tl, 4); + printf("%s\n", j); return 0; } @@ -24,7 +24,7 @@ double ftime(void); void srv_debug(int level, d_char *str); #define srv_log(str) srv_debug(0, str) -int srv_open(char *basedir); +int srv_open(char *basedir, int auto_upgrade); void srv_free(void); typedef struct _snac { @@ -50,11 +50,21 @@ int check_password(char *uid, char *passwd, char *hash); void srv_archive(char *direction, char *req, char *payload, int p_size, int status, char *headers, char *body, int b_size); -double mtime(char *fn); +double mtime_nl(const char *fn, int *n_link); +#define mtime(fn) mtime_nl(fn, NULL) -int follower_add(snac *snac, char *actor, char *msg); -int follower_del(snac *snac, char *actor); -int follower_check(snac *snac, char *actor); +int index_add(const char *fn, const char *md5); +int index_del(const char *fn, const char *md5); +int index_first(const char *fn, char *buf, int size); +d_char *index_list(const char *fn, int max); +d_char *index_list_desc(const char *fn, int max); + +int object_del(const char *id); +int object_del_if_unref(const char *id); + +int follower_add(snac *snac, const char *actor); +int follower_del(snac *snac, const char *actor); +int follower_check(snac *snac, const char *actor); d_char *follower_list(snac *snac); double timeline_mtime(snac *snac); @@ -66,7 +76,8 @@ d_char *timeline_get(snac *snac, char *fn); d_char *timeline_list(snac *snac, int max); int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer); void timeline_admire(snac *snac, char *id, char *admirer, int like); -int timeline_hide(snac *snac, char *id, int hide); + +d_char *timeline_top_level(snac *snac, d_char *list); d_char *local_list(snac *snac, int max); @@ -80,8 +91,11 @@ void mute(snac *snac, char *actor); void unmute(snac *snac, char *actor); int is_muted(snac *snac, char *actor); -int actor_add(snac *snac, char *actor, char *msg); -int actor_get(snac *snac, char *actor, d_char **data); +void hide(snac *snac, const char *id); +int is_hidden(snac *snac, const char *id); + +int actor_add(snac *snac, const char *actor, d_char *msg); +int actor_get(snac *snac, const char *actor, d_char **data); int static_get(snac *snac, const char *id, d_char **data, int *size); void static_put(snac *snac, const char *id, const char *data, int size); @@ -40,6 +40,137 @@ int db_upgrade(d_char **error) nf = 2.1; } + else + if (f < 2.2) { + xs *users = user_list(); + char *p, *v; + + p = users; + while (xs_list_iter(&p, &v)) { + snac snac; + + if (user_open(&snac, v)) { + xs *spec = xs_fmt("%s/actors/" "*.json", snac.basedir); + xs *list = xs_glob(spec, 0, 0); + char *g, *fn; + + g = list; + while (xs_list_iter(&g, &fn)) { + xs *l = xs_split(fn, "/"); + char *b = xs_list_get(l, -1); + xs *dir = xs_fmt("%s/object/%c%c", srv_basedir, b[0], b[1]); + xs *nfn = xs_fmt("%s/%s", dir, b); + + mkdir(dir, 0755); + rename(fn, nfn); + } + + xs *odir = xs_fmt("%s/actors", snac.basedir); + rmdir(odir); + + user_free(&snac); + } + } + + nf = 2.2; + } + else + if (f < 2.3) { + xs *users = user_list(); + char *p, *v; + + p = users; + while (xs_list_iter(&p, &v)) { + snac snac; + + if (user_open(&snac, v)) { + char *p, *v; + xs *dir = xs_fmt("%s/hidden", snac.basedir); + + /* create the hidden directory */ + mkdir(dir, 0755); + + /* rename all muted files incorrectly named .json */ + xs *spec = xs_fmt("%s/muted/" "*.json", snac.basedir); + xs *fns = xs_glob(spec, 0, 0); + + p = fns; + while (xs_list_iter(&p, &v)) { + xs *nfn = xs_replace(v, ".json", ""); + rename(v, nfn); + } + + user_free(&snac); + } + } + + nf = 2.3; + } + else + if (f < 2.4) { + xs *users = user_list(); + char *p, *v; + + p = users; + while (xs_list_iter(&p, &v)) { + snac snac; + + if (user_open(&snac, v)) { + xs *dir = xs_fmt("%s/public", snac.basedir); + mkdir(dir, 0755); + + dir = xs_replace_i(dir, "public", "private"); + mkdir(dir, 0755); + + user_free(&snac); + } + } + + nf = 2.4; + } + else + if (f < 2.5) { + /* upgrade followers */ + xs *users = user_list(); + char *p, *v; + + p = users; + while (xs_list_iter(&p, &v)) { + snac snac; + + if (user_open(&snac, v)) { + xs *spec = xs_fmt("%s/followers/" "*.json", snac.basedir); + xs *dir = xs_glob(spec, 0, 0); + char *p, *v; + + p = dir; + while (xs_list_iter(&p, &v)) { + FILE *f; + + if ((f = fopen(v, "r")) != NULL) { + xs *s = xs_readall(f); + xs *o = xs_json_loads(s); + fclose(f); + + char *type = xs_dict_get(o, "type"); + + if (!xs_is_null(type) && strcmp(type, "Follow") == 0) { + unlink(v); + + char *actor = xs_dict_get(o, "actor"); + + if (!xs_is_null(actor)) + follower_add(&snac, actor); + } + } + } + + user_free(&snac); + } + } + + nf = 2.5; + } if (f < nf) { f = nf; @@ -237,8 +237,9 @@ int adduser(char *uid) } const char *dirs[] = { - "actors", "followers", "following", "local", "muted", - "queue", "static", "timeline", "history", NULL }; + "followers", "following", "local", "muted", "hidden", + "public", "private", "queue", "history", + "static", "timeline", NULL }; int n; for (n = 0; dirs[n]; n++) { @@ -34,6 +34,9 @@ typedef char d_char; /* auto-destroyable strings */ #define xs __attribute__ ((__cleanup__ (_xs_destroy))) d_char +/* not really all, just very much */ +#define XS_ALL 0xfffffff + void *xs_free(void *ptr); void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func); #define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__) @@ -74,7 +77,7 @@ d_char *xs_list_pop(d_char *list, char **data); int xs_list_in(char *list, const char *val); d_char *xs_join(char *list, const char *sep); d_char *xs_split_n(const char *str, const char *sep, int times); -#define xs_split(str, sep) xs_split_n(str, sep, 0xfffffff) +#define xs_split(str, sep) xs_split_n(str, sep, XS_ALL) d_char *xs_dict_new(void); d_char *xs_dict_append_m(d_char *dict, const char *key, const char *mem, int dsz); #define xs_dict_append(dict, key, data) xs_dict_append_m(dict, key, data, xs_size(data)) @@ -5,7 +5,7 @@ #define _XS_GLOB_H d_char *xs_glob_n(const char *spec, int basename, int reverse, int max); -#define xs_glob(spec, basename, reverse) xs_glob_n(spec, basename, reverse, 0xfffffff) +#define xs_glob(spec, basename, reverse) xs_glob_n(spec, basename, reverse, XS_ALL) #ifdef XS_IMPLEMENTATION @@ -79,7 +79,7 @@ d_char *xs_read(FILE *f, int *sz) d_char *xs_readall(FILE *f) /* reads the rest of the file into a string */ { - int size = 0xfffffff; + int size = XS_ALL; return xs_read(f, &size); } @@ -5,9 +5,9 @@ #define _XS_REGEX_H d_char *xs_regex_split_n(const char *str, const char *rx, int count); -#define xs_regex_split(str, rx) xs_regex_split_n(str, rx, 0xfffffff) +#define xs_regex_split(str, rx) xs_regex_split_n(str, rx, XS_ALL) d_char *xs_regex_match_n(const char *str, const char *rx, int count); -#define xs_regex_match(str, rx) xs_regex_match_n(str, rx, 0xfffffff) +#define xs_regex_match(str, rx) xs_regex_match_n(str, rx, XS_ALL) #ifdef XS_IMPLEMENTATION @@ -12,6 +12,7 @@ typedef struct _xs_set { } xs_set; void xs_set_init(xs_set *s); +d_char *xs_set_result(xs_set *s); void xs_set_free(xs_set *s); int xs_set_add(xs_set *s, const char *data); @@ -32,11 +33,21 @@ void xs_set_init(xs_set *s) } -void xs_set_free(xs_set *s) -/* frees a set */ +d_char *xs_set_result(xs_set *s) +/* returns the set as a list and frees it */ { + d_char *list = s->list; + s->list = NULL; s->hash = xs_free(s->hash); - s->list = xs_free(s->list); + + return list; +} + + +void xs_set_free(xs_set *s) +/* frees a set, dropping the list */ +{ + free(xs_set_result(s)); } diff --git a/xs_version.h b/xs_version.h index ac5d43f..baefcba 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* a78beb97d364ff31cbaa504e275118afeaea7a59 */ +/* c18371e1f1d3de0f872354f93024a736caebea4d */ |