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