summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--RELEASE_NOTES.md8
-rw-r--r--TODO.md8
-rw-r--r--activitypub.c42
-rw-r--r--data.c778
-rw-r--r--doc/snac.15
-rw-r--r--doc/snac.826
-rw-r--r--html.c20
-rw-r--r--httpd.c2
-rw-r--r--main.c27
-rw-r--r--snac.h30
-rw-r--r--upgrade.c131
-rw-r--r--utils.c5
-rw-r--r--xs.h5
-rw-r--r--xs_glob.h2
-rw-r--r--xs_io.h2
-rw-r--r--xs_regex.h4
-rw-r--r--xs_set.h17
-rw-r--r--xs_version.h2
19 files changed, 888 insertions, 227 deletions
diff --git a/README.md b/README.md
index 6f5d5cd..1b7dc50 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ A simple, minimalistic ActivityPub instance
7- Lightweight, minimal dependencies 7- Lightweight, minimal dependencies
8- 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... 8- 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...
9- Simple but effective web interface 9- Simple but effective web interface
10- Multiuser
10- Easily-accessed MUTE button to silence morons 11- Easily-accessed MUTE button to silence morons
11- Tested interoperability with related software 12- Tested interoperability with related software
12- No database needed 13- 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 @@
1# Release Notes 1# Release Notes
2 2
3## 2.13
4
5A 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.
6
7Fixed HTML loose close tag.
8
9Fixed bug when closing sendmail pipe.
10
3## 2.12 11## 2.12
4 12
5Fixed some bugs triggered when a GET query does not have an `Accept:` header. 13Fixed some bugs triggered when a GET query does not have an `Accept:` header.
diff --git a/TODO.md b/TODO.md
index c80387d..1145f88 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,7 +2,11 @@
2 2
3## Open 3## Open
4 4
5Dropping on input those messages that have their parent hidden is not a good idea, as children of *these* dropped messages will pass unharmed. 5Add an ?skip=NNN parameter to the admin page, to see older timeline.
6
7Add support for Edit + Note.
8
9Add domain/subdomain flexibility according to https://codeberg.org/grunfink/snac2/issues/3
6 10
7Add 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). 11Add 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).
8 12
@@ -177,3 +181,5 @@ Add a purge timeout also for the local timeline (2022-11-12T08:32:56+0100).
177Add a switch for sensitive posts (2022-11-16T12:17:50+0100). 181Add a switch for sensitive posts (2022-11-16T12:17:50+0100).
178 182
179Add an RSS to the local timeline (2022-11-18T11:43:54+0100). 183Add an RSS to the local timeline (2022-11-18T11:43:54+0100).
184
185Dropping 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 @@
9#include "xs_openssl.h" 9#include "xs_openssl.h"
10#include "xs_regex.h" 10#include "xs_regex.h"
11#include "xs_time.h" 11#include "xs_time.h"
12#include "xs_set.h"
12 13
13#include "snac.h" 14#include "snac.h"
14 15
@@ -169,11 +170,13 @@ int send_to_actor(snac *snac, char *actor, char *msg, d_char **payload, int *p_s
169d_char *recipient_list(snac *snac, char *msg, int expand_public) 170d_char *recipient_list(snac *snac, char *msg, int expand_public)
170/* returns the list of recipients for a message */ 171/* returns the list of recipients for a message */
171{ 172{
172 d_char *list = xs_list_new();
173 char *to = xs_dict_get(msg, "to"); 173 char *to = xs_dict_get(msg, "to");
174 char *cc = xs_dict_get(msg, "cc"); 174 char *cc = xs_dict_get(msg, "cc");
175 xs_set rcpts;
175 int n; 176 int n;
176 177
178 xs_set_init(&rcpts);
179
177 char *lists[] = { to, cc, NULL }; 180 char *lists[] = { to, cc, NULL };
178 for (n = 0; lists[n]; n++) { 181 for (n = 0; lists[n]; n++) {
179 char *l = lists[n]; 182 char *l = lists[n];
@@ -192,45 +195,41 @@ d_char *recipient_list(snac *snac, char *msg, int expand_public)
192 if (expand_public && strcmp(v, public_address) == 0) { 195 if (expand_public && strcmp(v, public_address) == 0) {
193 /* iterate the followers and add them */ 196 /* iterate the followers and add them */
194 xs *fwers = follower_list(snac); 197 xs *fwers = follower_list(snac);
195 char *fw; 198 char *actor;
196 199
197 char *p = fwers; 200 char *p = fwers;
198 while (xs_list_iter(&p, &fw)) { 201 while (xs_list_iter(&p, &actor))
199 char *actor = xs_dict_get(fw, "actor"); 202 xs_set_add(&rcpts, actor);
200
201 if (xs_list_in(list, actor) == -1)
202 list = xs_list_append(list, actor);
203 }
204 } 203 }
205 else 204 else
206 if (xs_list_in(list, v) == -1) 205 xs_set_add(&rcpts, v);
207 list = xs_list_append(list, v);
208 } 206 }
209 } 207 }
210 208
211 return list; 209 return xs_set_result(&rcpts);
212} 210}
213 211
214 212
215d_char *inbox_list(snac *snac, char *msg) 213d_char *inbox_list(snac *snac, char *msg)
216/* returns the list of inboxes that are recipients of this message */ 214/* returns the list of inboxes that are recipients of this message */
217{ 215{
218 d_char *list = xs_list_new(); 216 xs *rcpts = recipient_list(snac, msg, 1);
219 xs *rcpts = recipient_list(snac, msg, 1); 217 xs_set inboxes;
220 char *p, *v; 218 char *p, *v;
221 219
220 xs_set_init(&inboxes);
221
222 p = rcpts; 222 p = rcpts;
223 while (xs_list_iter(&p, &v)) { 223 while (xs_list_iter(&p, &v)) {
224 xs *inbox; 224 xs *inbox;
225 225
226 if ((inbox = get_actor_inbox(snac, v)) != NULL) { 226 if ((inbox = get_actor_inbox(snac, v)) != NULL) {
227 /* add the inbox if it's not already there */ 227 /* add the inbox if it's not already there */
228 if (xs_list_in(list, inbox) == -1) 228 xs_set_add(&inboxes, inbox);
229 list = xs_list_append(list, inbox);
230 } 229 }
231 } 230 }
232 231
233 return list; 232 return xs_set_result(&inboxes);
234} 233}
235 234
236 235
@@ -824,7 +823,7 @@ int process_message(snac *snac, char *msg, char *req)
824 823
825 timeline_add(snac, xs_dict_get(f_msg, "id"), f_msg, NULL, NULL); 824 timeline_add(snac, xs_dict_get(f_msg, "id"), f_msg, NULL, NULL);
826 825
827 follower_add(snac, actor, f_msg); 826 follower_add(snac, actor);
828 827
829 snac_log(snac, xs_fmt("New follower %s", actor)); 828 snac_log(snac, xs_fmt("New follower %s", actor));
830 do_notify = 1; 829 do_notify = 1;
@@ -918,6 +917,7 @@ int process_message(snac *snac, char *msg, char *req)
918 if (strcmp(type, "Update") == 0) { 917 if (strcmp(type, "Update") == 0) {
919 if (strcmp(utype, "Person") == 0) { 918 if (strcmp(utype, "Person") == 0) {
920 actor_add(snac, actor, xs_dict_get(msg, "object")); 919 actor_add(snac, actor, xs_dict_get(msg, "object"));
920
921 snac_log(snac, xs_fmt("updated actor %s", actor)); 921 snac_log(snac, xs_fmt("updated actor %s", actor));
922 } 922 }
923 else 923 else
@@ -1017,10 +1017,10 @@ void process_queue(snac *snac)
1017 FILE *f; 1017 FILE *f;
1018 int ok = 0; 1018 int ok = 0;
1019 1019
1020 f = popen("/usr/sbin/sendmail -t", "w"); 1020 if ((f = popen("/usr/sbin/sendmail -t", "w")) != NULL) {
1021 if (f) {
1022 fprintf(f, "%s\n", msg); 1021 fprintf(f, "%s\n", msg);
1023 if (pclose(f) != EOF) //this is a pipe stream not just a file 1022
1023 if (pclose(f) != -1)
1024 ok = 1; 1024 ok = 1;
1025 } 1025 }
1026 1026
@@ -1089,7 +1089,7 @@ int activitypub_get_handler(d_char *req, char *q_path,
1089 if (p_path == NULL) { 1089 if (p_path == NULL) {
1090 /* if there was no component after the user, it's an actor request */ 1090 /* if there was no component after the user, it's an actor request */
1091 msg = msg_actor(&snac); 1091 msg = msg_actor(&snac);
1092 *ctype = "application/ld+json"; 1092 *ctype = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
1093 } 1093 }
1094 else 1094 else
1095 if (strcmp(p_path, "outbox") == 0) { 1095 if (strcmp(p_path, "outbox") == 0) {
diff --git a/data.c b/data.c
index 49f215b..51ee2c7 100644
--- a/data.c
+++ b/data.c
@@ -6,6 +6,7 @@
6#include "xs_json.h" 6#include "xs_json.h"
7#include "xs_openssl.h" 7#include "xs_openssl.h"
8#include "xs_glob.h" 8#include "xs_glob.h"
9#include "xs_set.h"
9 10
10#include "snac.h" 11#include "snac.h"
11 12
@@ -14,12 +15,12 @@
14#include <sys/file.h> 15#include <sys/file.h>
15#include <fcntl.h> 16#include <fcntl.h>
16 17
17double db_layout = 2.1; 18double db_layout = 2.5;
18 19
19 20
20int db_upgrade(d_char **error); 21int db_upgrade(d_char **error);
21 22
22int srv_open(char *basedir) 23int srv_open(char *basedir, int auto_upgrade)
23/* opens a server */ 24/* opens a server */
24{ 25{
25 int ret = 0; 26 int ret = 0;
@@ -69,7 +70,14 @@ int srv_open(char *basedir)
69 error = xs_fmt("DEBUG level set to %d from environment", dbglevel); 70 error = xs_fmt("DEBUG level set to %d from environment", dbglevel);
70 } 71 }
71 72
72 ret = db_upgrade(&error); 73 if (auto_upgrade)
74 ret = db_upgrade(&error);
75 else {
76 if (xs_number_get(xs_dict_get(srv_config, "layout")) < db_layout)
77 error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
78 else
79 ret = 1;
80 }
73 } 81 }
74 82
75 } 83 }
@@ -178,24 +186,215 @@ d_char *user_list(void)
178} 186}
179 187
180 188
181double mtime(char *fn) 189double mtime_nl(const char *fn, int *n_link)
182/* returns the mtime of a file or directory, or 0.0 */ 190/* returns the mtime and number of links of a file or directory, or 0.0 */
183{ 191{
184 struct stat st; 192 struct stat st;
185 double r = 0.0; 193 double r = 0.0;
194 int n = 0;
186 195
187 if (fn && stat(fn, &st) != -1) 196 if (fn && stat(fn, &st) != -1) {
188 r = (double)st.st_mtim.tv_sec; 197 r = (double) st.st_mtim.tv_sec;
198 n = st.st_nlink;
199 }
200
201 if (n_link)
202 *n_link = n;
189 203
190 return r; 204 return r;
191} 205}
192 206
193 207
194/** object database 2.1+ **/ 208/** database 2.1+ **/
209
210/** indexes **/
211
212int index_add_md5(const char *fn, const char *md5)
213/* adds an md5 to an index */
214{
215 int status = 201; /* Created */
216 FILE *f;
217
218 if ((f = fopen(fn, "a")) != NULL) {
219 flock(fileno(f), LOCK_EX);
220
221 /* ensure the position is at the end after getting the lock */
222 fseek(f, 0, SEEK_END);
223
224 fprintf(f, "%s\n", md5);
225 fclose(f);
226 }
227 else
228 status = 500;
229
230 return status;
231}
232
233
234int index_add(const char *fn, const char *id)
235/* adds an id to an index */
236{
237 xs *md5 = xs_md5_hex(id, strlen(id));
238 return index_add_md5(fn, md5);
239}
240
241
242int index_del(const char *fn, const char *md5)
243/* deletes an md5 from an index */
244{
245 int status = 404;
246 FILE *i, *o;
247
248 if ((i = fopen(fn, "r")) != NULL) {
249 flock(fileno(i), LOCK_EX);
250
251 xs *nfn = xs_fmt("%s.new", fn);
252 char line[256];
253
254 if ((o = fopen(nfn, "w")) != NULL) {
255 while (fgets(line, sizeof(line), i) != NULL) {
256 line[32] = '\0';
257 if (memcmp(line, md5, 32) != 0)
258 fprintf(o, "%s\n", line);
259 }
260
261 fclose(o);
262
263 xs *ofn = xs_fmt("%s.bak", fn);
264
265 link(fn, ofn);
266 rename(nfn, fn);
267 }
268 else
269 status = 500;
270
271 fclose(i);
272 }
273 else
274 status = 500;
275
276 return status;
277}
278
279
280int index_in_md5(const char *fn, const char *md5)
281/* checks if the md5 is already in the index */
282{
283 FILE *f;
284 int ret = 0;
285
286 if ((f = fopen(fn, "r")) != NULL) {
287 flock(fileno(f), LOCK_SH);
288
289 char line[256];
290
291 while (!ret && fgets(line, sizeof(line), f) != NULL) {
292 line[32] = '\0';
293
294 if (strcmp(line, md5) == 0)
295 ret = 1;
296 }
297
298 fclose(f);
299 }
300
301 return ret;
302}
303
304
305int index_in(const char *fn, const char *id)
306/* checks if the object id is already in the index */
307{
308 xs *md5 = xs_md5_hex(id, strlen(id));
309 return index_in_md5(fn, md5);
310}
311
312
313int index_first(const char *fn, char *line, int size)
314/* reads the first entry of an index */
315{
316 FILE *f;
317 int ret = 0;
318
319 if ((f = fopen(fn, "r")) != NULL) {
320 flock(fileno(f), LOCK_SH);
321
322 if (fgets(line, size, f) != NULL) {
323 line[32] = '\0';
324 ret = 1;
325 }
326
327 fclose(f);
328 }
329
330 return ret;
331}
332
333
334d_char *index_list(const char *fn, int max)
335/* returns an index as a list */
336{
337 d_char *list = NULL;
338 FILE *f;
339 int n = 0;
340
341 if ((f = fopen(fn, "r")) != NULL) {
342 flock(fileno(f), LOCK_SH);
343
344 char line[256];
345 list = xs_list_new();
346
347 while (n < max && fgets(line, sizeof(line), f) != NULL) {
348 line[32] = '\0';
349 list = xs_list_append(list, line);
350 n++;
351 }
352
353 fclose(f);
354 }
355
356 return list;
357}
358
359
360d_char *index_list_desc(const char *fn, int max)
361/* returns an index as a list, in reverse order */
362{
363 d_char *list = NULL;
364 FILE *f;
365 int n = 0;
366
367 if ((f = fopen(fn, "r")) != NULL) {
368 flock(fileno(f), LOCK_SH);
369
370 char line[256];
371 list = xs_list_new();
372
373 /* move to the end minus one entry */
374 if (!fseek(f, 0, SEEK_END) && !fseek(f, -33, SEEK_CUR)) {
375 while (n < max && fgets(line, sizeof(line), f) != NULL) {
376 line[32] = '\0';
377 list = xs_list_append(list, line);
378 n++;
379
380 /* move backwards 2 entries */
381 if (fseek(f, -66, SEEK_CUR) == -1)
382 break;
383 }
384 }
385
386 fclose(f);
387 }
388
389 return list;
390}
391
392
393/** objects **/
195 394
196d_char *_object_fn_by_md5(const char *md5) 395d_char *_object_fn_by_md5(const char *md5)
197{ 396{
198 xs *bfn = xs_fmt("%s/object/%c%c/", srv_basedir, md5[0], md5[1]); 397 xs *bfn = xs_fmt("%s/object/%c%c", srv_basedir, md5[0], md5[1]);
199 398
200 mkdir(bfn, 0755); 399 mkdir(bfn, 0755);
201 400
@@ -203,19 +402,18 @@ d_char *_object_fn_by_md5(const char *md5)
203} 402}
204 403
205 404
206d_char *_object_fn_by_id(const char *id) 405d_char *_object_fn(const char *id)
207{ 406{
208 xs *md5 = xs_md5_hex(id, strlen(id)); 407 xs *md5 = xs_md5_hex(id, strlen(id));
209
210 return _object_fn_by_md5(md5); 408 return _object_fn_by_md5(md5);
211} 409}
212 410
213 411
214int object_get(const char *id, d_char **obj, const char *type) 412int object_get_by_md5(const char *md5, d_char **obj, const char *type)
215/* returns a loaded object, optionally of the requested type */ 413/* returns a stored object, optionally of the requested type */
216{ 414{
217 int status = 404; 415 int status = 404;
218 xs *fn = _object_fn_by_id(id); 416 xs *fn = _object_fn_by_md5(md5);
219 FILE *f; 417 FILE *f;
220 418
221 if ((f = fopen(fn, "r")) != NULL) { 419 if ((f = fopen(fn, "r")) != NULL) {
@@ -247,13 +445,27 @@ int object_get(const char *id, d_char **obj, const char *type)
247} 445}
248 446
249 447
250int object_add(const char *id, d_char *obj) 448int object_get(const char *id, d_char **obj, const char *type)
449/* returns a stored object, optionally of the requested type */
450{
451 xs *md5 = xs_md5_hex(id, strlen(id));
452 return object_get_by_md5(md5, obj, type);
453}
454
455
456int _object_add(const char *id, d_char *obj, int ow)
251/* stores an object */ 457/* stores an object */
252{ 458{
253 int status = 201; /* Created */ 459 int status = 201; /* Created */
254 xs *fn = _object_fn_by_id(id); 460 xs *fn = _object_fn(id);
255 FILE *f; 461 FILE *f;
256 462
463 if (!ow && mtime(fn) > 0.0) {
464 /* object already here */
465 srv_debug(0, xs_fmt("object_add object already here %s", id));
466 return 204; /* No content */
467 }
468
257 if ((f = fopen(fn, "w")) != NULL) { 469 if ((f = fopen(fn, "w")) != NULL) {
258 flock(fileno(f), LOCK_EX); 470 flock(fileno(f), LOCK_EX);
259 471
@@ -261,100 +473,241 @@ int object_add(const char *id, d_char *obj)
261 473
262 fwrite(j, strlen(j), 1, f); 474 fwrite(j, strlen(j), 1, f);
263 fclose(f); 475 fclose(f);
476
477 /* does this object has a parent? */
478 char *in_reply_to = xs_dict_get(obj, "inReplyTo");
479
480 if (!xs_is_null(in_reply_to) && *in_reply_to) {
481 /* update the children index of the parent */
482 xs *c_idx = _object_fn(in_reply_to);
483
484 c_idx = xs_replace_i(c_idx, ".json", "_c.idx");
485 index_add(c_idx, id);
486
487 srv_debug(0, xs_fmt("object_add added child %s to %s", id, c_idx));
488
489 /* create a one-element index with the parent */
490 xs *p_idx = xs_replace(fn, ".json", "_p.idx");
491 index_add(p_idx, in_reply_to);
492
493 srv_debug(0, xs_fmt("object_add added parent %s to %s", in_reply_to, p_idx));
494 }
264 } 495 }
265 else 496 else
266 status = 500; 497 status = 500;
267 498
499 srv_debug(0, xs_fmt("object_add %s %s %d", id, fn, status));
500
268 return status; 501 return status;
269} 502}
270 503
271 504
272d_char *_follower_fn(snac *snac, char *actor) 505int object_add(const char *id, d_char *obj)
506/* stores an object */
273{ 507{
274 xs *md5 = xs_md5_hex(actor, strlen(actor)); 508 return _object_add(id, obj, 0);
275 return xs_fmt("%s/followers/%s.json", snac->basedir, md5);
276} 509}
277 510
278 511
279int follower_add(snac *snac, char *actor, char *msg) 512int object_add_ow(const char *id, d_char *obj)
280/* adds a follower */ 513/* stores an object (overwriting allowed) */
281{ 514{
282 int ret = 201; /* created */ 515 return _object_add(id, obj, 1);
283 xs *fn = _follower_fn(snac, actor); 516}
284 FILE *f;
285 517
286 if ((f = fopen(fn, "w")) != NULL) {
287 xs *j = xs_json_dumps_pp(msg, 4);
288 518
289 fwrite(j, 1, strlen(j), f); 519int object_del_by_md5(const char *md5)
290 fclose(f); 520/* deletes an object by its md5 */
521{
522 int status = 404;
523 xs *fn = _object_fn_by_md5(md5);
524
525 if (fn != NULL && unlink(fn) != -1) {
526 status = 200;
527
528 /* also delete associated indexes */
529 xs *spec = xs_dup(fn);
530 spec = xs_replace_i(spec, ".json", "*.idx");
531 xs *files = xs_glob(spec, 0, 0);
532 char *p, *v;
533
534 p = files;
535 while (xs_list_iter(&p, &v)) {
536 srv_debug(0, xs_fmt("object_del index %s", v));
537 unlink(v);
538 }
291 } 539 }
292 else
293 ret = 500;
294 540
295 snac_debug(snac, 2, xs_fmt("follower_add %s %s", actor, fn)); 541 srv_debug(0, xs_fmt("object_del %s %d", fn, status));
542
543 return status;
544}
545
546
547int object_del(const char *id)
548/* deletes an object */
549{
550 xs *md5 = xs_md5_hex(id, strlen(id));
551 return object_del_by_md5(md5);
552}
553
554
555int object_del_if_unref(const char *id)
556/* deletes an object if its n_links < 2 */
557{
558 xs *fn = _object_fn(id);
559 int n_links;
560 int ret = 0;
561
562 if (mtime_nl(fn, &n_links) > 0.0 && n_links < 2)
563 ret = object_del(id);
296 564
297 return ret; 565 return ret;
298} 566}
299 567
300 568
301int follower_del(snac *snac, char *actor) 569d_char *object_children(const char *id)
302/* deletes a follower */ 570/* returns the list of an object's children */
571{
572 xs *fn = _object_fn(id);
573
574 fn = xs_replace_i(fn, ".json", "_c.idx");
575
576 return index_list(fn, XS_ALL);
577}
578
579
580int object_admire(const char *id, const char *actor, int like)
581/* actor likes or announces this object */
303{ 582{
304 int status = 200; 583 int status = 200;
305 xs *fn = _follower_fn(snac, actor); 584 xs *fn = _object_fn(id);
306 585
307 if (fn != NULL) 586 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
308 unlink(fn);
309 else
310 status = 404;
311 587
312 snac_debug(snac, 2, xs_fmt("follower_del %s %s", actor, fn)); 588 if (!index_in(fn, actor)) {
589 status = index_add(fn, actor);
590
591 srv_debug(0, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn));
592 }
313 593
314 return status; 594 return status;
315} 595}
316 596
317 597
318int follower_check(snac *snac, char *actor) 598int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
319/* checks if someone is a follower */ 599/* adds or deletes from a user cache */
320{ 600{
321 xs *fn = _follower_fn(snac, actor); 601 xs *ofn = _object_fn(id);
602 xs *l = xs_split(ofn, "/");
603 xs *cfn = xs_fmt("%s/%s/%s", snac->basedir, cachedir, xs_list_get(l, -1));
604 xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir);
605 int ret;
606
607 if (del) {
608 if ((ret = unlink(cfn)) != -1)
609 index_del(idx, id);
610 }
611 else {
612 index_add(idx, id);
613 ret = link(ofn, cfn);
614 }
322 615
323 return !!(mtime(fn) != 0.0); 616 return ret;
617}
618
619
620int object_user_cache_add(snac *snac, const char *id, const char *cachedir)
621/* caches an object into a user cache */
622{
623 return _object_user_cache(snac, id, cachedir, 0);
624}
625
626
627int object_user_cache_del(snac *snac, const char *id, const char *cachedir)
628/* deletes an object from a user cache */
629{
630 return _object_user_cache(snac, id, cachedir, 1);
631}
632
633
634int object_user_cache_in(snac *snac, const char *id, const char *cachedir)
635/* checks if an object is stored in a cache */
636{
637 xs *md5 = xs_md5_hex(id, strlen(id));
638 xs *cfn = xs_fmt("%s/%s/%s.json", snac->basedir, cachedir, md5);
639
640 return !!(mtime(cfn) != 0.0);
641}
642
643
644d_char *object_user_cache_list(snac *snac, const char *cachedir, int max)
645/* returns the objects in a cache as a list */
646{
647 xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir);
648 return index_list(idx, max);
649}
650
651
652/** specialized functions **/
653
654/** followers **/
655
656int follower_add(snac *snac, const char *actor)
657/* adds a follower */
658{
659 int ret = object_user_cache_add(snac, actor, "followers");
660
661 snac_debug(snac, 2, xs_fmt("follower_add %s %s", actor));
662
663 return ret == -1 ? 500 : 200;
664}
665
666
667int follower_del(snac *snac, const char *actor)
668/* deletes a follower */
669{
670 int ret = object_user_cache_del(snac, actor, "followers");
671
672 snac_debug(snac, 2, xs_fmt("follower_del %s %s", actor));
673
674 return ret == -1 ? 404 : 200;
675}
676
677
678int follower_check(snac *snac, const char *actor)
679/* checks if someone is a follower */
680{
681 return object_user_cache_in(snac, actor, "followers");
324} 682}
325 683
326 684
327d_char *follower_list(snac *snac) 685d_char *follower_list(snac *snac)
328/* returns the list of followers */ 686/* returns the list of followers */
329{ 687{
330 xs *spec = xs_fmt("%s/followers/" "*.json", snac->basedir); 688 xs *list = object_user_cache_list(snac, "followers", XS_ALL);
331 xs *glist = xs_glob(spec, 0, 0); 689 d_char *fwers = xs_list_new();
332 char *p, *v; 690 char *p, *v;
333 d_char *list = xs_list_new();
334 691
335 /* iterate the list of files */ 692 /* resolve the list of md5 to be a list of actors */
336 p = glist; 693 p = list;
337 while (xs_list_iter(&p, &v)) { 694 while (xs_list_iter(&p, &v)) {
338 FILE *f; 695 xs *a_obj = NULL;
339
340 /* load the follower data */
341 if ((f = fopen(v, "r")) != NULL) {
342 xs *j = xs_readall(f);
343 fclose(f);
344 696
345 if (j != NULL) { 697 if (valid_status(object_get_by_md5(v, &a_obj, NULL))) {
346 xs *o = xs_json_loads(j); 698 char *actor = xs_dict_get(a_obj, "id");
347 699
348 if (o != NULL) 700 if (!xs_is_null(actor))
349 list = xs_list_append(list, o); 701 fwers = xs_list_append(fwers, actor);
350 }
351 } 702 }
352 } 703 }
353 704
354 return list; 705 return fwers;
355} 706}
356 707
357 708
709/** timeline **/
710
358double timeline_mtime(snac *snac) 711double timeline_mtime(snac *snac)
359{ 712{
360 xs *fn = xs_fmt("%s/timeline", snac->basedir); 713 xs *fn = xs_fmt("%s/timeline", snac->basedir);
@@ -437,6 +790,13 @@ int timeline_del(snac *snac, char *id)
437 ret = 200; 790 ret = 200;
438 } 791 }
439 792
793 /* delete from the user's caches */
794 object_user_cache_del(snac, id, "public");
795 object_user_cache_del(snac, id, "private");
796
797 /* try to delete the object if it's not used elsewhere */
798 object_del_if_unref(id);
799
440 return ret; 800 return ret;
441} 801}
442 802
@@ -511,22 +871,11 @@ int _timeline_write(snac *snac, char *id, char *msg, char *parent, char *referre
511 871
512 if (pfn != NULL && (f = fopen(pfn, "r")) != NULL) { 872 if (pfn != NULL && (f = fopen(pfn, "r")) != NULL) {
513 xs *j; 873 xs *j;
514 char *v;
515 874
516 j = xs_readall(f); 875 j = xs_readall(f);
517 fclose(f); 876 fclose(f);
518 877
519 p_msg = xs_json_loads(j); 878 p_msg = xs_json_loads(j);
520
521 if ((v = xs_dict_get(p_msg, "_snac")) != NULL) {
522 /* is parent hidden? */
523 if ((v = xs_dict_get(v, "hidden")) && xs_type(v) == XSTYPE_TRUE) {
524 snac_debug(snac, 1,
525 xs_fmt("_timeline_write dropping due to hidden parent %s (%s)", id, parent));
526
527 return 0;
528 }
529 }
530 } 879 }
531 } 880 }
532 881
@@ -648,6 +997,16 @@ int _timeline_write(snac *snac, char *id, char *msg, char *parent, char *referre
648} 997}
649 998
650 999
1000void timeline_update_indexes(snac *snac, const char *id)
1001/* updates the indexes */
1002{
1003 object_user_cache_add(snac, id, "private");
1004
1005 if (xs_startswith(id, snac->actor))
1006 object_user_cache_add(snac, id, "public");
1007}
1008
1009
651int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer) 1010int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer)
652/* adds a message to the timeline */ 1011/* adds a message to the timeline */
653{ 1012{
@@ -680,13 +1039,61 @@ int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer
680 1039
681 msg = xs_dict_set(msg, "_snac", md); 1040 msg = xs_dict_set(msg, "_snac", md);
682 1041
683 if ((ret = _timeline_write(snac, id, msg, parent, referrer))) 1042 if ((ret = _timeline_write(snac, id, msg, parent, referrer))) {
684 snac_debug(snac, 1, xs_fmt("timeline_add %s", id)); 1043 snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
685 1044
1045 object_add(id, o_msg);
1046 timeline_update_indexes(snac, id);
1047 }
1048
686 return ret; 1049 return ret;
687} 1050}
688 1051
689 1052
1053d_char *timeline_top_level(snac *snac, d_char *list)
1054/* returns the top level md5 entries from this index */
1055{
1056 d_char *tl = xs_list_new();
1057 xs_set seen;
1058 char *p, *v;
1059
1060 xs_set_init(&seen);
1061
1062 p = list;
1063 while (xs_list_iter(&p, &v)) {
1064 char line[256] = "";
1065
1066 strcpy(line, v);
1067
1068 for (;;) {
1069 char line2[256];
1070 xs *fn = _object_fn_by_md5(line);
1071 fn = xs_replace_i(fn, ".json", "_p.idx");
1072
1073 /* if it doesn't have a parent, use this */
1074 if (index_first(fn, line2, sizeof(line2)) == 0)
1075 break;
1076
1077 xs *pfn = _object_fn_by_md5(line2);
1078
1079 /* well, there is a parent... but if it's not there, use this */
1080 if (mtime(pfn) == 0.0)
1081 break;
1082
1083 /* it's here! try again with its own parent */
1084 strcpy(line, line2);
1085 }
1086
1087 if (xs_set_add(&seen, line) == 1)
1088 tl = xs_list_append(tl, line);
1089 }
1090
1091 xs_set_free(&seen);
1092
1093 return tl;
1094}
1095
1096
690void timeline_admire(snac *snac, char *id, char *admirer, int like) 1097void timeline_admire(snac *snac, char *id, char *admirer, int like)
691/* updates a timeline entry with a new admiration */ 1098/* updates a timeline entry with a new admiration */
692{ 1099{
@@ -734,52 +1141,16 @@ void timeline_admire(snac *snac, char *id, char *admirer, int like)
734 } 1141 }
735 else 1142 else
736 snac_log(snac, xs_fmt("timeline_admire ignored for unknown object %s", id)); 1143 snac_log(snac, xs_fmt("timeline_admire ignored for unknown object %s", id));
737}
738
739
740int timeline_hide(snac *snac, char *id, int hide)
741/* hides/unhides a timeline entry */
742{
743 int ret = 0;
744 xs *fn = _timeline_find_fn(snac, id);
745 FILE *f;
746
747 if (fn != NULL && (f = fopen(fn, "r")) != NULL) {
748 xs *s1 = xs_readall(f);
749 xs *msg = xs_json_loads(s1);
750 xs *meta = xs_dup(xs_dict_get(msg, "_snac"));
751 xs *hdn = xs_val_new(hide ? XSTYPE_TRUE : XSTYPE_FALSE);
752 char *p, *v;
753
754 fclose(f);
755
756 /* if it's already in this hidden state, we're done */
757 if ((v = xs_dict_get(meta, "hidden")) && xs_type(v) == xs_type(hdn))
758 return ret;
759
760 meta = xs_dict_set(meta, "hidden", hdn);
761 msg = xs_dict_set(msg, "_snac", meta);
762
763 if ((f = fopen(fn, "w")) != NULL) {
764 xs *j1 = xs_json_dumps_pp(msg, 4);
765
766 fwrite(j1, strlen(j1), 1, f);
767 fclose(f);
768 1144
769 snac_debug(snac, 1, xs_fmt("timeline_hide %d %s", hide, id)); 1145 object_admire(id, admirer, like);
1146}
770 1147
771 /* now hide the children */
772 p = xs_dict_get(meta, "children");
773 while (xs_list_iter(&p, &v))
774 timeline_hide(snac, v, hide);
775 1148
776 ret = 1; 1149/** following **/
777 }
778 }
779
780 return ret;
781}
782 1150
1151/* this needs special treatment and cannot use the object db as is,
1152 with a link to a cached author, because we need the Follow object
1153 in case we need to unfollow (Undo + original Follow) */
783 1154
784d_char *_following_fn(snac *snac, char *actor) 1155d_char *_following_fn(snac *snac, char *actor)
785{ 1156{
@@ -811,7 +1182,7 @@ int following_add(snac *snac, char *actor, char *msg)
811 1182
812 1183
813int following_del(snac *snac, char *actor) 1184int following_del(snac *snac, char *actor)
814/* someone is no longer following us */ 1185/* we're not following this actor any longer */
815{ 1186{
816 xs *fn = _following_fn(snac, actor); 1187 xs *fn = _following_fn(snac, actor);
817 1188
@@ -824,7 +1195,7 @@ int following_del(snac *snac, char *actor)
824 1195
825 1196
826int following_check(snac *snac, char *actor) 1197int following_check(snac *snac, char *actor)
827/* checks if someone is following us */ 1198/* checks if we are following this actor */
828{ 1199{
829 xs *fn = _following_fn(snac, actor); 1200 xs *fn = _following_fn(snac, actor);
830 1201
@@ -877,8 +1248,12 @@ d_char *following_list(snac *snac)
877 if (o != NULL) { 1248 if (o != NULL) {
878 char *type = xs_dict_get(o, "type"); 1249 char *type = xs_dict_get(o, "type");
879 1250
880 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) 1251 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
881 list = xs_list_append(list, o); 1252 char *actor = xs_dict_get(o, "actor");
1253
1254 if (!xs_is_null(actor))
1255 list = xs_list_append(list, actor);
1256 }
882 } 1257 }
883 } 1258 }
884 } 1259 }
@@ -891,7 +1266,7 @@ d_char *following_list(snac *snac)
891d_char *_muted_fn(snac *snac, char *actor) 1266d_char *_muted_fn(snac *snac, char *actor)
892{ 1267{
893 xs *md5 = xs_md5_hex(actor, strlen(actor)); 1268 xs *md5 = xs_md5_hex(actor, strlen(actor));
894 return xs_fmt("%s/muted/%s.json", snac->basedir, md5); 1269 return xs_fmt("%s/muted/%s", snac->basedir, md5);
895} 1270}
896 1271
897 1272
@@ -930,65 +1305,89 @@ int is_muted(snac *snac, char *actor)
930} 1305}
931 1306
932 1307
933d_char *_actor_fn(snac *snac, char *actor) 1308d_char *_hidden_fn(snac *snac, const char *id)
934/* returns the file name for an actor */
935{ 1309{
936 xs *md5 = xs_md5_hex(actor, strlen(actor)); 1310 xs *md5 = xs_md5_hex(id, strlen(id));
937 return xs_fmt("%s/actors/%s.json", snac->basedir, md5); 1311 return xs_fmt("%s/hidden/%s", snac->basedir, md5);
938} 1312}
939 1313
940 1314
941int actor_add(snac *snac, char *actor, char *msg) 1315void hide(snac *snac, const char *id)
942/* adds an actor */ 1316/* hides a message tree */
943{ 1317{
944 int ret = 201; /* created */ 1318 xs *fn = _hidden_fn(snac, id);
945 xs *fn = _actor_fn(snac, actor);
946 FILE *f; 1319 FILE *f;
947 1320
948 if ((f = fopen(fn, "w")) != NULL) { 1321 if ((f = fopen(fn, "w")) != NULL) {
949 xs *j = xs_json_dumps_pp(msg, 4); 1322 fprintf(f, "%s\n", id);
950
951 fwrite(j, 1, strlen(j), f);
952 fclose(f); 1323 fclose(f);
1324
1325 snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
1326
1327 /* hide all the children */
1328 xs *chld = object_children(id);
1329 char *p, *v;
1330
1331 p = chld;
1332 while (xs_list_iter(&p, &v)) {
1333 xs *co = NULL;
1334
1335 /* resolve to get the id */
1336 if (valid_status(object_get_by_md5(v, &co, NULL))) {
1337 if ((v = xs_dict_get(co, "id")) != NULL)
1338 hide(snac, v);
1339 }
1340 }
953 } 1341 }
954 else 1342}
955 ret = 500;
956 1343
957 snac_debug(snac, 2, xs_fmt("actor_add %s %s", actor, fn));
958 1344
959// object_add(actor, msg); 1345int is_hidden(snac *snac, const char *id)
1346/* check is id is hidden */
1347{
1348 xs *fn = _hidden_fn(snac, id);
960 1349
961 return ret; 1350 return !!(mtime(fn) != 0.0);
1351}
1352
1353
1354int actor_add(snac *snac, const char *actor, d_char *msg)
1355/* adds an actor */
1356{
1357 return object_add_ow(actor, msg);
962} 1358}
963 1359
964 1360
965int actor_get(snac *snac, char *actor, d_char **data) 1361int actor_get(snac *snac, const char *actor, d_char **data)
966/* returns an already downloaded actor */ 1362/* returns an already downloaded actor */
967{ 1363{
968 xs *fn = _actor_fn(snac, actor); 1364 int status = 200;
969 double t; 1365 char *d;
970 double max_time;
971 int status;
972 FILE *f;
973 1366
974 if (strcmp(actor, snac->actor) == 0) { 1367 if (strcmp(actor, snac->actor) == 0) {
1368 /* this actor */
975 if (data) 1369 if (data)
976 *data = msg_actor(snac); 1370 *data = msg_actor(snac);
977 1371
978 return 200; 1372 return status;
979 } 1373 }
980 1374
981 t = mtime(fn); 1375 /* read the object */
1376 if (!valid_status(status = object_get(actor, &d, NULL)))
1377 return status;
982 1378
983 /* no mtime? there is nothing here */ 1379 if (data)
984 if (t == 0.0) 1380 *data = d;
985 return 404; 1381
1382 xs *fn = _object_fn(actor);
1383 double max_time;
986 1384
987 /* maximum time for the actor data to be considered stale */ 1385 /* maximum time for the actor data to be considered stale */
988 max_time = 3600.0 * 36.0; 1386 max_time = 3600.0 * 36.0;
989 1387
990 if (t + max_time < (double) time(NULL)) { 1388 if (mtime(fn) + max_time < (double) time(NULL)) {
991 /* actor data exists but also stinks */ 1389 /* actor data exists but also stinks */
1390 FILE *f;
992 1391
993 if ((f = fopen(fn, "a")) != NULL) { 1392 if ((f = fopen(fn, "a")) != NULL) {
994 /* write a blank at the end to 'touch' the file */ 1393 /* write a blank at the end to 'touch' the file */
@@ -998,22 +1397,6 @@ int actor_get(snac *snac, char *actor, d_char **data)
998 1397
999 status = 205; /* "205: Reset Content" "110: Response Is Stale" */ 1398 status = 205; /* "205: Reset Content" "110: Response Is Stale" */
1000 } 1399 }
1001 else {
1002 /* it's still valid */
1003 status = 200;
1004 }
1005
1006 if (data) {
1007 if ((f = fopen(fn, "r")) != NULL) {
1008 xs *j = xs_readall(f);
1009
1010 fclose(f);
1011
1012 *data = xs_json_loads(j);
1013 }
1014 else
1015 status = 500;
1016 }
1017 1400
1018 return status; 1401 return status;
1019} 1402}
@@ -1033,7 +1416,7 @@ int static_get(snac *snac, const char *id, d_char **data, int *size)
1033 FILE *f; 1416 FILE *f;
1034 int status = 404; 1417 int status = 404;
1035 1418
1036 *size = 0xfffffff; 1419 *size = XS_ALL;
1037 1420
1038 if ((f = fopen(fn, "rb")) != NULL) { 1421 if ((f = fopen(fn, "rb")) != NULL) {
1039 *data = xs_read(f, size); 1422 *data = xs_read(f, size);
@@ -1121,6 +1504,8 @@ d_char *history_list(snac *snac)
1121} 1504}
1122 1505
1123 1506
1507/** the queue **/
1508
1124static int _enqueue_put(char *fn, char *msg) 1509static int _enqueue_put(char *fn, char *msg)
1125/* writes safely to the queue */ 1510/* writes safely to the queue */
1126{ 1511{
@@ -1267,38 +1652,81 @@ d_char *dequeue(snac *snac, char *fn)
1267} 1652}
1268 1653
1269 1654
1655/** the purge **/
1656
1657static void _purge_file(const char *fn, time_t mt)
1658/* purge fn if it's older than days */
1659{
1660 if (mtime(fn) < mt) {
1661 /* older than the minimum time: delete it */
1662 unlink(fn);
1663 srv_debug(1, xs_fmt("purged %s", fn));
1664 }
1665}
1666
1667
1270static void _purge_subdir(snac *snac, const char *subdir, int days) 1668static void _purge_subdir(snac *snac, const char *subdir, int days)
1271/* purges all files in subdir older than days */ 1669/* purges all files in subdir older than days */
1272{ 1670{
1273 if (days) { 1671 if (days) {
1274 time_t mt = time(NULL) - days * 24 * 3600; 1672 time_t mt = time(NULL) - days * 24 * 3600;
1275 xs *spec = xs_fmt("%s/%s/" "*.json", snac->basedir, subdir); 1673 xs *spec = xs_fmt("%s/%s/" "*", snac->basedir, subdir);
1276 xs *list = xs_glob(spec, 0, 0); 1674 xs *list = xs_glob(spec, 0, 0);
1277 char *p, *v; 1675 char *p, *v;
1278 1676
1279 p = list; 1677 p = list;
1280 while (xs_list_iter(&p, &v)) { 1678 while (xs_list_iter(&p, &v))
1281 if (mtime(v) < mt) { 1679 _purge_file(v, mt);
1282 /* older than the minimum time: delete it */ 1680 }
1283 unlink(v); 1681}
1284 snac_debug(snac, 1, xs_fmt("purged %s", v)); 1682
1683
1684void purge_server(void)
1685/* purge global server data */
1686{
1687 int tpd = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
1688 xs *spec = xs_fmt("%s/object/??", srv_basedir);
1689 xs *dirs = xs_glob(spec, 0, 0);
1690 char *p, *v;
1691
1692 time_t mt = time(NULL) - tpd * 24 * 3600;
1693
1694 p = dirs;
1695 while (xs_list_iter(&p, &v)) {
1696 xs *spec2 = xs_fmt("%s/" "*.json", v);
1697 xs *files = xs_glob(spec2, 0, 0);
1698 char *p2, *v2;
1699
1700 p2 = files;
1701 while (xs_list_iter(&p2, &v2)) {
1702 int n_link;
1703
1704 /* old and with no hard links? */
1705 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
1706 xs *s1 = xs_replace(v2, ".json", "");
1707 xs *l = xs_split(s1, "/");
1708 char *md5 = xs_list_get(l, -1);
1709
1710 object_del_by_md5(md5);
1285 } 1711 }
1286 } 1712 }
1287 } 1713 }
1288} 1714}
1289 1715
1290 1716
1291void purge(snac *snac) 1717void purge_user(snac *snac)
1292/* do the purge */ 1718/* do the purge for this user */
1293{ 1719{
1294 int days; 1720 int days;
1295 1721
1296 days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days")); 1722 days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
1297 _purge_subdir(snac, "timeline", days); 1723 _purge_subdir(snac, "timeline", days);
1298 _purge_subdir(snac, "actors", days); 1724 _purge_subdir(snac, "hidden", days);
1725 _purge_subdir(snac, "private", days);
1299 1726
1300 days = xs_number_get(xs_dict_get(srv_config, "local_purge_days")); 1727 days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
1301 _purge_subdir(snac, "local", days); 1728 _purge_subdir(snac, "local", days);
1729 _purge_subdir(snac, "public", days);
1302} 1730}
1303 1731
1304 1732
@@ -1312,8 +1740,10 @@ void purge_all(void)
1312 p = list; 1740 p = list;
1313 while (xs_list_iter(&p, &uid)) { 1741 while (xs_list_iter(&p, &uid)) {
1314 if (user_open(&snac, uid)) { 1742 if (user_open(&snac, uid)) {
1315 purge(&snac); 1743 purge_user(&snac);
1316 user_free(&snac); 1744 user_free(&snac);
1317 } 1745 }
1318 } 1746 }
1747
1748 purge_server();
1319} 1749}
diff --git a/doc/snac.1 b/doc/snac.1
index 5303bc8..3e34725 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -145,6 +145,11 @@ Initializes the database. This is an interactive command; necessary
145information will be prompted for. The 145information will be prompted for. The
146.Ar basedir 146.Ar basedir
147directory must not exist. 147directory must not exist.
148.It Cm upgrade Ar basedir
149Upgrades the database disk layout after installing a new version.
150Only necessary if
151.Nm
152complains and demands it.
148.It Cm purge Ar basedir 153.It Cm purge Ar basedir
149Purges old data from the timeline of all users. 154Purges old data from the timeline of all users.
150.It Cm adduser Ar basedir Op uid 155.It Cm adduser Ar basedir Op uid
diff --git a/doc/snac.8 b/doc/snac.8
index a241158..60671a1 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -14,6 +14,19 @@ This is the admin manual. For user operation, see
14.Xr snac 1 . 14.Xr snac 1 .
15For file and data formats, see 15For file and data formats, see
16.Xr snac 5 . 16.Xr snac 5 .
17.Ss Special cares about your snac you must know beforehand
18.Nm
19makes heavy use of hard links and link reference counts for its work, so
20don't even think of using it on a filesystem that doesn't support this
21feature. Most UNIX-like operating systems (Linux, the BSDs, the old DEC
22Ultrix machine in your grandfather basement, probably MacOS) support hard
23links on their native filesystems. Don't do fancy things like moving the
24subdirectories to different filesystems. Also, if you move your
25.Nm
26installation to another server, do it with a tool that respect hard
27link counts. Remember:
28.Nm
29is a very UNIXy program that loves hard links.
17.Ss Building and Installation 30.Ss Building and Installation
18A C compiler must be installed in the system, as well as the development 31A C compiler must be installed in the system, as well as the development
19headers and libraries for OpenSSL and curl. To build 32headers and libraries for OpenSSL and curl. To build
@@ -69,6 +82,19 @@ startup scripts and configuration data in the
69directory. 82directory.
70For other operating systems, please read the appropriate documentation 83For other operating systems, please read the appropriate documentation
71on how to install a daemon as a non-root service. 84on how to install a daemon as a non-root service.
85.Ss Upgrading to a new version
86Sometimes, the database disk layout changes between versions. If there
87is such a change,
88.Nm
89will refuse to run and require an upgrade. Do this by running
90.Bd -literal -offset indent
91snac upgrade $HOME/snac-data
92.Ed
93Take special care to execute this upgrade operation without any
94.Nm
95processes serving on the same folder. You can break everything. I know
96this because Tyler knows this.
97.Pp
72.Ss Server Setup 98.Ss Server Setup
73.Pp 99.Pp
74An http server with TLS and proxying support must already be 100An http server with TLS and proxying support must already be
diff --git a/html.c b/html.c
index 3f59b09..63b54c3 100644
--- a/html.c
+++ b/html.c
@@ -196,14 +196,14 @@ d_char *html_user_header(snac *snac, d_char *s, int local)
196 "<a href=\"%s.rss\">%s</a> - " 196 "<a href=\"%s.rss\">%s</a> - "
197 "<a href=\"%s/admin\" rel=\"nofollow\">%s</a></nav>\n", 197 "<a href=\"%s/admin\" rel=\"nofollow\">%s</a></nav>\n",
198 snac->actor, L("RSS"), 198 snac->actor, L("RSS"),
199 snac->actor, L("admin")); 199 snac->actor, L("private"));
200 else 200 else
201 s1 = xs_fmt( 201 s1 = xs_fmt(
202 "<a href=\"%s\">%s</a> - " 202 "<a href=\"%s\">%s</a> - "
203 "<a href=\"%s/admin\">%s</a> - " 203 "<a href=\"%s/admin\">%s</a> - "
204 "<a href=\"%s/people\">%s</a></nav>\n", 204 "<a href=\"%s/people\">%s</a></nav>\n",
205 snac->actor, L("public"), 205 snac->actor, L("public"),
206 snac->actor, L("admin"), 206 snac->actor, L("private"),
207 snac->actor, L("people")); 207 snac->actor, L("people"));
208 208
209 s = xs_str_cat(s, s1); 209 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
497 xs *s = xs_str_new(NULL); 497 xs *s = xs_str_new(NULL);
498 498
499 /* top wrap */ 499 /* top wrap */
500 if ((v = xs_dict_get(meta, "hidden")) && xs_type(v) == XSTYPE_TRUE) 500 if (is_hidden(snac, id))
501 s = xs_str_cat(s, "<div style=\"display: none\">\n"); 501 s = xs_str_cat(s, "<div style=\"display: none\">\n");
502 else 502 else
503 s = xs_str_cat(s, "<div>\n"); 503 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
840{ 840{
841 xs *s = xs_str_new(NULL); 841 xs *s = xs_str_new(NULL);
842 xs *h = xs_fmt("<h2>%s</h2>\n", header); 842 xs *h = xs_fmt("<h2>%s</h2>\n", header);
843 char *p, *v; 843 char *p, *actor_id;
844 844
845 s = xs_str_cat(s, h); 845 s = xs_str_cat(s, h);
846 846
847 p = list; 847 p = list;
848 while (xs_list_iter(&p, &v)) { 848 while (xs_list_iter(&p, &actor_id)) {
849 char *actor_id = xs_dict_get(v, "actor");
850 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); 849 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
851 xs *actor = NULL; 850 xs *actor = NULL;
852 851
@@ -1008,7 +1007,7 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
1008 status = 200; 1007 status = 200;
1009 } 1008 }
1010 else { 1009 else {
1011 xs *list = local_list(&snac, 0xfffffff); 1010 xs *list = local_list(&snac, XS_ALL);
1012 1011
1013 *body = html_timeline(&snac, list, 1); 1012 *body = html_timeline(&snac, list, 1);
1014 *b_size = strlen(*body); 1013 *b_size = strlen(*body);
@@ -1034,7 +1033,7 @@ int html_get_handler(d_char *req, char *q_path, char **body, int *b_size, char *
1034 else { 1033 else {
1035 snac_debug(&snac, 1, xs_fmt("building timeline")); 1034 snac_debug(&snac, 1, xs_fmt("building timeline"));
1036 1035
1037 xs *list = timeline_list(&snac, 0xfffffff); 1036 xs *list = timeline_list(&snac, XS_ALL);
1038 1037
1039 *body = html_timeline(&snac, list, 0); 1038 *body = html_timeline(&snac, list, 0);
1040 *b_size = strlen(*body); 1039 *b_size = strlen(*body);
@@ -1300,7 +1299,7 @@ int html_post_handler(d_char *req, char *q_path, d_char *payload, int p_size,
1300 } 1299 }
1301 else 1300 else
1302 if (strcmp(action, L("Hide")) == 0) { 1301 if (strcmp(action, L("Hide")) == 0) {
1303 timeline_hide(&snac, id, 1); 1302 hide(&snac, id);
1304 } 1303 }
1305 else 1304 else
1306 if (strcmp(action, L("Follow")) == 0) { 1305 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,
1341 1340
1342 post(&snac, msg); 1341 post(&snac, msg);
1343 1342
1343 /* FIXME: also post this Tombstone to people
1344 that Announce'd it */
1345
1344 snac_log(&snac, xs_fmt("posted tombstone for %s", id)); 1346 snac_log(&snac, xs_fmt("posted tombstone for %s", id));
1345 } 1347 }
1346 1348
diff --git a/httpd.c b/httpd.c
index 76bebad..dd8978d 100644
--- a/httpd.c
+++ b/httpd.c
@@ -222,7 +222,7 @@ static void *queue_thread(void *arg)
222 time_t purge_time; 222 time_t purge_time;
223 223
224 /* first purge time */ 224 /* first purge time */
225 purge_time = time(NULL) + 15 * 60; 225 purge_time = time(NULL) + 10 * 60;
226 226
227 srv_log(xs_fmt("queue thread start")); 227 srv_log(xs_fmt("queue thread start"));
228 228
diff --git a/main.c b/main.c
index 8e3dbd2..a109192 100644
--- a/main.c
+++ b/main.c
@@ -16,6 +16,7 @@ int usage(void)
16 printf("Commands:\n"); 16 printf("Commands:\n");
17 printf("\n"); 17 printf("\n");
18 printf("init [{basedir}] Initializes the database\n"); 18 printf("init [{basedir}] Initializes the database\n");
19 printf("upgrade {basedir} Upgrade to a new version\n");
19 printf("adduser {basedir} [{uid}] Adds a new user\n"); 20 printf("adduser {basedir} [{uid}] Adds a new user\n");
20 printf("httpd {basedir} Starts the HTTPD daemon\n"); 21 printf("httpd {basedir} Starts the HTTPD daemon\n");
21 printf("purge {basedir} Purges old data\n"); 22 printf("purge {basedir} Purges old data\n");
@@ -76,6 +77,19 @@ int main(int argc, char *argv[])
76 return initdb(basedir); 77 return initdb(basedir);
77 } 78 }
78 79
80 if (strcmp(cmd, "upgrade") == 0) {
81 int ret;
82
83 /* database upgrade */
84 if ((basedir = GET_ARGV()) == NULL)
85 return usage();
86
87 if ((ret = srv_open(basedir, 1)) == 1)
88 srv_log(xs_dup("OK"));
89
90 return ret;
91 }
92
79 if (strcmp(cmd, "markdown") == 0) { 93 if (strcmp(cmd, "markdown") == 0) {
80 /* undocumented, for testing only */ 94 /* undocumented, for testing only */
81 xs *c = xs_readall(stdin); 95 xs *c = xs_readall(stdin);
@@ -88,7 +102,7 @@ int main(int argc, char *argv[])
88 if ((basedir = GET_ARGV()) == NULL) 102 if ((basedir = GET_ARGV()) == NULL)
89 return usage(); 103 return usage();
90 104
91 if (!srv_open(basedir)) { 105 if (!srv_open(basedir, 0)) {
92 srv_log(xs_fmt("error opening database at %s", basedir)); 106 srv_log(xs_fmt("error opening database at %s", basedir));
93 return 1; 107 return 1;
94 } 108 }
@@ -142,12 +156,21 @@ int main(int argc, char *argv[])
142 } 156 }
143 157
144 if (strcmp(cmd, "timeline") == 0) { 158 if (strcmp(cmd, "timeline") == 0) {
145 xs *list = local_list(&snac, 0xfffffff); 159#if 0
160 xs *list = local_list(&snac, XS_ALL);
146 xs *body = html_timeline(&snac, list, 1); 161 xs *body = html_timeline(&snac, list, 1);
147 162
148 printf("%s\n", body); 163 printf("%s\n", body);
149 user_free(&snac); 164 user_free(&snac);
150 srv_free(); 165 srv_free();
166#endif
167
168 xs *idx = xs_fmt("%s/private.idx", snac.basedir);
169 xs *list = index_list_desc(idx, 256);
170 xs *tl = timeline_top_level(&snac, list);
171
172 xs *j = xs_json_dumps_pp(tl, 4);
173 printf("%s\n", j);
151 174
152 return 0; 175 return 0;
153 } 176 }
diff --git a/snac.h b/snac.h
index 5427497..b8b5215 100644
--- a/snac.h
+++ b/snac.h
@@ -24,7 +24,7 @@ double ftime(void);
24void srv_debug(int level, d_char *str); 24void srv_debug(int level, d_char *str);
25#define srv_log(str) srv_debug(0, str) 25#define srv_log(str) srv_debug(0, str)
26 26
27int srv_open(char *basedir); 27int srv_open(char *basedir, int auto_upgrade);
28void srv_free(void); 28void srv_free(void);
29 29
30typedef struct _snac { 30typedef struct _snac {
@@ -50,11 +50,21 @@ int check_password(char *uid, char *passwd, char *hash);
50void srv_archive(char *direction, char *req, char *payload, int p_size, 50void srv_archive(char *direction, char *req, char *payload, int p_size,
51 int status, char *headers, char *body, int b_size); 51 int status, char *headers, char *body, int b_size);
52 52
53double mtime(char *fn); 53double mtime_nl(const char *fn, int *n_link);
54#define mtime(fn) mtime_nl(fn, NULL)
54 55
55int follower_add(snac *snac, char *actor, char *msg); 56int index_add(const char *fn, const char *md5);
56int follower_del(snac *snac, char *actor); 57int index_del(const char *fn, const char *md5);
57int follower_check(snac *snac, char *actor); 58int index_first(const char *fn, char *buf, int size);
59d_char *index_list(const char *fn, int max);
60d_char *index_list_desc(const char *fn, int max);
61
62int object_del(const char *id);
63int object_del_if_unref(const char *id);
64
65int follower_add(snac *snac, const char *actor);
66int follower_del(snac *snac, const char *actor);
67int follower_check(snac *snac, const char *actor);
58d_char *follower_list(snac *snac); 68d_char *follower_list(snac *snac);
59 69
60double timeline_mtime(snac *snac); 70double timeline_mtime(snac *snac);
@@ -66,7 +76,8 @@ d_char *timeline_get(snac *snac, char *fn);
66d_char *timeline_list(snac *snac, int max); 76d_char *timeline_list(snac *snac, int max);
67int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer); 77int timeline_add(snac *snac, char *id, char *o_msg, char *parent, char *referrer);
68void timeline_admire(snac *snac, char *id, char *admirer, int like); 78void timeline_admire(snac *snac, char *id, char *admirer, int like);
69int timeline_hide(snac *snac, char *id, int hide); 79
80d_char *timeline_top_level(snac *snac, d_char *list);
70 81
71d_char *local_list(snac *snac, int max); 82d_char *local_list(snac *snac, int max);
72 83
@@ -80,8 +91,11 @@ void mute(snac *snac, char *actor);
80void unmute(snac *snac, char *actor); 91void unmute(snac *snac, char *actor);
81int is_muted(snac *snac, char *actor); 92int is_muted(snac *snac, char *actor);
82 93
83int actor_add(snac *snac, char *actor, char *msg); 94void hide(snac *snac, const char *id);
84int actor_get(snac *snac, char *actor, d_char **data); 95int is_hidden(snac *snac, const char *id);
96
97int actor_add(snac *snac, const char *actor, d_char *msg);
98int actor_get(snac *snac, const char *actor, d_char **data);
85 99
86int static_get(snac *snac, const char *id, d_char **data, int *size); 100int static_get(snac *snac, const char *id, d_char **data, int *size);
87void static_put(snac *snac, const char *id, const char *data, int size); 101void static_put(snac *snac, const char *id, const char *data, int size);
diff --git a/upgrade.c b/upgrade.c
index e4c75bb..e2983e4 100644
--- a/upgrade.c
+++ b/upgrade.c
@@ -40,6 +40,137 @@ int db_upgrade(d_char **error)
40 40
41 nf = 2.1; 41 nf = 2.1;
42 } 42 }
43 else
44 if (f < 2.2) {
45 xs *users = user_list();
46 char *p, *v;
47
48 p = users;
49 while (xs_list_iter(&p, &v)) {
50 snac snac;
51
52 if (user_open(&snac, v)) {
53 xs *spec = xs_fmt("%s/actors/" "*.json", snac.basedir);
54 xs *list = xs_glob(spec, 0, 0);
55 char *g, *fn;
56
57 g = list;
58 while (xs_list_iter(&g, &fn)) {
59 xs *l = xs_split(fn, "/");
60 char *b = xs_list_get(l, -1);
61 xs *dir = xs_fmt("%s/object/%c%c", srv_basedir, b[0], b[1]);
62 xs *nfn = xs_fmt("%s/%s", dir, b);
63
64 mkdir(dir, 0755);
65 rename(fn, nfn);
66 }
67
68 xs *odir = xs_fmt("%s/actors", snac.basedir);
69 rmdir(odir);
70
71 user_free(&snac);
72 }
73 }
74
75 nf = 2.2;
76 }
77 else
78 if (f < 2.3) {
79 xs *users = user_list();
80 char *p, *v;
81
82 p = users;
83 while (xs_list_iter(&p, &v)) {
84 snac snac;
85
86 if (user_open(&snac, v)) {
87 char *p, *v;
88 xs *dir = xs_fmt("%s/hidden", snac.basedir);
89
90 /* create the hidden directory */
91 mkdir(dir, 0755);
92
93 /* rename all muted files incorrectly named .json */
94 xs *spec = xs_fmt("%s/muted/" "*.json", snac.basedir);
95 xs *fns = xs_glob(spec, 0, 0);
96
97 p = fns;
98 while (xs_list_iter(&p, &v)) {
99 xs *nfn = xs_replace(v, ".json", "");
100 rename(v, nfn);
101 }
102
103 user_free(&snac);
104 }
105 }
106
107 nf = 2.3;
108 }
109 else
110 if (f < 2.4) {
111 xs *users = user_list();
112 char *p, *v;
113
114 p = users;
115 while (xs_list_iter(&p, &v)) {
116 snac snac;
117
118 if (user_open(&snac, v)) {
119 xs *dir = xs_fmt("%s/public", snac.basedir);
120 mkdir(dir, 0755);
121
122 dir = xs_replace_i(dir, "public", "private");
123 mkdir(dir, 0755);
124
125 user_free(&snac);
126 }
127 }
128
129 nf = 2.4;
130 }
131 else
132 if (f < 2.5) {
133 /* upgrade followers */
134 xs *users = user_list();
135 char *p, *v;
136
137 p = users;
138 while (xs_list_iter(&p, &v)) {
139 snac snac;
140
141 if (user_open(&snac, v)) {
142 xs *spec = xs_fmt("%s/followers/" "*.json", snac.basedir);
143 xs *dir = xs_glob(spec, 0, 0);
144 char *p, *v;
145
146 p = dir;
147 while (xs_list_iter(&p, &v)) {
148 FILE *f;
149
150 if ((f = fopen(v, "r")) != NULL) {
151 xs *s = xs_readall(f);
152 xs *o = xs_json_loads(s);
153 fclose(f);
154
155 char *type = xs_dict_get(o, "type");
156
157 if (!xs_is_null(type) && strcmp(type, "Follow") == 0) {
158 unlink(v);
159
160 char *actor = xs_dict_get(o, "actor");
161
162 if (!xs_is_null(actor))
163 follower_add(&snac, actor);
164 }
165 }
166 }
167
168 user_free(&snac);
169 }
170 }
171
172 nf = 2.5;
173 }
43 174
44 if (f < nf) { 175 if (f < nf) {
45 f = nf; 176 f = nf;
diff --git a/utils.c b/utils.c
index 58c2246..84b367f 100644
--- a/utils.c
+++ b/utils.c
@@ -237,8 +237,9 @@ int adduser(char *uid)
237 } 237 }
238 238
239 const char *dirs[] = { 239 const char *dirs[] = {
240 "actors", "followers", "following", "local", "muted", 240 "followers", "following", "local", "muted", "hidden",
241 "queue", "static", "timeline", "history", NULL }; 241 "public", "private", "queue", "history",
242 "static", "timeline", NULL };
242 int n; 243 int n;
243 244
244 for (n = 0; dirs[n]; n++) { 245 for (n = 0; dirs[n]; n++) {
diff --git a/xs.h b/xs.h
index da853f5..9cae3dc 100644
--- a/xs.h
+++ b/xs.h
@@ -34,6 +34,9 @@ typedef char d_char;
34/* auto-destroyable strings */ 34/* auto-destroyable strings */
35#define xs __attribute__ ((__cleanup__ (_xs_destroy))) d_char 35#define xs __attribute__ ((__cleanup__ (_xs_destroy))) d_char
36 36
37/* not really all, just very much */
38#define XS_ALL 0xfffffff
39
37void *xs_free(void *ptr); 40void *xs_free(void *ptr);
38void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func); 41void *_xs_realloc(void *ptr, size_t size, const char *file, int line, const char *func);
39#define xs_realloc(ptr, size) _xs_realloc(ptr, size, __FILE__, __LINE__, __FUNCTION__) 42#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);
74int xs_list_in(char *list, const char *val); 77int xs_list_in(char *list, const char *val);
75d_char *xs_join(char *list, const char *sep); 78d_char *xs_join(char *list, const char *sep);
76d_char *xs_split_n(const char *str, const char *sep, int times); 79d_char *xs_split_n(const char *str, const char *sep, int times);
77#define xs_split(str, sep) xs_split_n(str, sep, 0xfffffff) 80#define xs_split(str, sep) xs_split_n(str, sep, XS_ALL)
78d_char *xs_dict_new(void); 81d_char *xs_dict_new(void);
79d_char *xs_dict_append_m(d_char *dict, const char *key, const char *mem, int dsz); 82d_char *xs_dict_append_m(d_char *dict, const char *key, const char *mem, int dsz);
80#define xs_dict_append(dict, key, data) xs_dict_append_m(dict, key, data, xs_size(data)) 83#define xs_dict_append(dict, key, data) xs_dict_append_m(dict, key, data, xs_size(data))
diff --git a/xs_glob.h b/xs_glob.h
index c5293dc..978c200 100644
--- a/xs_glob.h
+++ b/xs_glob.h
@@ -5,7 +5,7 @@
5#define _XS_GLOB_H 5#define _XS_GLOB_H
6 6
7d_char *xs_glob_n(const char *spec, int basename, int reverse, int max); 7d_char *xs_glob_n(const char *spec, int basename, int reverse, int max);
8#define xs_glob(spec, basename, reverse) xs_glob_n(spec, basename, reverse, 0xfffffff) 8#define xs_glob(spec, basename, reverse) xs_glob_n(spec, basename, reverse, XS_ALL)
9 9
10 10
11#ifdef XS_IMPLEMENTATION 11#ifdef XS_IMPLEMENTATION
diff --git a/xs_io.h b/xs_io.h
index 9649484..6112dcb 100644
--- a/xs_io.h
+++ b/xs_io.h
@@ -79,7 +79,7 @@ d_char *xs_read(FILE *f, int *sz)
79d_char *xs_readall(FILE *f) 79d_char *xs_readall(FILE *f)
80/* reads the rest of the file into a string */ 80/* reads the rest of the file into a string */
81{ 81{
82 int size = 0xfffffff; 82 int size = XS_ALL;
83 83
84 return xs_read(f, &size); 84 return xs_read(f, &size);
85} 85}
diff --git a/xs_regex.h b/xs_regex.h
index e0d1b7a..302bcf0 100644
--- a/xs_regex.h
+++ b/xs_regex.h
@@ -5,9 +5,9 @@
5#define _XS_REGEX_H 5#define _XS_REGEX_H
6 6
7d_char *xs_regex_split_n(const char *str, const char *rx, int count); 7d_char *xs_regex_split_n(const char *str, const char *rx, int count);
8#define xs_regex_split(str, rx) xs_regex_split_n(str, rx, 0xfffffff) 8#define xs_regex_split(str, rx) xs_regex_split_n(str, rx, XS_ALL)
9d_char *xs_regex_match_n(const char *str, const char *rx, int count); 9d_char *xs_regex_match_n(const char *str, const char *rx, int count);
10#define xs_regex_match(str, rx) xs_regex_match_n(str, rx, 0xfffffff) 10#define xs_regex_match(str, rx) xs_regex_match_n(str, rx, XS_ALL)
11 11
12#ifdef XS_IMPLEMENTATION 12#ifdef XS_IMPLEMENTATION
13 13
diff --git a/xs_set.h b/xs_set.h
index bd1b8ea..f97eb20 100644
--- a/xs_set.h
+++ b/xs_set.h
@@ -12,6 +12,7 @@ typedef struct _xs_set {
12} xs_set; 12} xs_set;
13 13
14void xs_set_init(xs_set *s); 14void xs_set_init(xs_set *s);
15d_char *xs_set_result(xs_set *s);
15void xs_set_free(xs_set *s); 16void xs_set_free(xs_set *s);
16int xs_set_add(xs_set *s, const char *data); 17int xs_set_add(xs_set *s, const char *data);
17 18
@@ -32,11 +33,21 @@ void xs_set_init(xs_set *s)
32} 33}
33 34
34 35
35void xs_set_free(xs_set *s) 36d_char *xs_set_result(xs_set *s)
36/* frees a set */ 37/* returns the set as a list and frees it */
37{ 38{
39 d_char *list = s->list;
40 s->list = NULL;
38 s->hash = xs_free(s->hash); 41 s->hash = xs_free(s->hash);
39 s->list = xs_free(s->list); 42
43 return list;
44}
45
46
47void xs_set_free(xs_set *s)
48/* frees a set, dropping the list */
49{
50 free(xs_set_result(s));
40} 51}
41 52
42 53
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 */