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