2.53
data.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
3
4#include "xs.h"
5#include "xs_hex.h"
6#include "xs_io.h"
7#include "xs_json.h"
8#include "xs_openssl.h"
9#include "xs_glob.h"
10#include "xs_set.h"
11#include "xs_time.h"
12#include "xs_regex.h"
13#include "xs_match.h"
14
15#include "snac.h"
16
17#include <time.h>
18#include <sys/stat.h>
19#include <sys/file.h>
20#include <sys/time.h>
21#include <fcntl.h>
22#include <pthread.h>
23
24double disk_layout = 2.7;
25
26/* storage serializer */
27pthread_mutex_t data_mutex = {0};
28
29int snac_upgrade(xs_str **error);
30
31
32int srv_open(char *basedir, int auto_upgrade)
33/* opens a server */
34{
35 int ret = 0;
36 xs *cfg_file = NULL;
37 FILE *f;
38 xs_str *error = NULL;
39
40 pthread_mutex_init(&data_mutex, NULL);
41
42 srv_basedir = xs_str_new(basedir);
43
44 if (xs_endswith(srv_basedir, "/"))
45 srv_basedir = xs_crop_i(srv_basedir, 0, -1);
46
47 cfg_file = xs_fmt("%s/server.json", basedir);
48
49 if ((f = fopen(cfg_file, "r")) == NULL)
50 error = xs_fmt("ERROR: cannot opening '%s'", cfg_file);
51 else {
52 /* read full config file */
53 srv_config = xs_json_load(f);
54 fclose(f);
55
56 /* parse */
57
58 if (srv_config == NULL)
59 error = xs_fmt("ERROR: cannot parse '%s'", cfg_file);
60 else {
61 char *host;
62 char *prefix;
63 char *dbglvl;
64 char *proto;
65
66 host = xs_dict_get(srv_config, "host");
67 prefix = xs_dict_get(srv_config, "prefix");
68 dbglvl = xs_dict_get(srv_config, "dbglevel");
69 proto = xs_dict_get_def(srv_config, "protocol", "https");
70
71 if (host == NULL || prefix == NULL)
72 error = xs_str_new("ERROR: cannot get server data");
73 else {
74 srv_baseurl = xs_fmt("%s:/" "/%s%s", proto, host, prefix);
75
76 dbglevel = (int) xs_number_get(dbglvl);
77
78 if ((dbglvl = getenv("DEBUG")) != NULL) {
79 dbglevel = atoi(dbglvl);
80 error = xs_fmt("DEBUG level set to %d from environment", dbglevel);
81 }
82
83 if (auto_upgrade)
84 ret = snac_upgrade(&error);
85 else {
86 if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
87 error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
88 else
89 ret = 1;
90 }
91 }
92
93 }
94 }
95
96 if (error != NULL)
97 srv_log(error);
98
99 /* create the queue/ subdir, just in case */
100 xs *qdir = xs_fmt("%s/queue", srv_basedir);
101 mkdirx(qdir);
102
103 xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
104 mkdirx(ibdir);
105
106 xs *tmpdir = xs_fmt("%s/tmp", srv_basedir);
107 mkdirx(tmpdir);
108
109#ifdef __APPLE__
110/* Apple uses st_atimespec instead of st_atim etc */
111#define st_atim st_atimespec
112#define st_ctim st_ctimespec
113#define st_mtim st_mtimespec
114#endif
115
116#ifdef __OpenBSD__
117 char *v = xs_dict_get(srv_config, "disable_openbsd_security");
118
119 if (v && xs_type(v) == XSTYPE_TRUE) {
120 srv_debug(1, xs_dup("OpenBSD security disabled by admin"));
121 }
122 else {
123 int smail = xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE;
124
125 srv_debug(1, xs_fmt("Calling unveil()"));
126 unveil(basedir, "rwc");
127 unveil("/tmp", "rwc");
128 unveil("/etc/resolv.conf", "r");
129 unveil("/etc/hosts", "r");
130 unveil("/etc/ssl/openssl.cnf", "r");
131 unveil("/etc/ssl/cert.pem", "r");
132 unveil("/usr/share/zoneinfo", "r");
133
134 if (smail)
135 unveil("/usr/sbin/sendmail", "x");
136
137 unveil(NULL, NULL);
138 srv_debug(1, xs_fmt("Calling pledge()"));
139
140 if (smail)
141 pledge("stdio rpath wpath cpath flock inet proc exec dns fattr", NULL);
142 else
143 pledge("stdio rpath wpath cpath flock inet proc dns fattr", NULL);
144 }
145#endif /* __OpenBSD__ */
146
147 /* read (and drop) emojis.json, possibly creating it */
148 xs_free(emojis());
149
150 return ret;
151}
152
153
154void srv_free(void)
155{
156 xs_free(srv_basedir);
157 xs_free(srv_config);
158 xs_free(srv_baseurl);
159
160 pthread_mutex_destroy(&data_mutex);
161}
162
163
164void user_free(snac *snac)
165/* frees a user snac */
166{
167 xs_free(snac->uid);
168 xs_free(snac->basedir);
169 xs_free(snac->config);
170 xs_free(snac->config_o);
171 xs_free(snac->key);
172 xs_free(snac->links);
173 xs_free(snac->actor);
174 xs_free(snac->md5);
175}
176
177
178int user_open(snac *user, const char *uid)
179/* opens a user */
180{
181 int ret = 0;
182
183 *user = (snac){0};
184
185 if (validate_uid(uid)) {
186 xs *cfg_file = NULL;
187 FILE *f;
188
189 xs *t = xs_fmt("%s/user/%s", srv_basedir, uid);
190
191 if (mtime(t) == 0.0) {
192 /* user folder does not exist; try with a different case */
193 xs *lcuid = xs_tolower_i(xs_dup(uid));
194 xs *ulist = user_list();
195 xs_list *p = ulist;
196 xs_str *v;
197
198 while (xs_list_iter(&p, &v)) {
199 xs *v2 = xs_tolower_i(xs_dup(v));
200
201 if (strcmp(lcuid, v2) == 0) {
202 user->uid = xs_dup(v);
203 break;
204 }
205 }
206 }
207 else
208 user->uid = xs_str_new(uid);
209
210 if (user->uid == NULL)
211 return ret;
212
213 user->basedir = xs_fmt("%s/user/%s", srv_basedir, user->uid);
214
215 cfg_file = xs_fmt("%s/user.json", user->basedir);
216
217 if ((f = fopen(cfg_file, "r")) != NULL) {
218 /* read full config file */
219 user->config = xs_json_load(f);
220 fclose(f);
221
222 if (user->config != NULL) {
223 xs *key_file = xs_fmt("%s/key.json", user->basedir);
224
225 if ((f = fopen(key_file, "r")) != NULL) {
226 user->key = xs_json_load(f);
227 fclose(f);
228
229 if (user->key != NULL) {
230 user->actor = xs_fmt("%s/%s", srv_baseurl, user->uid);
231 user->md5 = xs_md5_hex(user->actor, strlen(user->actor));
232
233 /* everything is ok right now */
234 ret = 1;
235
236 /* does it have a configuration override? */
237 xs *cfg_file_o = xs_fmt("%s/user_o.json", user->basedir);
238 if ((f = fopen(cfg_file_o, "r")) != NULL) {
239 user->config_o = xs_json_load(f);
240 fclose(f);
241
242 if (user->config_o == NULL)
243 srv_log(xs_fmt("error parsing '%s'", cfg_file_o));
244 }
245
246 if (user->config_o == NULL)
247 user->config_o = xs_dict_new();
248 }
249 else
250 srv_log(xs_fmt("error parsing '%s'", key_file));
251 }
252 else
253 srv_log(xs_fmt("error opening '%s' %d", key_file, errno));
254 }
255 else
256 srv_log(xs_fmt("error parsing '%s'", cfg_file));
257 }
258 else
259 srv_debug(2, xs_fmt("error opening '%s' %d", cfg_file, errno));
260
261 /* verified links */
262 xs *links_file = xs_fmt("%s/links.json", user->basedir);
263
264 if ((f = fopen(links_file, "r")) != NULL) {
265 user->links = xs_json_load(f);
266 fclose(f);
267 }
268 }
269 else
270 srv_debug(1, xs_fmt("invalid user '%s'", uid));
271
272 if (!ret)
273 user_free(user);
274
275 return ret;
276}
277
278
279xs_list *user_list(void)
280/* returns the list of user ids */
281{
282 xs *spec = xs_fmt("%s/user/" "*", srv_basedir);
283 return xs_glob(spec, 1, 0);
284}
285
286
287int user_open_by_md5(snac *snac, const char *md5)
288/* iterates all users searching by md5 */
289{
290 xs *ulist = user_list();
291 xs_list *p = ulist;
292 xs_str *v;
293
294 while (xs_list_iter(&p, &v)) {
295 user_open(snac, v);
296
297 if (strcmp(snac->md5, md5) == 0)
298 return 1;
299
300 user_free(snac);
301 }
302
303 return 0;
304}
305
306
307double mtime_nl(const char *fn, int *n_link)
308/* returns the mtime and number of links of a file or directory, or 0.0 */
309{
310 struct stat st;
311 double r = 0.0;
312 int n = 0;
313
314 if (fn && stat(fn, &st) != -1) {
315 r = (double) st.st_mtim.tv_sec;
316 n = st.st_nlink;
317 }
318
319 if (n_link)
320 *n_link = n;
321
322 return r;
323}
324
325
326#define MIN(v1, v2) ((v1) < (v2) ? (v1) : (v2))
327
328double f_ctime(const char *fn)
329/* returns the ctime of a file or directory, or 0.0 */
330{
331 struct stat st;
332 double r = 0.0;
333
334 if (fn && stat(fn, &st) != -1) {
335 /* return the lowest of ctime and mtime;
336 there are operations that change the ctime, like link() */
337 r = (double) MIN(st.st_ctim.tv_sec, st.st_mtim.tv_sec);
338 }
339
340 return r;
341}
342
343
344int is_md5_hex(const char *md5)
345{
346 return xs_is_hex(md5) && strlen(md5) == 32;
347}
348
349
350/** database 2.1+ **/
351
352/** indexes **/
353
354
355int index_add_md5(const char *fn, const char *md5)
356/* adds an md5 to an index */
357{
358 int status = 201; /* Created */
359 FILE *f;
360
361 if (!is_md5_hex(md5)) {
362 srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
363 return 400;
364 }
365
366 pthread_mutex_lock(&data_mutex);
367
368 if ((f = fopen(fn, "a")) != NULL) {
369 flock(fileno(f), LOCK_EX);
370
371 /* ensure the position is at the end after getting the lock */
372 fseek(f, 0, SEEK_END);
373
374 fprintf(f, "%s\n", md5);
375 fclose(f);
376 }
377 else
378 status = 500;
379
380 pthread_mutex_unlock(&data_mutex);
381
382 return status;
383}
384
385
386int index_add(const char *fn, const char *id)
387/* adds an id to an index */
388{
389 xs *md5 = xs_md5_hex(id, strlen(id));
390 return index_add_md5(fn, md5);
391}
392
393
394int index_del_md5(const char *fn, const char *md5)
395/* deletes an md5 from an index */
396{
397 int status = 404;
398 FILE *f;
399
400 pthread_mutex_lock(&data_mutex);
401
402 if ((f = fopen(fn, "r+")) != NULL) {
403 char line[256];
404
405 while (fgets(line, sizeof(line), f) != NULL) {
406 line[32] = '\0';
407
408 if (strcmp(line, md5) == 0) {
409 /* found! just rewind, overwrite it with garbage
410 and an eventual call to index_gc() will clean it
411 [yes: this breaks index_len()] */
412 fseek(f, -33, SEEK_CUR);
413 fwrite("-", 1, 1, f);
414 status = 200;
415
416 break;
417 }
418 }
419
420 fclose(f);
421 }
422 else
423 status = 410;
424
425 pthread_mutex_unlock(&data_mutex);
426
427 return status;
428}
429
430
431int index_del(const char *fn, const char *id)
432/* deletes an id from an index */
433{
434 xs *md5 = xs_md5_hex(id, strlen(id));
435 return index_del_md5(fn, md5);
436}
437
438
439int index_gc(const char *fn)
440/* garbage-collects an index, deleting objects that are not here */
441{
442 FILE *i, *o;
443 int gc = -1;
444
445 pthread_mutex_lock(&data_mutex);
446
447 if ((i = fopen(fn, "r")) != NULL) {
448 xs *nfn = xs_fmt("%s.new", fn);
449 char line[256];
450
451 if ((o = fopen(nfn, "w")) != NULL) {
452 gc = 0;
453
454 while (fgets(line, sizeof(line), i) != NULL) {
455 line[32] = '\0';
456
457 if (line[0] != '-' && object_here_by_md5(line))
458 fprintf(o, "%s\n", line);
459 else
460 gc++;
461 }
462
463 fclose(o);
464
465 xs *ofn = xs_fmt("%s.bak", fn);
466
467 unlink(ofn);
468 link(fn, ofn);
469 rename(nfn, fn);
470 }
471
472 fclose(i);
473 }
474
475 pthread_mutex_unlock(&data_mutex);
476
477 return gc;
478}
479
480
481int index_in_md5(const char *fn, const char *md5)
482/* checks if the md5 is already in the index */
483{
484 FILE *f;
485 int ret = 0;
486
487 if ((f = fopen(fn, "r")) != NULL) {
488 flock(fileno(f), LOCK_SH);
489
490 char line[256];
491
492 while (!ret && fgets(line, sizeof(line), f) != NULL) {
493 line[32] = '\0';
494
495 if (strcmp(line, md5) == 0)
496 ret = 1;
497 }
498
499 fclose(f);
500 }
501
502 return ret;
503}
504
505
506int index_in(const char *fn, const char *id)
507/* checks if the object id is already in the index */
508{
509 xs *md5 = xs_md5_hex(id, strlen(id));
510 return index_in_md5(fn, md5);
511}
512
513
514int index_first(const char *fn, char *line, int size)
515/* reads the first entry of an index */
516{
517 FILE *f;
518 int ret = 0;
519
520 if ((f = fopen(fn, "r")) != NULL) {
521 flock(fileno(f), LOCK_SH);
522
523 if (fgets(line, size, f) != NULL) {
524 line[32] = '\0';
525 ret = 1;
526 }
527
528 fclose(f);
529 }
530
531 return ret;
532}
533
534
535int index_len(const char *fn)
536/* returns the number of elements in an index */
537{
538 struct stat st;
539 int len = 0;
540
541 if (stat(fn, &st) != -1)
542 len = st.st_size / 33;
543
544 return len;
545}
546
547
548xs_list *index_list(const char *fn, int max)
549/* returns an index as a list */
550{
551 xs_list *list = xs_list_new();
552 FILE *f;
553 int n = 0;
554
555 if ((f = fopen(fn, "r")) != NULL) {
556 flock(fileno(f), LOCK_SH);
557
558 char line[256];
559
560 while (n < max && fgets(line, sizeof(line), f) != NULL) {
561 if (line[0] != '-') {
562 line[32] = '\0';
563 list = xs_list_append(list, line);
564 n++;
565 }
566 }
567
568 fclose(f);
569 }
570
571 return list;
572}
573
574
575xs_list *index_list_desc(const char *fn, int skip, int show)
576/* returns an index as a list, in reverse order */
577{
578 xs_list *list = xs_list_new();
579 FILE *f;
580 int n = 0;
581
582 if ((f = fopen(fn, "r")) != NULL) {
583 flock(fileno(f), LOCK_SH);
584
585 char line[256];
586
587 /* move to the end minus one entry (or more, if skipping entries) */
588 if (!fseek(f, 0, SEEK_END) && !fseek(f, (skip + 1) * -33, SEEK_CUR)) {
589 while (n < show && fgets(line, sizeof(line), f) != NULL) {
590 if (line[0] != '-') {
591 line[32] = '\0';
592 list = xs_list_append(list, line);
593 n++;
594 }
595
596 /* move backwards 2 entries */
597 if (fseek(f, -66, SEEK_CUR) == -1)
598 break;
599 }
600 }
601
602 fclose(f);
603 }
604
605 return list;
606}
607
608
609/** objects **/
610
611static xs_str *_object_fn_by_md5(const char *md5, const char *func)
612{
613 xs *bfn = xs_fmt("%s/object/%c%c", srv_basedir, md5[0], md5[1]);
614 xs_str *ret;
615 int ok = 1;
616
617 /* an object deleted from an index; fail but don't bark */
618 if (md5[0] == '-')
619 ok = 0;
620 else
621 if (!is_md5_hex(md5)) {
622 srv_log(xs_fmt("_object_fn_by_md5() [from %s()]: bad md5 '%s'", func, md5));
623 ok = 0;
624 }
625
626 if (ok) {
627 mkdirx(bfn);
628 ret = xs_fmt("%s/%s.json", bfn, md5);
629 }
630 else
631 ret = xs_fmt("%s/object/invalid/invalid.json", srv_basedir);
632
633 return ret;
634}
635
636
637static xs_str *_object_fn(const char *id)
638{
639 xs *md5 = xs_md5_hex(id, strlen(id));
640 return _object_fn_by_md5(md5, "_object_fn");
641}
642
643
644int object_here_by_md5(const char *id)
645/* checks if an object is already downloaded */
646{
647 xs *fn = _object_fn_by_md5(id, "object_here_by_md5");
648 return mtime(fn) > 0.0;
649}
650
651
652int object_here(const char *id)
653/* checks if an object is already downloaded */
654{
655 xs *fn = _object_fn(id);
656 return mtime(fn) > 0.0;
657}
658
659
660int object_get_by_md5(const char *md5, xs_dict **obj)
661/* returns a stored object, optionally of the requested type */
662{
663 int status = 404;
664 xs *fn = _object_fn_by_md5(md5, "object_get_by_md5");
665 FILE *f;
666
667 if ((f = fopen(fn, "r")) != NULL) {
668 *obj = xs_json_load(f);
669 fclose(f);
670
671 if (*obj)
672 status = 200;
673 }
674 else
675 *obj = NULL;
676
677 return status;
678}
679
680
681int object_get(const char *id, xs_dict **obj)
682/* returns a stored object, optionally of the requested type */
683{
684 xs *md5 = xs_md5_hex(id, strlen(id));
685 return object_get_by_md5(md5, obj);
686}
687
688
689int _object_add(const char *id, const xs_dict *obj, int ow)
690/* stores an object */
691{
692 int status = 201; /* Created */
693 xs *fn = _object_fn(id);
694 FILE *f;
695
696 if (mtime(fn) > 0.0) {
697 if (!ow) {
698 /* object already here */
699 srv_debug(1, xs_fmt("object_add object already here %s", id));
700 return 204; /* No content */
701 }
702 else
703 status = 200;
704 }
705
706 if ((f = fopen(fn, "w")) != NULL) {
707 flock(fileno(f), LOCK_EX);
708
709 xs_json_dump(obj, 4, f);
710 fclose(f);
711
712 /* does this object has a parent? */
713 char *in_reply_to = xs_dict_get(obj, "inReplyTo");
714
715 if (!xs_is_null(in_reply_to) && *in_reply_to) {
716 /* update the children index of the parent */
717 xs *c_idx = _object_fn(in_reply_to);
718
719 c_idx = xs_replace_i(c_idx, ".json", "_c.idx");
720
721 if (!index_in(c_idx, id)) {
722 index_add(c_idx, id);
723 srv_debug(1, xs_fmt("object_add added child %s to %s", id, c_idx));
724 }
725 else
726 srv_debug(1, xs_fmt("object_add %s child already in %s", id, c_idx));
727
728 /* create a one-element index with the parent */
729 xs *p_idx = xs_replace(fn, ".json", "_p.idx");
730
731 if (mtime(p_idx) == 0.0) {
732 index_add(p_idx, in_reply_to);
733 srv_debug(1, xs_fmt("object_add added parent %s to %s", in_reply_to, p_idx));
734 }
735 }
736 }
737 else {
738 srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
739 status = 500;
740 }
741
742 srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
743
744 return status;
745}
746
747
748int object_add(const char *id, const xs_dict *obj)
749/* stores an object */
750{
751 return _object_add(id, obj, 0);
752}
753
754
755int object_add_ow(const char *id, const xs_dict *obj)
756/* stores an object (overwriting allowed) */
757{
758 return _object_add(id, obj, 1);
759}
760
761
762int object_del_by_md5(const char *md5)
763/* deletes an object by its md5 */
764{
765 int status = 404;
766 xs *fn = _object_fn_by_md5(md5, "object_del_by_md5");
767
768 if (unlink(fn) != -1) {
769 status = 200;
770
771 /* also delete associated indexes */
772 xs *spec = xs_dup(fn);
773 spec = xs_replace_i(spec, ".json", "*.idx");
774 xs *files = xs_glob(spec, 0, 0);
775 char *p, *v;
776
777 p = files;
778 while (xs_list_iter(&p, &v)) {
779 srv_debug(1, xs_fmt("object_del index %s", v));
780 unlink(v);
781 }
782 }
783
784 srv_debug(1, xs_fmt("object_del %s %d", fn, status));
785
786 return status;
787}
788
789
790int object_del(const char *id)
791/* deletes an object */
792{
793 xs *md5 = xs_md5_hex(id, strlen(id));
794 return object_del_by_md5(md5);
795}
796
797
798int object_del_if_unref(const char *id)
799/* deletes an object if its n_links < 2 */
800{
801 xs *fn = _object_fn(id);
802 int n_links;
803 int ret = 0;
804
805 if (mtime_nl(fn, &n_links) > 0.0 && n_links < 2)
806 ret = object_del(id);
807
808 return ret;
809}
810
811
812double object_ctime_by_md5(const char *md5)
813{
814 xs *fn = _object_fn_by_md5(md5, "object_ctime_by_md5");
815 return f_ctime(fn);
816}
817
818
819double object_ctime(const char *id)
820{
821 xs *md5 = xs_md5_hex(id, strlen(id));
822 return object_ctime_by_md5(md5);
823}
824
825
826double object_mtime_by_md5(const char *md5)
827{
828 xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5");
829 return mtime(fn);
830}
831
832
833double object_mtime(const char *id)
834{
835 xs *md5 = xs_md5_hex(id, strlen(id));
836 return object_mtime_by_md5(md5);
837}
838
839
840void object_touch(const char *id)
841{
842 xs *md5 = xs_md5_hex(id, strlen(id));
843 xs *fn = _object_fn_by_md5(md5, "object_touch");
844
845 if (mtime(fn))
846 utimes(fn, NULL);
847}
848
849
850xs_str *_object_index_fn(const char *id, const char *idxsfx)
851/* returns the filename of an object's index */
852{
853 xs_str *fn = _object_fn(id);
854 return xs_replace_i(fn, ".json", idxsfx);
855}
856
857
858int object_likes_len(const char *id)
859/* returns the number of likes (without reading the index) */
860{
861 xs *fn = _object_index_fn(id, "_l.idx");
862 return index_len(fn);
863}
864
865
866int object_announces_len(const char *id)
867/* returns the number of announces (without reading the index) */
868{
869 xs *fn = _object_index_fn(id, "_a.idx");
870 return index_len(fn);
871}
872
873
874xs_list *object_children(const char *id)
875/* returns the list of an object's children */
876{
877 xs *fn = _object_index_fn(id, "_c.idx");
878 return index_list(fn, XS_ALL);
879}
880
881
882xs_list *object_likes(const char *id)
883{
884 xs *fn = _object_index_fn(id, "_l.idx");
885 return index_list(fn, XS_ALL);
886}
887
888
889xs_list *object_announces(const char *id)
890{
891 xs *fn = _object_index_fn(id, "_a.idx");
892 return index_list(fn, XS_ALL);
893}
894
895
896int object_parent(const char *md5, char *buf, int size)
897/* returns the object parent, if any */
898{
899 xs *fn = _object_fn_by_md5(md5, "object_parent");
900
901 fn = xs_replace_i(fn, ".json", "_p.idx");
902 return index_first(fn, buf, size);
903}
904
905
906int object_admire(const char *id, const char *actor, int like)
907/* actor likes or announces this object */
908{
909 int status = 200;
910 xs *fn = _object_fn(id);
911
912 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
913
914 if (!index_in(fn, actor)) {
915 status = index_add(fn, actor);
916
917 srv_debug(1, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn));
918 }
919
920 return status;
921}
922
923
924int object_unadmire(const char *id, const char *actor, int like)
925/* actor no longer likes or announces this object */
926{
927 int status;
928 xs *fn = _object_fn(id);
929
930 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
931
932 status = index_del(fn, actor);
933
934 if (valid_status(status))
935 index_gc(fn);
936
937 srv_debug(0,
938 xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
939
940 return status;
941}
942
943
944int _object_user_cache(snac *snac, const char *id, const char *cachedir, int del)
945/* adds or deletes from a user cache */
946{
947 xs *ofn = _object_fn(id);
948 xs *l = xs_split(ofn, "/");
949 xs *cfn = xs_fmt("%s/%s/%s", snac->basedir, cachedir, xs_list_get(l, -1));
950 xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir);
951 int ret;
952
953 if (del) {
954 ret = unlink(cfn);
955 index_del(idx, id);
956 }
957 else {
958 if ((ret = link(ofn, cfn)) != -1)
959 index_add(idx, id);
960 }
961
962 return ret;
963}
964
965
966int object_user_cache_add(snac *snac, const char *id, const char *cachedir)
967/* caches an object into a user cache */
968{
969 return _object_user_cache(snac, id, cachedir, 0);
970}
971
972
973int object_user_cache_del(snac *snac, const char *id, const char *cachedir)
974/* deletes an object from a user cache */
975{
976 return _object_user_cache(snac, id, cachedir, 1);
977}
978
979
980int object_user_cache_in(snac *snac, const char *id, const char *cachedir)
981/* checks if an object is stored in a cache */
982{
983 xs *md5 = xs_md5_hex(id, strlen(id));
984 xs *cfn = xs_fmt("%s/%s/%s.json", snac->basedir, cachedir, md5);
985
986 return !!(mtime(cfn) != 0.0);
987}
988
989
990xs_list *object_user_cache_list(snac *snac, const char *cachedir, int max, int inv)
991/* returns the objects in a cache as a list */
992{
993 xs *idx = xs_fmt("%s/%s.idx", snac->basedir, cachedir);
994 return inv ? index_list_desc(idx, 0, max) : index_list(idx, max);
995}
996
997
998/** specialized functions **/
999
1000/** followers **/
1001
1002int follower_add(snac *snac, const char *actor)
1003/* adds a follower */
1004{
1005 int ret = object_user_cache_add(snac, actor, "followers");
1006
1007 snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
1008
1009 return ret == -1 ? 500 : 200;
1010}
1011
1012
1013int follower_del(snac *snac, const char *actor)
1014/* deletes a follower */
1015{
1016 int ret = object_user_cache_del(snac, actor, "followers");
1017
1018 snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
1019
1020 return ret == -1 ? 404 : 200;
1021}
1022
1023
1024int follower_check(snac *snac, const char *actor)
1025/* checks if someone is a follower */
1026{
1027 return object_user_cache_in(snac, actor, "followers");
1028}
1029
1030
1031xs_list *follower_list(snac *snac)
1032/* returns the list of followers */
1033{
1034 xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1035 xs_list *fwers = xs_list_new();
1036 char *p, *v;
1037
1038 /* resolve the list of md5 to be a list of actors */
1039 p = list;
1040 while (xs_list_iter(&p, &v)) {
1041 xs *a_obj = NULL;
1042
1043 if (valid_status(object_get_by_md5(v, &a_obj))) {
1044 const char *actor = xs_dict_get(a_obj, "id");
1045
1046 if (!xs_is_null(actor)) {
1047 /* check if the actor is still cached */
1048 xs *fn = xs_fmt("%s/followers/%s.json", snac->basedir, v);
1049
1050 if (mtime(fn) > 0.0)
1051 fwers = xs_list_append(fwers, actor);
1052 }
1053 }
1054 }
1055
1056 return fwers;
1057}
1058
1059
1060/** timeline **/
1061
1062double timeline_mtime(snac *snac)
1063{
1064 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1065 return mtime(fn);
1066}
1067
1068
1069int timeline_touch(snac *snac)
1070/* changes the date of the timeline index */
1071{
1072 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1073 return utimes(fn, NULL);
1074}
1075
1076
1077xs_str *timeline_fn_by_md5(snac *snac, const char *md5)
1078/* get the filename of an entry by md5 from any timeline */
1079{
1080 xs_str *fn = NULL;
1081
1082 if (xs_is_hex(md5) && strlen(md5) == 32) {
1083 fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
1084
1085 if (mtime(fn) == 0.0) {
1086 fn = xs_free(fn);
1087 fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
1088
1089 if (mtime(fn) == 0.0)
1090 fn = xs_free(fn);
1091 }
1092 }
1093
1094 return fn;
1095}
1096
1097
1098int timeline_here(snac *snac, const char *md5)
1099/* checks if an object is in the user cache */
1100{
1101 xs *fn = timeline_fn_by_md5(snac, md5);
1102
1103 return !(fn == NULL);
1104}
1105
1106
1107int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
1108/* gets a message from the timeline */
1109{
1110 int status = 404;
1111 FILE *f = NULL;
1112
1113 xs *fn = timeline_fn_by_md5(snac, md5);
1114
1115 if (fn != NULL && (f = fopen(fn, "r")) != NULL) {
1116 *msg = xs_json_load(f);
1117 fclose(f);
1118
1119 if (*msg != NULL)
1120 status = 200;
1121 }
1122
1123 return status;
1124}
1125
1126
1127int timeline_del(snac *snac, char *id)
1128/* deletes a message from the timeline */
1129{
1130 /* delete from the user's caches */
1131 object_user_cache_del(snac, id, "public");
1132 object_user_cache_del(snac, id, "private");
1133
1134 /* try to delete the object if it's not used elsewhere */
1135 return object_del_if_unref(id);
1136}
1137
1138
1139void timeline_update_indexes(snac *snac, const char *id)
1140/* updates the indexes */
1141{
1142 object_user_cache_add(snac, id, "private");
1143
1144 if (xs_startswith(id, snac->actor)) {
1145 xs *msg = NULL;
1146
1147 if (valid_status(object_get(id, &msg))) {
1148 /* if its ours and is public, also store in public */
1149 if (is_msg_public(msg)) {
1150 object_user_cache_add(snac, id, "public");
1151
1152 /* also add it to the instance public timeline */
1153 xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
1154 index_add(ipt, id);
1155 }
1156 }
1157 }
1158}
1159
1160
1161int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
1162/* adds a message to the timeline */
1163{
1164 int ret = object_add(id, o_msg);
1165 timeline_update_indexes(snac, id);
1166
1167 tag_index(id, o_msg);
1168
1169 list_distribute(snac, NULL, o_msg);
1170
1171 snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
1172
1173 return ret;
1174}
1175
1176
1177int timeline_admire(snac *snac, const char *id, const char *admirer, int like)
1178/* updates a timeline entry with a new admiration */
1179{
1180 /* if we are admiring this, add to both timelines */
1181 if (!like && strcmp(admirer, snac->actor) == 0) {
1182 object_user_cache_add(snac, id, "public");
1183 object_user_cache_add(snac, id, "private");
1184 }
1185
1186 int ret = object_admire(id, admirer, like);
1187
1188 snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
1189 like ? "Like" : "Announce", id, admirer));
1190
1191 return ret;
1192}
1193
1194
1195xs_list *timeline_top_level(snac *snac, xs_list *list)
1196/* returns the top level md5 entries from this index */
1197{
1198 xs_set seen;
1199 xs_list *p;
1200 xs_str *v;
1201
1202 xs_set_init(&seen);
1203
1204 p = list;
1205 while (xs_list_iter(&p, &v)) {
1206 char line[256] = "";
1207
1208 strncpy(line, v, sizeof(line));
1209
1210 for (;;) {
1211 char line2[256];
1212
1213 /* if it doesn't have a parent, use this */
1214 if (!object_parent(line, line2, sizeof(line2)))
1215 break;
1216
1217 /* well, there is a parent... but is it here? */
1218 if (!timeline_here(snac, line2))
1219 break;
1220
1221 /* it's here! try again with its own parent */
1222 strncpy(line, line2, sizeof(line));
1223 }
1224
1225 xs_set_add(&seen, line);
1226 }
1227
1228 return xs_set_result(&seen);
1229}
1230
1231
1232xs_list *timeline_simple_list(snac *snac, const char *idx_name, int skip, int show)
1233/* returns a timeline (with all entries) */
1234{
1235 xs *idx = xs_fmt("%s/%s.idx", snac->basedir, idx_name);
1236
1237 return index_list_desc(idx, skip, show);
1238}
1239
1240
1241xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
1242/* returns a timeline (only top level entries) */
1243{
1244 int c_max;
1245
1246 /* maximum number of items in the timeline */
1247 c_max = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
1248
1249 /* never more timeline entries than the configured maximum */
1250 if (show > c_max)
1251 show = c_max;
1252
1253 xs *list = timeline_simple_list(snac, idx_name, skip, show);
1254
1255 return timeline_top_level(snac, list);
1256}
1257
1258
1259xs_list *timeline_instance_list(int skip, int show)
1260/* returns the timeline for the full instance */
1261{
1262 xs *idx = xs_fmt("%s/public.idx", srv_basedir);
1263
1264 return index_list_desc(idx, skip, show);
1265}
1266
1267
1268/** following **/
1269
1270/* this needs special treatment and cannot use the object db as is,
1271 with a link to a cached author, because we need the Follow object
1272 in case we need to unfollow (Undo + original Follow) */
1273
1274xs_str *_following_fn(snac *snac, const char *actor)
1275{
1276 xs *md5 = xs_md5_hex(actor, strlen(actor));
1277 return xs_fmt("%s/following/%s.json", snac->basedir, md5);
1278}
1279
1280
1281int following_add(snac *snac, const char *actor, const xs_dict *msg)
1282/* adds to the following list */
1283{
1284 int ret = 201; /* created */
1285 xs *fn = _following_fn(snac, actor);
1286 FILE *f;
1287 xs *p_object = NULL;
1288
1289 if (valid_status(following_get(snac, actor, &p_object))) {
1290 /* object already exists; if it's of type Accept,
1291 the actor is already being followed and confirmed,
1292 so do nothing */
1293 char *type = xs_dict_get(p_object, "type");
1294
1295 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1296 snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
1297 return 200;
1298 }
1299 }
1300
1301 if ((f = fopen(fn, "w")) != NULL) {
1302 xs_json_dump(msg, 4, f);
1303 fclose(f);
1304
1305 /* get the filename of the actor object */
1306 xs *actor_fn = _object_fn(actor);
1307
1308 /* increase its reference count */
1309 fn = xs_replace_i(fn, ".json", "_a.json");
1310 link(actor_fn, fn);
1311 }
1312 else
1313 ret = 500;
1314
1315 snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
1316
1317 return ret;
1318}
1319
1320
1321int following_del(snac *snac, const char *actor)
1322/* we're not following this actor any longer */
1323{
1324 xs *fn = _following_fn(snac, actor);
1325
1326 snac_debug(snac, 2, xs_fmt("following_del %s %s", actor, fn));
1327
1328 unlink(fn);
1329
1330 /* also delete the reference to the author */
1331 fn = xs_replace_i(fn, ".json", "_a.json");
1332 unlink(fn);
1333
1334 return 200;
1335}
1336
1337
1338int following_check(snac *snac, const char *actor)
1339/* checks if we are following this actor */
1340{
1341 xs *fn = _following_fn(snac, actor);
1342
1343 return !!(mtime(fn) != 0.0);
1344}
1345
1346
1347int following_get(snac *snac, const char *actor, xs_dict **data)
1348/* returns the 'Follow' object */
1349{
1350 xs *fn = _following_fn(snac, actor);
1351 FILE *f;
1352 int status = 200;
1353
1354 if ((f = fopen(fn, "r")) != NULL) {
1355 *data = xs_json_load(f);
1356 fclose(f);
1357 }
1358 else
1359 status = 404;
1360
1361 return status;
1362}
1363
1364
1365xs_list *following_list(snac *snac)
1366/* returns the list of people being followed */
1367{
1368 xs *spec = xs_fmt("%s/following/" "*.json", snac->basedir);
1369 xs *glist = xs_glob(spec, 0, 0);
1370 xs_list *p;
1371 xs_str *v;
1372 xs_list *list = xs_list_new();
1373
1374 /* iterate the list of files */
1375 p = glist;
1376 while (xs_list_iter(&p, &v)) {
1377 FILE *f;
1378
1379 /* load the follower data */
1380 if ((f = fopen(v, "r")) != NULL) {
1381 xs *o = xs_json_load(f);
1382 fclose(f);
1383
1384 if (o != NULL) {
1385 const char *type = xs_dict_get(o, "type");
1386
1387 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1388 const char *actor = xs_dict_get(o, "actor");
1389
1390 if (!xs_is_null(actor)) {
1391 list = xs_list_append(list, actor);
1392
1393 /* check if there is a link to the actor object */
1394 xs *v2 = xs_replace(v, ".json", "_a.json");
1395
1396 if (mtime(v2) == 0.0) {
1397 /* no; add a link to it */
1398 xs *actor_fn = _object_fn(actor);
1399 link(actor_fn, v2);
1400 }
1401 }
1402 }
1403 }
1404 }
1405 }
1406
1407 return list;
1408}
1409
1410
1411xs_str *_muted_fn(snac *snac, const char *actor)
1412{
1413 xs *md5 = xs_md5_hex(actor, strlen(actor));
1414 return xs_fmt("%s/muted/%s", snac->basedir, md5);
1415}
1416
1417
1418void mute(snac *snac, const char *actor)
1419/* mutes a moron */
1420{
1421 xs *fn = _muted_fn(snac, actor);
1422 FILE *f;
1423
1424 if ((f = fopen(fn, "w")) != NULL) {
1425 fprintf(f, "%s\n", actor);
1426 fclose(f);
1427
1428 snac_debug(snac, 2, xs_fmt("muted %s %s", actor, fn));
1429 }
1430}
1431
1432
1433void unmute(snac *snac, const char *actor)
1434/* actor is no longer a moron */
1435{
1436 xs *fn = _muted_fn(snac, actor);
1437
1438 unlink(fn);
1439
1440 snac_debug(snac, 2, xs_fmt("unmuted %s %s", actor, fn));
1441}
1442
1443
1444int is_muted(snac *snac, const char *actor)
1445/* check if someone is muted */
1446{
1447 xs *fn = _muted_fn(snac, actor);
1448
1449 return !!(mtime(fn) != 0.0);
1450}
1451
1452
1453/** pinning **/
1454
1455xs_str *_pinned_fn(snac *user, const char *id)
1456{
1457 xs *md5 = xs_md5_hex(id, strlen(id));
1458 return xs_fmt("%s/pinned/%s.json", user->basedir, md5);
1459}
1460
1461
1462int is_pinned(snac *user, const char *id)
1463/* returns true if this note is pinned */
1464{
1465 xs *fn = _pinned_fn(user, id);
1466 return !!(mtime(fn) != 0.0);
1467}
1468
1469
1470int is_pinned_by_md5(snac *user, const char *md5)
1471{
1472 xs *fn = xs_fmt("%s/pinned/%s.json", user->basedir, md5);
1473 return !!(mtime(fn) != 0.0);
1474}
1475
1476
1477int pin(snac *user, const char *id)
1478/* pins a message */
1479{
1480 int ret = -2;
1481
1482 if (xs_startswith(id, user->actor)) {
1483 if (is_pinned(user, id))
1484 ret = -3;
1485 else {
1486 /* create the subfolder, if it does not exist */
1487 xs *fn = xs_fmt("%s/pinned/", user->basedir);
1488 mkdirx(fn);
1489
1490 ret = object_user_cache_add(user, id, "pinned");
1491 }
1492 }
1493
1494 return ret;
1495}
1496
1497
1498int unpin(snac *user, const char *id)
1499/* unpin a message */
1500{
1501 int ret = object_user_cache_del(user, id, "pinned");
1502
1503 if (ret != -1) {
1504 /* delete from the index */
1505 xs *idx = xs_fmt("%s/pinned.idx", user->basedir);
1506 index_del(idx, id);
1507 }
1508
1509 return ret;
1510}
1511
1512
1513xs_list *pinned_list(snac *user)
1514/* return the lists of pinned posts */
1515{
1516 return object_user_cache_list(user, "pinned", XS_ALL, 1);
1517}
1518
1519
1520xs_str *_hidden_fn(snac *snac, const char *id)
1521{
1522 xs *md5 = xs_md5_hex(id, strlen(id));
1523 return xs_fmt("%s/hidden/%s", snac->basedir, md5);
1524}
1525
1526
1527void hide(snac *snac, const char *id)
1528/* hides a message tree */
1529{
1530 xs *fn = _hidden_fn(snac, id);
1531 FILE *f;
1532
1533 if ((f = fopen(fn, "w")) != NULL) {
1534 fprintf(f, "%s\n", id);
1535 fclose(f);
1536
1537 snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
1538
1539 /* hide all the children */
1540 xs *chld = object_children(id);
1541 char *p, *v;
1542
1543 p = chld;
1544 while (xs_list_iter(&p, &v)) {
1545 xs *co = NULL;
1546
1547 /* resolve to get the id */
1548 if (valid_status(object_get_by_md5(v, &co))) {
1549 if ((v = xs_dict_get(co, "id")) != NULL)
1550 hide(snac, v);
1551 }
1552 }
1553 }
1554}
1555
1556
1557int is_hidden(snac *snac, const char *id)
1558/* check is id is hidden */
1559{
1560 xs *fn = _hidden_fn(snac, id);
1561
1562 return !!(mtime(fn) != 0.0);
1563}
1564
1565
1566int actor_add(const char *actor, xs_dict *msg)
1567/* adds an actor */
1568{
1569 return object_add_ow(actor, msg);
1570}
1571
1572
1573int actor_get(const char *actor, xs_dict **data)
1574/* returns an already downloaded actor */
1575{
1576 int status = 200;
1577 xs_dict *d = NULL;
1578
1579 if (xs_startswith(actor, srv_baseurl)) {
1580 /* it's a (possible) local user */
1581 xs *l = xs_split(actor, "/");
1582 const char *uid = xs_list_get(l, -1);
1583 snac user;
1584
1585 if (!xs_is_null(uid) && user_open(&user, uid)) {
1586 if (data)
1587 *data = msg_actor(&user);
1588
1589 user_free(&user);
1590 return 200;
1591 }
1592 else
1593 return 404;
1594 }
1595
1596 /* read the object */
1597 if (!valid_status(status = object_get(actor, &d))) {
1598 d = xs_free(d);
1599 return status;
1600 }
1601
1602 /* if the object is corrupted, discard it */
1603 if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
1604 srv_debug(1, xs_fmt("corrupted actor object %s", actor));
1605 d = xs_free(d);
1606 return 404;
1607 }
1608
1609 if (data)
1610 *data = d;
1611 else
1612 d = xs_free(d);
1613
1614 xs *fn = _object_fn(actor);
1615 double max_time;
1616
1617 /* maximum time for the actor data to be considered stale */
1618 max_time = 3600.0 * 36.0;
1619
1620 if (mtime(fn) + max_time < (double) time(NULL)) {
1621 /* actor data exists but also stinks */
1622 status = 205; /* "205: Reset Content" "110: Response Is Stale" */
1623 }
1624
1625 return status;
1626}
1627
1628
1629int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
1630/* gets an actor and requests a refresh if it's stale */
1631{
1632 int status = actor_get(actor, data);
1633
1634 if (status == 205 && user && !xs_startswith(actor, srv_baseurl))
1635 enqueue_actor_refresh(user, actor, 0);
1636
1637 return status;
1638}
1639
1640
1641/** user limiting (announce blocks) **/
1642
1643int limited(snac *user, const char *id, int cmd)
1644/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
1645{
1646 int ret = 0;
1647 xs *dir = xs_fmt("%s/limited", user->basedir);
1648 xs *md5 = xs_md5_hex(id, strlen(id));
1649 xs *fn = xs_fmt("%s/%s", dir, md5);
1650
1651 switch (cmd) {
1652 case 0: /** check **/
1653 ret = !!(mtime(fn) > 0.0);
1654 break;
1655
1656 case 1: /** limit **/
1657 mkdirx(dir);
1658
1659 if (mtime(fn) > 0.0)
1660 ret = -1;
1661 else {
1662 FILE *f;
1663
1664 if ((f = fopen(fn, "w")) != NULL) {
1665 fprintf(f, "%s\n", id);
1666 fclose(f);
1667 }
1668 else
1669 ret = -2;
1670 }
1671 break;
1672
1673 case 2: /** unlimit **/
1674 if (mtime(fn) > 0.0)
1675 ret = unlink(fn);
1676 else
1677 ret = -1;
1678 break;
1679 }
1680
1681 return ret;
1682}
1683
1684
1685/** tag indexing **/
1686
1687void tag_index(const char *id, const xs_dict *obj)
1688/* update the tag indexes for this object */
1689{
1690 xs_list *tags = xs_dict_get(obj, "tag");
1691
1692 if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
1693 xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
1694
1695 mkdirx(g_tag_dir);
1696
1697 xs_dict *v;
1698 while (xs_list_iter(&tags, &v)) {
1699 char *type = xs_dict_get(v, "type");
1700 char *name = xs_dict_get(v, "name");
1701
1702 if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
1703 while (*name == '#' || *name == '@')
1704 name++;
1705
1706 if (*name == '\0')
1707 continue;
1708
1709 name = xs_tolower_i(name);
1710
1711 xs *md5_tag = xs_md5_hex(name, strlen(name));
1712 xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
1713 mkdirx(tag_dir);
1714
1715 xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
1716 index_add(g_tag_idx, id);
1717
1718 FILE *f;
1719 xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
1720 if ((f = fopen(g_tag_name, "w")) != NULL) {
1721 fprintf(f, "%s\n", name);
1722 fclose(f);
1723 }
1724
1725 srv_debug(0, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
1726 }
1727 }
1728 }
1729}
1730
1731
1732xs_list *tag_search(char *tag, int skip, int show)
1733/* returns the list of posts tagged with tag */
1734{
1735 if (*tag == '#')
1736 tag++;
1737
1738 xs *lw_tag = xs_tolower_i(xs_dup(tag));
1739 xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag));
1740 xs *idx = xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
1741
1742 return index_list_desc(idx, skip, show);
1743}
1744
1745
1746/** lists **/
1747
1748xs_val *list_maint(snac *user, const char *list, int op)
1749/* list maintenance */
1750{
1751 xs_val *l = NULL;
1752
1753 switch (op) {
1754 case 0: /** list of lists **/
1755 {
1756 FILE *f;
1757 xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
1758 xs *ls = xs_glob(spec, 0, 0);
1759 int c = 0;
1760 char *v;
1761
1762 l = xs_list_new();
1763
1764 while (xs_list_next(ls, &v, &c)) {
1765 if ((f = fopen(v, "r")) != NULL) {
1766 xs *title = xs_readline(f);
1767 fclose(f);
1768
1769 title = xs_strip_i(title);
1770
1771 xs *v2 = xs_replace(v, ".id", "");
1772 xs *l2 = xs_split(v2, "/");
1773
1774 /* return [ list_id, list_title ] */
1775 l = xs_list_append(l, xs_list_append(xs_list_new(), xs_list_get(l2, -1), title));
1776 }
1777 }
1778 }
1779
1780 break;
1781
1782 case 1: /** create new list (list is the name) **/
1783 {
1784 xs *lol = list_maint(user, NULL, 0);
1785 int c = 0;
1786 xs_list *v;
1787 int add = 1;
1788
1789 /* check if this list name already exists */
1790 while (xs_list_next(lol, &v, &c)) {
1791 if (strcmp(xs_list_get(v, 1), list) == 0) {
1792 add = 0;
1793 break;
1794 }
1795 }
1796
1797 if (add) {
1798 FILE *f;
1799 xs *dir = xs_fmt("%s/list/", user->basedir);
1800 xs *id = xs_fmt("%010x", time(NULL));
1801
1802 mkdirx(dir);
1803
1804 xs *fn = xs_fmt("%s%s.id", dir, id);
1805
1806 if ((f = fopen(fn, "w")) != NULL) {
1807 fprintf(f, "%s\n", list);
1808 fclose(f);
1809 }
1810
1811 l = xs_stock(XSTYPE_TRUE);
1812 }
1813 else
1814 l = xs_stock(XSTYPE_FALSE);
1815 }
1816
1817 break;
1818
1819 case 2: /** delete list (list is the id) **/
1820 {
1821 if (xs_is_hex(list)) {
1822 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
1823 unlink(fn);
1824
1825 fn = xs_replace_i(fn, ".id", ".lst");
1826 unlink(fn);
1827
1828 fn = xs_replace_i(fn, ".lst", ".idx");
1829 unlink(fn);
1830 }
1831 }
1832
1833 break;
1834
1835 case 3: /** get list name **/
1836 if (xs_is_hex(list)) {
1837 FILE *f;
1838 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
1839
1840 if ((f = fopen(fn, "r")) != NULL) {
1841 l = xs_strip_i(xs_readline(f));
1842 fclose(f);
1843 }
1844 }
1845
1846 break;
1847 }
1848
1849 return l;
1850}
1851
1852
1853xs_list *list_timeline(snac *user, const char *list, int skip, int show)
1854/* returns the timeline of a list */
1855{
1856 xs_list *l = NULL;
1857
1858 if (!xs_is_hex(list))
1859 return NULL;
1860
1861 xs *fn = xs_fmt("%s/list/%s.idx", user->basedir, list);
1862
1863 if (mtime(fn) > 0.0)
1864 l = index_list_desc(fn, skip, show);
1865
1866 return l;
1867}
1868
1869
1870xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
1871/* list content management */
1872{
1873 xs_val *l = NULL;
1874
1875 if (!xs_is_hex(list))
1876 return NULL;
1877
1878 if (actor_md5 != NULL && !xs_is_hex(actor_md5))
1879 return NULL;
1880
1881 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
1882
1883 switch (op) {
1884 case 0: /** list content **/
1885 l = index_list(fn, XS_ALL);
1886
1887 break;
1888
1889 case 1: /** append actor to list **/
1890 if (actor_md5 != NULL) {
1891 if (!index_in(fn, actor_md5))
1892 index_add_md5(fn, actor_md5);
1893 }
1894
1895 break;
1896
1897 case 2: /** delete actor from list **/
1898 if (actor_md5 != NULL)
1899 index_del_md5(fn, actor_md5);
1900
1901 break;
1902
1903 default:
1904 srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
1905 break;
1906 }
1907
1908 return l;
1909}
1910
1911
1912void list_distribute(snac *user, const char *who, const xs_dict *post)
1913/* distributes the post to all appropriate lists */
1914{
1915 char *id = xs_dict_get(post, "id");
1916
1917 /* if who is not set, use the attributedTo in the message */
1918 if (xs_is_null(who))
1919 who = get_atto(post);
1920
1921 if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
1922 xs *a_md5 = xs_md5_hex(who, strlen(who));
1923 xs *i_md5 = xs_md5_hex(id, strlen(id));
1924 xs *spec = xs_fmt("%s/list/" "*.lst", user->basedir);
1925 xs *ls = xs_glob(spec, 0, 0);
1926 int c = 0;
1927 char *v;
1928
1929 while (xs_list_next(ls, &v, &c)) {
1930 /* is the actor in this list? */
1931 if (index_in_md5(v, a_md5)) {
1932 /* it is; add post md5 to its timeline */
1933 xs *idx = xs_replace(v, ".lst", ".idx");
1934 index_add_md5(idx, i_md5);
1935
1936 snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
1937 }
1938 }
1939 }
1940}
1941
1942
1943/** static data **/
1944
1945static int _load_raw_file(const char *fn, xs_val **data, int *size,
1946 const char *inm, xs_str **etag)
1947/* loads a cached file */
1948{
1949 int status = 404;
1950
1951 if (fn) {
1952 double tm = mtime(fn);
1953
1954 if (tm > 0.0) {
1955 /* file exists; build the etag */
1956 xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
1957
1958 /* if if-none-match is set, check if it's the same */
1959 if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
1960 /* client has the newest version */
1961 status = 304;
1962 }
1963 else {
1964 /* newer or never downloaded; read the full file */
1965 FILE *f;
1966
1967 if ((f = fopen(fn, "rb")) != NULL) {
1968 *size = XS_ALL;
1969 *data = xs_read(f, size);
1970 fclose(f);
1971
1972 status = 200;
1973 }
1974 }
1975
1976 /* if caller wants the etag, return it */
1977 if (etag != NULL)
1978 *etag = xs_dup(e);
1979
1980 srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
1981 }
1982 }
1983
1984 return status;
1985}
1986
1987
1988xs_str *_static_fn(snac *snac, const char *id)
1989/* gets the filename for a static file */
1990{
1991 if (strchr(id, '/'))
1992 return NULL;
1993 else
1994 return xs_fmt("%s/static/%s", snac->basedir, id);
1995}
1996
1997
1998int static_get(snac *snac, const char *id, xs_val **data, int *size,
1999 const char *inm, xs_str **etag)
2000/* returns static content */
2001{
2002 xs *fn = _static_fn(snac, id);
2003
2004 return _load_raw_file(fn, data, size, inm, etag);
2005}
2006
2007
2008void static_put(snac *snac, const char *id, const char *data, int size)
2009/* writes status content */
2010{
2011 xs *fn = _static_fn(snac, id);
2012 FILE *f;
2013
2014 if (fn && (f = fopen(fn, "wb")) != NULL) {
2015 fwrite(data, size, 1, f);
2016 fclose(f);
2017 }
2018}
2019
2020
2021void static_put_meta(snac *snac, const char *id, const char *str)
2022/* puts metadata (i.e. a media description string) to id */
2023{
2024 xs *fn = _static_fn(snac, id);
2025
2026 if (fn) {
2027 fn = xs_str_cat(fn, ".txt");
2028 FILE *f;
2029
2030 if ((f = fopen(fn, "w")) != NULL) {
2031 fprintf(f, "%s\n", str);
2032 fclose(f);
2033 }
2034 }
2035}
2036
2037
2038xs_str *static_get_meta(snac *snac, const char *id)
2039/* gets metadata from a media */
2040{
2041 xs *fn = _static_fn(snac, id);
2042 xs_str *r = NULL;
2043
2044 if (fn) {
2045 fn = xs_str_cat(fn, ".txt");
2046 FILE *f;
2047
2048 if ((f = fopen(fn, "r")) != NULL) {
2049 r = xs_strip_i(xs_readline(f));
2050 fclose(f);
2051 }
2052 }
2053 else
2054 r = xs_str_new("");
2055
2056 return r;
2057}
2058
2059
2060/** history **/
2061
2062xs_str *_history_fn(snac *snac, const char *id)
2063/* gets the filename for the history */
2064{
2065 if (strchr(id, '/'))
2066 return NULL;
2067 else
2068 return xs_fmt("%s/history/%s", snac->basedir, id);
2069}
2070
2071
2072double history_mtime(snac *snac, const char *id)
2073{
2074 double t = 0.0;
2075 xs *fn = _history_fn(snac, id);
2076
2077 if (fn != NULL)
2078 t = mtime(fn);
2079
2080 return t;
2081}
2082
2083
2084void history_add(snac *snac, const char *id, const char *content, int size,
2085 xs_str **etag)
2086/* adds something to the history */
2087{
2088 xs *fn = _history_fn(snac, id);
2089 FILE *f;
2090
2091 if (fn && (f = fopen(fn, "w")) != NULL) {
2092 fwrite(content, size, 1, f);
2093 fclose(f);
2094
2095 if (etag) {
2096 double tm = mtime(fn);
2097 *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2098 }
2099 }
2100}
2101
2102
2103int history_get(snac *snac, const char *id, xs_str **content, int *size,
2104 const char *inm, xs_str **etag)
2105{
2106 xs *fn = _history_fn(snac, id);
2107
2108 return _load_raw_file(fn, content, size, inm, etag);
2109}
2110
2111
2112int history_del(snac *snac, const char *id)
2113{
2114 xs *fn = _history_fn(snac, id);
2115
2116 if (fn)
2117 return unlink(fn);
2118 else
2119 return -1;
2120}
2121
2122
2123xs_list *history_list(snac *snac)
2124{
2125 xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2126
2127 return xs_glob(spec, 1, 1);
2128}
2129
2130
2131void lastlog_write(snac *snac, const char *source)
2132/* writes the last time the user logged in */
2133{
2134 xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2135 FILE *f;
2136
2137 if ((f = fopen(fn, "w")) != NULL) {
2138 fprintf(f, "%lf %s\n", ftime(), source);
2139 fclose(f);
2140 }
2141}
2142
2143
2144/** inbox collection **/
2145
2146void inbox_add(const char *inbox)
2147/* collects a shared inbox */
2148{
2149 /* don't collect ourselves */
2150 if (xs_startswith(inbox, srv_baseurl))
2151 return;
2152
2153 xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2154 xs *fn = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2155 FILE *f;
2156
2157 if ((f = fopen(fn, "w")) != NULL) {
2158 fprintf(f, "%s\n", inbox);
2159 fclose(f);
2160 }
2161}
2162
2163
2164void inbox_add_by_actor(const xs_dict *actor)
2165/* collects an actor's shared inbox, if it has one */
2166{
2167 char *v;
2168
2169 if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2170 !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2171 /* only collect this inbox if its instance is not blocked */
2172 if (!is_instance_blocked(v))
2173 inbox_add(v);
2174 }
2175}
2176
2177
2178xs_list *inbox_list(void)
2179/* returns the collected inboxes as a list */
2180{
2181 xs_list *ibl = xs_list_new();
2182 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2183 xs *files = xs_glob(spec, 0, 0);
2184 xs_list *p = files;
2185 xs_val *v;
2186
2187 while (xs_list_iter(&p, &v)) {
2188 FILE *f;
2189
2190 if ((f = fopen(v, "r")) != NULL) {
2191 xs *line = xs_readline(f);
2192
2193 if (line) {
2194 line = xs_strip_i(line);
2195 ibl = xs_list_append(ibl, line);
2196 }
2197
2198 fclose(f);
2199 }
2200 }
2201
2202 return ibl;
2203}
2204
2205
2206/** instance-wide operations **/
2207
2208xs_str *_instance_block_fn(const char *instance)
2209{
2210 xs *s = xs_replace(instance, "http:/" "/", "");
2211 xs *s1 = xs_replace(s, "https:/" "/", "");
2212 xs *l = xs_split(s1, "/");
2213 char *p = xs_list_get(l, 0);
2214 xs *md5 = xs_md5_hex(p, strlen(p));
2215
2216 return xs_fmt("%s/block/%s", srv_basedir, md5);
2217}
2218
2219
2220int is_instance_blocked(const char *instance)
2221{
2222 xs *fn = _instance_block_fn(instance);
2223
2224 return !!(mtime(fn) != 0.0);
2225}
2226
2227
2228int instance_block(const char *instance)
2229/* blocks a full instance */
2230{
2231 int ret;
2232
2233 /* create the subdir */
2234 xs *dir = xs_fmt("%s/block/", srv_basedir);
2235 mkdirx(dir);
2236
2237 if (!is_instance_blocked(instance)) {
2238 xs *fn = _instance_block_fn(instance);
2239 FILE *f;
2240
2241 if ((f = fopen(fn, "w")) != NULL) {
2242 fprintf(f, "%s\n", instance);
2243 fclose(f);
2244
2245 ret = 0;
2246 }
2247 else
2248 ret = -1;
2249 }
2250 else
2251 ret = -2;
2252
2253 return ret;
2254}
2255
2256
2257int instance_unblock(const char *instance)
2258/* unblocks a full instance */
2259{
2260 int ret;
2261
2262 if (is_instance_blocked(instance)) {
2263 xs *fn = _instance_block_fn(instance);
2264 ret = unlink(fn);
2265 }
2266 else
2267 ret = -2;
2268
2269 return ret;
2270}
2271
2272
2273/** operations by content **/
2274
2275int content_match(const char *file, const xs_dict *msg)
2276/* checks if a message's content matches any of the regexes in file */
2277/* file format: one regex per line */
2278{
2279 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2280 FILE *f;
2281 int r = 0;
2282 char *v = xs_dict_get(msg, "content");
2283
2284 if (xs_type(v) == XSTYPE_STRING && *v) {
2285 if ((f = fopen(fn, "r")) != NULL) {
2286 srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2287
2288 /* massage content (strip HTML tags, etc.) */
2289 xs *c = xs_regex_replace(v, "<[^>]+>", " ");
2290 c = xs_regex_replace_i(c, " {2,}", " ");
2291 c = xs_tolower_i(c);
2292
2293 while (!r && !feof(f)) {
2294 xs *rx = xs_strip_i(xs_readline(f));
2295
2296 if (*rx && xs_regex_match(c, rx)) {
2297 srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2298 r = 1;
2299 }
2300 }
2301
2302 fclose(f);
2303 }
2304 }
2305
2306 return r;
2307}
2308
2309
2310xs_list *content_search(snac *user, const char *regex,
2311 int priv, int skip, int show, int max_secs, int *timeout)
2312/* returns a list of posts which content matches the regex */
2313{
2314 if (regex == NULL || *regex == '\0')
2315 return xs_list_new();
2316
2317 xs *i_regex = xs_tolower_i(xs_dup(regex));
2318
2319 xs_set seen;
2320
2321 xs_set_init(&seen);
2322
2323 if (max_secs == 0)
2324 max_secs = 3;
2325
2326 time_t t = time(NULL) + max_secs;
2327 *timeout = 0;
2328
2329 /* iterate all timelines simultaneously */
2330 xs_list *tls[3] = {0};
2331 char *md5s[3] = {0};
2332 int c[3] = {0};
2333
2334 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */
2335 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2336 tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
2337
2338 /* first positioning */
2339 for (int n = 0; n < 3; n++)
2340 xs_list_next(tls[n], &md5s[n], &c[n]);
2341
2342 show += skip;
2343
2344 while (show > 0) {
2345 /* timeout? */
2346 if (time(NULL) > t) {
2347 *timeout = 1;
2348 break;
2349 }
2350
2351 /* find the newest post */
2352 int newest = -1;
2353 double mtime = 0.0;
2354
2355 for (int n = 0; n < 3; n++) {
2356 if (md5s[n] != NULL) {
2357 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2358 double mt = mtime(fn);
2359
2360 if (mt > mtime) {
2361 newest = n;
2362 mtime = mt;
2363 }
2364 }
2365 }
2366
2367 if (newest == -1)
2368 break;
2369
2370 char *md5 = md5s[newest];
2371
2372 /* advance the chosen timeline */
2373 if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2374 md5s[newest] = NULL;
2375
2376 xs *post = NULL;
2377
2378 if (!valid_status(object_get_by_md5(md5, &post)))
2379 continue;
2380
2381 if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2382 continue;
2383
2384 const char *id = xs_dict_get(post, "id");
2385
2386 if (id == NULL || is_hidden(user, id))
2387 continue;
2388
2389 char *content = xs_dict_get(post, "content");
2390
2391 if (xs_is_null(content))
2392 continue;
2393
2394 /* strip HTML */
2395 xs *c = xs_regex_replace(content, "<[^>]+>", " ");
2396 c = xs_regex_replace_i(c, " {2,}", " ");
2397 c = xs_tolower_i(c);
2398
2399 /* apply regex */
2400 if (xs_regex_match(c, i_regex)) {
2401 if (xs_set_add(&seen, md5) == 1)
2402 show--;
2403 }
2404 }
2405
2406 xs_list *r = xs_set_result(&seen);
2407
2408 if (skip) {
2409 /* BAD */
2410 while (skip--) {
2411 r = xs_list_del(r, 0);
2412 }
2413 }
2414
2415 xs_free(tls[0]);
2416 xs_free(tls[1]);
2417 xs_free(tls[2]);
2418
2419 return r;
2420}
2421
2422
2423/** notifications **/
2424
2425xs_str *notify_check_time(snac *snac, int reset)
2426/* gets or resets the latest notification check time */
2427{
2428 xs_str *t = NULL;
2429 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
2430 FILE *f;
2431
2432 if (reset) {
2433 if ((f = fopen(fn, "w")) != NULL) {
2434 t = tid(0);
2435 fprintf(f, "%s\n", t);
2436 fclose(f);
2437 }
2438 }
2439 else {
2440 if ((f = fopen(fn, "r")) != NULL) {
2441 t = xs_readline(f);
2442 fclose(f);
2443 }
2444 else
2445 /* never set before */
2446 t = xs_fmt("%16.6f", 0.0);
2447 }
2448
2449 return t;
2450}
2451
2452
2453void notify_add(snac *snac, const char *type, const char *utype,
2454 const char *actor, const char *objid)
2455/* adds a new notification */
2456{
2457 xs *ntid = tid(0);
2458 xs *fn = xs_fmt("%s/notify/", snac->basedir);
2459 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
2460 FILE *f;
2461
2462 /* create the directory */
2463 mkdirx(fn);
2464 fn = xs_str_cat(fn, ntid);
2465 fn = xs_str_cat(fn, ".json");
2466
2467 xs *noti = xs_dict_new();
2468
2469 noti = xs_dict_append(noti, "id", ntid);
2470 noti = xs_dict_append(noti, "type", type);
2471 noti = xs_dict_append(noti, "utype", utype);
2472 noti = xs_dict_append(noti, "actor", actor);
2473 noti = xs_dict_append(noti, "date", date);
2474
2475 if (!xs_is_null(objid))
2476 noti = xs_dict_append(noti, "objid", objid);
2477
2478 if ((f = fopen(fn, "w")) != NULL) {
2479 xs_json_dump(noti, 4, f);
2480 fclose(f);
2481 }
2482
2483 /* add it to the index if it already exists */
2484 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2485
2486 if (mtime(idx) != 0.0) {
2487 pthread_mutex_lock(&data_mutex);
2488
2489 if ((f = fopen(idx, "a")) != NULL) {
2490 fprintf(f, "%-32s\n", ntid);
2491 fclose(f);
2492 }
2493
2494 pthread_mutex_unlock(&data_mutex);
2495 }
2496}
2497
2498
2499xs_dict *notify_get(snac *snac, const char *id)
2500/* gets a notification */
2501{
2502 /* base file */
2503 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
2504
2505 /* strip spaces and add extension */
2506 fn = xs_strip_i(fn);
2507 fn = xs_str_cat(fn, ".json");
2508
2509 FILE *f;
2510 xs_dict *out = NULL;
2511
2512 if ((f = fopen(fn, "r")) != NULL) {
2513 out = xs_json_load(f);
2514 fclose(f);
2515 }
2516
2517 return out;
2518}
2519
2520
2521xs_list *notify_list(snac *snac, int skip, int show)
2522/* returns a list of notification ids */
2523{
2524 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2525
2526 if (mtime(idx) == 0.0) {
2527 /* create the index from scratch */
2528 FILE *f;
2529
2530 pthread_mutex_lock(&data_mutex);
2531
2532 if ((f = fopen(idx, "w")) != NULL) {
2533 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
2534 xs *lst = xs_glob(spec, 1, 0);
2535 xs_list *p = lst;
2536 char *v;
2537
2538 while (xs_list_iter(&p, &v)) {
2539 char *p = strrchr(v, '.');
2540 if (p) {
2541 *p = '\0';
2542 fprintf(f, "%-32s\n", v);
2543 }
2544 }
2545
2546 fclose(f);
2547 }
2548
2549 pthread_mutex_unlock(&data_mutex);
2550 }
2551
2552 return index_list_desc(idx, skip, show);
2553}
2554
2555
2556int notify_new_num(snac *snac)
2557/* counts the number of new notifications */
2558{
2559 xs *t = notify_check_time(snac, 0);
2560 xs *lst = notify_list(snac, 0, XS_ALL);
2561 int cnt = 0;
2562
2563 xs_list *p = lst;
2564 xs_str *v;
2565
2566 while (xs_list_iter(&p, &v)) {
2567 xs *id = xs_strip_i(xs_dup(v));
2568
2569 /* old? count no more */
2570 if (strcmp(id, t) < 0)
2571 break;
2572
2573 cnt++;
2574 }
2575
2576 return cnt;
2577}
2578
2579
2580void notify_clear(snac *snac)
2581/* clears all notifications */
2582{
2583 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
2584 xs *lst = xs_glob(spec, 0, 0);
2585 xs_list *p = lst;
2586 xs_str *v;
2587
2588 while (xs_list_iter(&p, &v))
2589 unlink(v);
2590
2591 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2592
2593 if (mtime(idx) != 0.0) {
2594 pthread_mutex_lock(&data_mutex);
2595 truncate(idx, 0);
2596 pthread_mutex_unlock(&data_mutex);
2597 }
2598}
2599
2600
2601/** the queue **/
2602
2603static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
2604/* writes safely to the queue */
2605{
2606 xs *tfn = xs_fmt("%s.tmp", fn);
2607 FILE *f;
2608
2609 if ((f = fopen(tfn, "w")) != NULL) {
2610 xs_json_dump(msg, 4, f);
2611 fclose(f);
2612
2613 rename(tfn, fn);
2614 }
2615
2616 return msg;
2617}
2618
2619
2620static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
2621/* creates a queue message */
2622{
2623 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
2624 xs *ntid = tid(retries * 60 * qrt);
2625 xs *rn = xs_number_new(retries);
2626
2627 xs_dict *qmsg = xs_dict_new();
2628
2629 qmsg = xs_dict_append(qmsg, "type", type);
2630 qmsg = xs_dict_append(qmsg, "message", msg);
2631 qmsg = xs_dict_append(qmsg, "retries", rn);
2632 qmsg = xs_dict_append(qmsg, "ntid", ntid);
2633
2634 return qmsg;
2635}
2636
2637
2638void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
2639/* enqueues an input message */
2640{
2641 xs *qmsg = _new_qmsg("input", msg, retries);
2642 char *ntid = xs_dict_get(qmsg, "ntid");
2643 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2644
2645 qmsg = xs_dict_append(qmsg, "req", req);
2646
2647 qmsg = _enqueue_put(fn, qmsg);
2648
2649 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
2650}
2651
2652
2653void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
2654/* enqueues an input message from the shared input */
2655{
2656 xs *qmsg = _new_qmsg("input", msg, retries);
2657 char *ntid = xs_dict_get(qmsg, "ntid");
2658 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2659
2660 qmsg = xs_dict_append(qmsg, "req", req);
2661
2662 qmsg = _enqueue_put(fn, qmsg);
2663
2664 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
2665}
2666
2667
2668void enqueue_output_raw(const char *keyid, const char *seckey,
2669 xs_dict *msg, xs_str *inbox, int retries, int p_status)
2670/* enqueues an output message to an inbox */
2671{
2672 xs *qmsg = _new_qmsg("output", msg, retries);
2673 char *ntid = xs_dict_get(qmsg, "ntid");
2674 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2675
2676 xs *ns = xs_number_new(p_status);
2677 qmsg = xs_dict_append(qmsg, "p_status", ns);
2678
2679 qmsg = xs_dict_append(qmsg, "inbox", inbox);
2680 qmsg = xs_dict_append(qmsg, "keyid", keyid);
2681 qmsg = xs_dict_append(qmsg, "seckey", seckey);
2682
2683 /* if it's to be sent right now, bypass the disk queue and post the job */
2684 if (retries == 0 && p_state != NULL)
2685 job_post(qmsg, 0);
2686 else {
2687 qmsg = _enqueue_put(fn, qmsg);
2688 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
2689 }
2690}
2691
2692
2693void enqueue_output(snac *snac, xs_dict *msg, xs_str *inbox, int retries, int p_status)
2694/* enqueues an output message to an inbox */
2695{
2696 if (xs_startswith(inbox, snac->actor)) {
2697 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
2698 return;
2699 }
2700
2701 char *seckey = xs_dict_get(snac->key, "secret");
2702
2703 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
2704}
2705
2706
2707void enqueue_output_by_actor(snac *snac, xs_dict *msg, const xs_str *actor, int retries)
2708/* enqueues an output message for an actor */
2709{
2710 xs *inbox = get_actor_inbox(actor);
2711
2712 if (!xs_is_null(inbox))
2713 enqueue_output(snac, msg, inbox, retries, 0);
2714 else
2715 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
2716}
2717
2718
2719void enqueue_email(xs_str *msg, int retries)
2720/* enqueues an email message to be sent */
2721{
2722 xs *qmsg = _new_qmsg("email", msg, retries);
2723 char *ntid = xs_dict_get(qmsg, "ntid");
2724 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2725
2726 qmsg = _enqueue_put(fn, qmsg);
2727
2728 srv_debug(1, xs_fmt("enqueue_email %d", retries));
2729}
2730
2731
2732void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
2733/* enqueues a message to be sent via Telegram */
2734{
2735 xs *qmsg = _new_qmsg("telegram", msg, 0);
2736 char *ntid = xs_dict_get(qmsg, "ntid");
2737 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2738
2739 qmsg = xs_dict_append(qmsg, "bot", bot);
2740 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
2741
2742 qmsg = _enqueue_put(fn, qmsg);
2743
2744 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
2745}
2746
2747void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
2748/* enqueues a message to be sent via ntfy */
2749{
2750 xs *qmsg = _new_qmsg("ntfy", msg, 0);
2751 char *ntid = xs_dict_get(qmsg, "ntid");
2752 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2753
2754 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
2755 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
2756
2757
2758 qmsg = _enqueue_put(fn, qmsg);
2759
2760 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
2761}
2762
2763void enqueue_message(snac *snac, const xs_dict *msg)
2764/* enqueues an output message */
2765{
2766 xs *qmsg = _new_qmsg("message", msg, 0);
2767 char *ntid = xs_dict_get(qmsg, "ntid");
2768 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2769
2770 qmsg = _enqueue_put(fn, qmsg);
2771
2772 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
2773}
2774
2775
2776void enqueue_close_question(snac *user, const char *id, int end_secs)
2777/* enqueues the closing of a question */
2778{
2779 xs *qmsg = _new_qmsg("close_question", id, 0);
2780 xs *ntid = tid(end_secs);
2781 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2782
2783 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2784
2785 qmsg = _enqueue_put(fn, qmsg);
2786
2787 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
2788}
2789
2790
2791void enqueue_object_request(snac *user, const char *id, int forward_secs)
2792/* enqueues the request of an object in the future */
2793{
2794 xs *qmsg = _new_qmsg("object_request", id, 0);
2795 xs *ntid = tid(forward_secs);
2796 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2797
2798 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2799
2800 qmsg = _enqueue_put(fn, qmsg);
2801
2802 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
2803}
2804
2805
2806void enqueue_verify_links(snac *user)
2807/* enqueues a link verification */
2808{
2809 xs *qmsg = _new_qmsg("verify_links", "", 0);
2810 char *ntid = xs_dict_get(qmsg, "ntid");
2811 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2812
2813 qmsg = _enqueue_put(fn, qmsg);
2814
2815 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
2816}
2817
2818
2819void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
2820/* enqueues an actor refresh */
2821{
2822 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
2823 xs *ntid = tid(forward_secs);
2824 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2825
2826 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2827 qmsg = xs_dict_append(qmsg, "actor", actor);
2828
2829 qmsg = _enqueue_put(fn, qmsg);
2830
2831 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
2832}
2833
2834
2835void enqueue_request_replies(snac *user, const char *id)
2836/* enqueues a request for the replies of a message */
2837{
2838 /* test first if this precise request is already in the queue */
2839 xs *queue = user_queue(user);
2840 xs_list *p = queue;
2841 xs_str *v;
2842
2843 while (xs_list_iter(&p, &v)) {
2844 xs *q_item = queue_get(v);
2845
2846 if (q_item != NULL) {
2847 const char *type = xs_dict_get(q_item, "type");
2848 const char *msg = xs_dict_get(q_item, "message");
2849
2850 if (type && msg && strcmp(type, "request_replies") == 0 && strcmp(msg, id) == 0) {
2851 /* don't requeue */
2852 snac_debug(user, 1, xs_fmt("enqueue_request_replies already here %s", id));
2853 return;
2854 }
2855 }
2856 }
2857
2858 /* not there; enqueue the request with a small delay */
2859 xs *qmsg = _new_qmsg("request_replies", id, 0);
2860 xs *ntid = tid(10);
2861 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2862
2863 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2864
2865 qmsg = _enqueue_put(fn, qmsg);
2866
2867 snac_debug(user, 2, xs_fmt("enqueue_request_replies %s", id));
2868}
2869
2870
2871int was_question_voted(snac *user, const char *id)
2872/* returns true if the user voted in this poll */
2873{
2874 xs *children = object_children(id);
2875 int voted = 0;
2876 xs_list *p;
2877 xs_str *md5;
2878
2879 p = children;
2880 while (xs_list_iter(&p, &md5)) {
2881 xs *obj = NULL;
2882
2883 if (valid_status(object_get_by_md5(md5, &obj))) {
2884 char *atto = get_atto(obj);
2885 if (atto && strcmp(atto, user->actor) == 0 &&
2886 !xs_is_null(xs_dict_get(obj, "name"))) {
2887 voted = 1;
2888 break;
2889 }
2890 }
2891 }
2892
2893 return voted;
2894}
2895
2896
2897xs_list *user_queue(snac *snac)
2898/* returns a list with filenames that can be dequeued */
2899{
2900 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
2901 xs_list *list = xs_list_new();
2902 time_t t = time(NULL);
2903 xs_list *p;
2904 xs_val *v;
2905
2906 xs *fns = xs_glob(spec, 0, 0);
2907
2908 p = fns;
2909 while (xs_list_iter(&p, &v)) {
2910 /* get the retry time from the basename */
2911 char *bn = strrchr(v, '/');
2912 time_t t2 = atol(bn + 1);
2913
2914 if (t2 > t)
2915 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
2916 else {
2917 list = xs_list_append(list, v);
2918 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
2919 }
2920 }
2921
2922 return list;
2923}
2924
2925
2926xs_list *queue(void)
2927/* returns a list with filenames that can be dequeued */
2928{
2929 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
2930 xs_list *list = xs_list_new();
2931 time_t t = time(NULL);
2932 xs_list *p;
2933 xs_val *v;
2934
2935 xs *fns = xs_glob(spec, 0, 0);
2936
2937 p = fns;
2938 while (xs_list_iter(&p, &v)) {
2939 /* get the retry time from the basename */
2940 char *bn = strrchr(v, '/');
2941 time_t t2 = atol(bn + 1);
2942
2943 if (t2 > t)
2944 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
2945 else {
2946 list = xs_list_append(list, v);
2947 srv_debug(2, xs_fmt("queue ready for %s", v));
2948 }
2949 }
2950
2951 return list;
2952}
2953
2954
2955xs_dict *queue_get(const char *fn)
2956/* gets a file from a queue */
2957{
2958 FILE *f;
2959 xs_dict *obj = NULL;
2960
2961 if ((f = fopen(fn, "r")) != NULL) {
2962 obj = xs_json_load(f);
2963 fclose(f);
2964 }
2965
2966 return obj;
2967}
2968
2969
2970xs_dict *dequeue(const char *fn)
2971/* dequeues a message */
2972{
2973 xs_dict *obj = queue_get(fn);
2974
2975 unlink(fn);
2976
2977 return obj;
2978}
2979
2980
2981/** the purge **/
2982
2983static int _purge_file(const char *fn, time_t mt)
2984/* purge fn if it's older than days */
2985{
2986 int ret = 0;
2987
2988 if (mtime(fn) < mt) {
2989 /* older than the minimum time: delete it */
2990 unlink(fn);
2991 srv_debug(2, xs_fmt("purged %s", fn));
2992 ret = 1;
2993 }
2994
2995 return ret;
2996}
2997
2998
2999static void _purge_dir(const char *dir, int days)
3000/* purges all files in a directory older than days */
3001{
3002 int cnt = 0;
3003
3004 if (days) {
3005 time_t mt = time(NULL) - days * 24 * 3600;
3006 xs *spec = xs_fmt("%s/" "*", dir);
3007 xs *list = xs_glob(spec, 0, 0);
3008 xs_list *p;
3009 xs_str *v;
3010
3011 p = list;
3012 while (xs_list_iter(&p, &v))
3013 cnt += _purge_file(v, mt);
3014
3015 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3016 }
3017}
3018
3019
3020static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3021/* purges all files in a user subdir older than days */
3022{
3023 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3024
3025 _purge_dir(u_subdir, days);
3026}
3027
3028
3029void purge_server(void)
3030/* purge global server data */
3031{
3032 xs *spec = xs_fmt("%s/object/??", srv_basedir);
3033 xs *dirs = xs_glob(spec, 0, 0);
3034 xs_list *p;
3035 xs_str *v;
3036 int cnt = 0;
3037 int icnt = 0;
3038
3039 time_t mt = time(NULL) - 7 * 24 * 3600;
3040
3041 p = dirs;
3042 while (xs_list_iter(&p, &v)) {
3043 xs_list *p2;
3044 xs_str *v2;
3045
3046 {
3047 xs *spec2 = xs_fmt("%s/" "*.json", v);
3048 xs *files = xs_glob(spec2, 0, 0);
3049
3050 p2 = files;
3051 while (xs_list_iter(&p2, &v2)) {
3052 int n_link;
3053
3054 /* old and with no hard links? */
3055 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3056 xs *s1 = xs_replace(v2, ".json", "");
3057 xs *l = xs_split(s1, "/");
3058 char *md5 = xs_list_get(l, -1);
3059
3060 object_del_by_md5(md5);
3061 cnt++;
3062 }
3063 }
3064 }
3065
3066 {
3067 /* look for stray indexes */
3068 xs *speci = xs_fmt("%s/" "*_?.idx", v);
3069 xs *idxfs = xs_glob(speci, 0, 0);
3070
3071 p2 = idxfs;
3072 while (xs_list_iter(&p2, &v2)) {
3073 /* old enough to consider? */
3074 if (mtime(v2) < mt) {
3075 /* check if the indexed object is here */
3076 xs *o = xs_dup(v2);
3077 char *ext = strchr(o, '_');
3078
3079 if (ext) {
3080 *ext = '\0';
3081 o = xs_str_cat(o, ".json");
3082
3083 if (mtime(o) == 0.0) {
3084 /* delete */
3085 unlink(v2);
3086 srv_debug(1, xs_fmt("purged %s", v2));
3087 icnt++;
3088 }
3089 }
3090 }
3091 }
3092
3093 /* delete index backups */
3094 xs *specb = xs_fmt("%s/" "*.bak", v);
3095 xs *bakfs = xs_glob(specb, 0, 0);
3096
3097 p2 = bakfs;
3098 while (xs_list_iter(&p2, &v2)) {
3099 unlink(v2);
3100 srv_debug(1, xs_fmt("purged %s", v2));
3101 }
3102 }
3103 }
3104
3105 /* purge collected inboxes */
3106 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3107 _purge_dir(ib_dir, 7);
3108
3109 /* purge the instance timeline */
3110 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3111 int itl_gc = index_gc(itl_fn);
3112
3113 /* purge tag indexes */
3114 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3115 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3116 p = tag_dirs;
3117
3118 int tag_gc = 0;
3119 while (xs_list_iter(&p, &v)) {
3120 xs *spec2 = xs_fmt("%s/" "*.idx", v);
3121 xs *files = xs_glob(spec2, 0, 0);
3122 xs_list *p2;
3123 xs_str *v2;
3124
3125 p2 = files;
3126 while (xs_list_iter(&p2, &v2)) {
3127 tag_gc += index_gc(v2);
3128 xs *bak = xs_fmt("%s.bak", v2);
3129 unlink(bak);
3130
3131 if (index_len(v2) == 0) {
3132 /* there are no longer any entry with this tag;
3133 purge it completely */
3134 unlink(v2);
3135 xs *dottag = xs_replace(v2, ".idx", ".tag");
3136 unlink(dottag);
3137 }
3138 }
3139 }
3140
3141 srv_debug(1, xs_fmt("purge: global "
3142 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3143}
3144
3145
3146void purge_user(snac *snac)
3147/* do the purge for this user */
3148{
3149 int priv_days, pub_days, user_days = 0;
3150 char *v;
3151 int n;
3152
3153 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3154 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3155
3156 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3157 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3158 user_days = xs_number_get(v);
3159
3160 if (user_days) {
3161 /* override admin settings only if they are lesser */
3162 if (priv_days == 0 || user_days < priv_days)
3163 priv_days = user_days;
3164
3165 if (pub_days == 0 || user_days < pub_days)
3166 pub_days = user_days;
3167 }
3168
3169 _purge_user_subdir(snac, "hidden", priv_days);
3170 _purge_user_subdir(snac, "private", priv_days);
3171
3172 _purge_user_subdir(snac, "public", pub_days);
3173
3174 const char *idxs[] = { "followers.idx", "private.idx", "public.idx", "pinned.idx", NULL };
3175
3176 for (n = 0; idxs[n]; n++) {
3177 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3178 int gc = index_gc(idx);
3179 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3180 }
3181
3182 /* purge lists */
3183 {
3184 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3185 xs *lol = xs_glob(spec, 0, 0);
3186 int c = 0;
3187 char *v;
3188
3189 while (xs_list_next(lol, &v, &c)) {
3190 int gc = index_gc(v);
3191 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3192 }
3193 }
3194
3195 /* unrelated to purging, but it's a janitorial process, so what the hell */
3196 verify_links(snac);
3197}
3198
3199
3200void purge_all(void)
3201/* purge all users */
3202{
3203 snac snac;
3204 xs *list = user_list();
3205 char *p, *uid;
3206
3207 p = list;
3208 while (xs_list_iter(&p, &uid)) {
3209 if (user_open(&snac, uid)) {
3210 purge_user(&snac);
3211 user_free(&snac);
3212 }
3213 }
3214
3215 purge_server();
3216
3217#ifndef NO_MASTODON_API
3218 mastoapi_purge();
3219#endif
3220}
3221
3222
3223/** archive **/
3224
3225void srv_archive(const char *direction, const char *url, xs_dict *req,
3226 const char *payload, int p_size,
3227 int status, xs_dict *headers,
3228 const char *body, int b_size)
3229/* archives a connection */
3230{
3231 /* obsessive archiving */
3232 xs *date = tid(0);
3233 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3234 FILE *f;
3235
3236 if (mkdirx(dir) != -1) {
3237 xs *meta_fn = xs_fmt("%s/_META", dir);
3238
3239 if ((f = fopen(meta_fn, "w")) != NULL) {
3240 xs *j1 = xs_json_dumps(req, 4);
3241 xs *j2 = xs_json_dumps(headers, 4);
3242
3243 fprintf(f, "dir: %s\n", direction);
3244
3245 if (url)
3246 fprintf(f, "url: %s\n", url);
3247
3248 fprintf(f, "req: %s\n", j1);
3249 fprintf(f, "p_size: %d\n", p_size);
3250 fprintf(f, "status: %d\n", status);
3251 fprintf(f, "response: %s\n", j2);
3252 fprintf(f, "b_size: %d\n", b_size);
3253 fclose(f);
3254 }
3255
3256 if (p_size && payload) {
3257 xs *payload_fn = NULL;
3258 xs *payload_fn_raw = NULL;
3259 char *v = xs_dict_get(req, "content-type");
3260
3261 if (v && xs_str_in(v, "json") != -1) {
3262 payload_fn = xs_fmt("%s/payload.json", dir);
3263
3264 if ((f = fopen(payload_fn, "w")) != NULL) {
3265 xs *v1 = xs_json_loads(payload);
3266 xs *j1 = NULL;
3267
3268 if (v1 != NULL)
3269 j1 = xs_json_dumps(v1, 4);
3270
3271 if (j1 != NULL)
3272 fwrite(j1, strlen(j1), 1, f);
3273 else
3274 fwrite(payload, p_size, 1, f);
3275
3276 fclose(f);
3277 }
3278 }
3279
3280 payload_fn_raw = xs_fmt("%s/payload", dir);
3281
3282 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3283 fwrite(payload, p_size, 1, f);
3284 fclose(f);
3285 }
3286 }
3287
3288 if (b_size && body) {
3289 xs *body_fn = NULL;
3290 char *v = xs_dict_get(headers, "content-type");
3291
3292 if (v && xs_str_in(v, "json") != -1) {
3293 body_fn = xs_fmt("%s/body.json", dir);
3294
3295 if ((f = fopen(body_fn, "w")) != NULL) {
3296 xs *v1 = xs_json_loads(body);
3297 xs *j1 = NULL;
3298
3299 if (v1 != NULL)
3300 j1 = xs_json_dumps(v1, 4);
3301
3302 if (j1 != NULL)
3303 fwrite(j1, strlen(j1), 1, f);
3304 else
3305 fwrite(body, b_size, 1, f);
3306
3307 fclose(f);
3308 }
3309 }
3310 else {
3311 body_fn = xs_fmt("%s/body", dir);
3312
3313 if ((f = fopen(body_fn, "w")) != NULL) {
3314 fwrite(body, b_size, 1, f);
3315 fclose(f);
3316 }
3317 }
3318 }
3319 }
3320}
3321
3322
3323void srv_archive_error(const char *prefix, const xs_str *err,
3324 const xs_dict *req, const xs_val *data)
3325/* archives an error */
3326{
3327 xs *ntid = tid(0);
3328 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
3329 FILE *f;
3330
3331 if ((f = fopen(fn, "w")) != NULL) {
3332 fprintf(f, "Error: %s\n", err);
3333
3334 if (req) {
3335 fprintf(f, "Request headers:\n");
3336
3337 xs_json_dump(req, 4, f);
3338
3339 fprintf(f, "\n");
3340 }
3341
3342 if (data) {
3343 fprintf(f, "Data:\n");
3344
3345 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
3346 xs_json_dump(data, 4, f);
3347 }
3348 else
3349 fprintf(f, "%s", data);
3350
3351 fprintf(f, "\n");
3352 }
3353
3354 fclose(f);
3355 }
3356}
3357
3358
3359void srv_archive_qitem(char *prefix, xs_dict *q_item)
3360/* archives a q_item in the error folder */
3361{
3362 xs *ntid = tid(0);
3363 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
3364 FILE *f;
3365
3366 if ((f = fopen(fn, "w")) != NULL) {
3367 xs_json_dump(q_item, 4, f);
3368 fclose(f);
3369 }
3370}