2.58
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
1577/** bookmarking **/
1578
1579int is_bookmarked(snac *user, const char *id)
1580/* returns true if this note is bookmarked */
1581{
1582 return object_user_cache_in(user, id, "bookmark");
1583}
1584
1585
1586int bookmark(snac *user, const char *id)
1587/* bookmarks a post */
1588{
1589 if (is_bookmarked(user, id))
1590 return -3;
1591
1592 return object_user_cache_add(user, id, "bookmark");
1593}
1594
1595
1596int unbookmark(snac *user, const char *id)
1597/* unbookmarks a post */
1598{
1599 return object_user_cache_del(user, id, "bookmark");
1600}
1601
1602
1603xs_list *bookmark_list(snac *user)
1604/* return the lists of bookmarked posts */
1605{
1606 return object_user_cache_list(user, "bookmark", XS_ALL, 1);
1607}
1608
1609
1610xs_str *bookmark_index_fn(snac *user)
1611{
1612 return object_user_cache_index_fn(user, "bookmark");
1613}
1614
1615
1616/** pinning **/
1617
1618int is_pinned(snac *user, const char *id)
1619/* returns true if this note is pinned */
1620{
1621 return object_user_cache_in(user, id, "pinned");
1622}
1623
1624
1625int is_pinned_by_md5(snac *user, const char *md5)
1626{
1627 return object_user_cache_in_by_md5(user, md5, "pinned");
1628}
1629
1630
1631int pin(snac *user, const char *id)
1632/* pins a message */
1633{
1634 int ret = -2;
1635
1636 if (xs_startswith(id, user->actor)) {
1637 if (is_pinned(user, id))
1638 ret = -3;
1639 else
1640 ret = object_user_cache_add(user, id, "pinned");
1641 }
1642
1643 return ret;
1644}
1645
1646
1647int unpin(snac *user, const char *id)
1648/* unpin a message */
1649{
1650 return object_user_cache_del(user, id, "pinned");
1651}
1652
1653
1654xs_list *pinned_list(snac *user)
1655/* return the lists of pinned posts */
1656{
1657 return object_user_cache_list(user, "pinned", XS_ALL, 1);
1658}
1659
1660
1661/** hiding **/
1662
1663xs_str *_hidden_fn(snac *snac, const char *id)
1664{
1665 xs *md5 = xs_md5_hex(id, strlen(id));
1666 return xs_fmt("%s/hidden/%s", snac->basedir, md5);
1667}
1668
1669
1670void hide(snac *snac, const char *id)
1671/* hides a message tree */
1672{
1673 xs *fn = _hidden_fn(snac, id);
1674 FILE *f;
1675
1676 if ((f = fopen(fn, "w")) != NULL) {
1677 fprintf(f, "%s\n", id);
1678 fclose(f);
1679
1680 snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
1681
1682 /* hide all the children */
1683 xs *chld = object_children(id);
1684 char *p;
1685 const char *v;
1686
1687 p = chld;
1688 while (xs_list_iter(&p, &v)) {
1689 xs *co = NULL;
1690
1691 /* resolve to get the id */
1692 if (valid_status(object_get_by_md5(v, &co))) {
1693 const char *id = xs_dict_get(co, "id");
1694 if (id != NULL)
1695 hide(snac, id);
1696 }
1697 }
1698 }
1699}
1700
1701
1702int is_hidden(snac *snac, const char *id)
1703/* check is id is hidden */
1704{
1705 xs *fn = _hidden_fn(snac, id);
1706
1707 return !!(mtime(fn) != 0.0);
1708}
1709
1710
1711int actor_add(const char *actor, const xs_dict *msg)
1712/* adds an actor */
1713{
1714 return object_add_ow(actor, msg);
1715}
1716
1717
1718int actor_get(const char *actor, xs_dict **data)
1719/* returns an already downloaded actor */
1720{
1721 int status = HTTP_STATUS_OK;
1722 xs_dict *d = NULL;
1723
1724 if (xs_startswith(actor, srv_baseurl)) {
1725 /* it's a (possible) local user */
1726 xs *l = xs_split(actor, "/");
1727 const char *uid = xs_list_get(l, -1);
1728 snac user;
1729
1730 if (!xs_is_null(uid) && user_open(&user, uid)) {
1731 if (data)
1732 *data = msg_actor(&user);
1733
1734 user_free(&user);
1735 return HTTP_STATUS_OK;
1736 }
1737 else
1738 return HTTP_STATUS_NOT_FOUND;
1739 }
1740
1741 /* read the object */
1742 if (!valid_status(status = object_get(actor, &d))) {
1743 d = xs_free(d);
1744 return status;
1745 }
1746
1747 /* if the object is corrupted, discard it */
1748 if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
1749 srv_debug(1, xs_fmt("corrupted actor object %s", actor));
1750 d = xs_free(d);
1751 return HTTP_STATUS_NOT_FOUND;
1752 }
1753
1754 if (data)
1755 *data = d;
1756 else
1757 d = xs_free(d);
1758
1759 xs *fn = _object_fn(actor);
1760 double max_time;
1761
1762 /* maximum time for the actor data to be considered stale */
1763 max_time = 3600.0 * 36.0;
1764
1765 if (mtime(fn) + max_time < (double) time(NULL)) {
1766 /* actor data exists but also stinks */
1767 status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
1768 }
1769
1770 return status;
1771}
1772
1773
1774int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
1775/* gets an actor and requests a refresh if it's stale */
1776{
1777 int status = actor_get(actor, data);
1778
1779 if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
1780 enqueue_actor_refresh(user, actor, 0);
1781
1782 return status;
1783}
1784
1785
1786/** user limiting (announce blocks) **/
1787
1788int limited(snac *user, const char *id, int cmd)
1789/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
1790{
1791 int ret = 0;
1792 xs *dir = xs_fmt("%s/limited", user->basedir);
1793 xs *md5 = xs_md5_hex(id, strlen(id));
1794 xs *fn = xs_fmt("%s/%s", dir, md5);
1795
1796 switch (cmd) {
1797 case 0: /** check **/
1798 ret = !!(mtime(fn) > 0.0);
1799 break;
1800
1801 case 1: /** limit **/
1802 mkdirx(dir);
1803
1804 if (mtime(fn) > 0.0)
1805 ret = -1;
1806 else {
1807 FILE *f;
1808
1809 if ((f = fopen(fn, "w")) != NULL) {
1810 fprintf(f, "%s\n", id);
1811 fclose(f);
1812 }
1813 else
1814 ret = -2;
1815 }
1816 break;
1817
1818 case 2: /** unlimit **/
1819 if (mtime(fn) > 0.0)
1820 ret = unlink(fn);
1821 else
1822 ret = -1;
1823 break;
1824 }
1825
1826 return ret;
1827}
1828
1829
1830/** tag indexing **/
1831
1832void tag_index(const char *id, const xs_dict *obj)
1833/* update the tag indexes for this object */
1834{
1835 const xs_list *tags = xs_dict_get(obj, "tag");
1836
1837 if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
1838 xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
1839
1840 mkdirx(g_tag_dir);
1841
1842 const xs_dict *v;
1843 int ct = 0;
1844 while (xs_list_next(tags, &v, &ct)) {
1845 const char *type = xs_dict_get(v, "type");
1846 const char *name = xs_dict_get(v, "name");
1847
1848 if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
1849 while (*name == '#' || *name == '@')
1850 name++;
1851
1852 if (*name == '\0')
1853 continue;
1854
1855 name = xs_tolower_i((xs_str *)name);
1856
1857 xs *md5_tag = xs_md5_hex(name, strlen(name));
1858 xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
1859 mkdirx(tag_dir);
1860
1861 xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
1862 index_add(g_tag_idx, id);
1863
1864 FILE *f;
1865 xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
1866 if ((f = fopen(g_tag_name, "w")) != NULL) {
1867 fprintf(f, "%s\n", name);
1868 fclose(f);
1869 }
1870
1871 srv_debug(0, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
1872 }
1873 }
1874 }
1875}
1876
1877
1878xs_str *tag_fn(const char *tag)
1879{
1880 if (*tag == '#')
1881 tag++;
1882
1883 xs *lw_tag = xs_tolower_i(xs_dup(tag));
1884 xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag));
1885
1886 return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
1887}
1888
1889
1890xs_list *tag_search(const char *tag, int skip, int show)
1891/* returns the list of posts tagged with tag */
1892{
1893 xs *idx = tag_fn(tag);
1894
1895 return index_list_desc(idx, skip, show);
1896}
1897
1898
1899/** lists **/
1900
1901xs_val *list_maint(snac *user, const char *list, int op)
1902/* list maintenance */
1903{
1904 xs_val *l = NULL;
1905
1906 switch (op) {
1907 case 0: /** list of lists **/
1908 {
1909 FILE *f;
1910 xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
1911 xs *ls = xs_glob(spec, 0, 0);
1912 int c = 0;
1913 const char *v;
1914
1915 l = xs_list_new();
1916
1917 while (xs_list_next(ls, &v, &c)) {
1918 if ((f = fopen(v, "r")) != NULL) {
1919 xs *title = xs_readline(f);
1920 fclose(f);
1921
1922 title = xs_strip_i(title);
1923
1924 xs *v2 = xs_replace(v, ".id", "");
1925 xs *l2 = xs_split(v2, "/");
1926
1927 /* return [ list_id, list_title ] */
1928 l = xs_list_append(l, xs_list_append(xs_list_new(), xs_list_get(l2, -1), title));
1929 }
1930 }
1931 }
1932
1933 break;
1934
1935 case 1: /** create new list (list is the name) **/
1936 {
1937 xs *lol = list_maint(user, NULL, 0);
1938 int c = 0;
1939 const xs_list *v;
1940 int add = 1;
1941
1942 /* check if this list name already exists */
1943 while (xs_list_next(lol, &v, &c)) {
1944 if (strcmp(xs_list_get(v, 1), list) == 0) {
1945 add = 0;
1946 break;
1947 }
1948 }
1949
1950 if (add) {
1951 FILE *f;
1952 xs *dir = xs_fmt("%s/list/", user->basedir);
1953
1954 l = xs_fmt("%010x", time(NULL));
1955
1956 mkdirx(dir);
1957
1958 xs *fn = xs_fmt("%s%s.id", dir, l);
1959
1960 if ((f = fopen(fn, "w")) != NULL) {
1961 fprintf(f, "%s\n", list);
1962 fclose(f);
1963 }
1964 }
1965 }
1966
1967 break;
1968
1969 case 2: /** delete list (list is the id) **/
1970 {
1971 if (xs_is_hex(list)) {
1972 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
1973 unlink(fn);
1974
1975 fn = xs_replace_i(fn, ".id", ".lst");
1976 unlink(fn);
1977
1978 fn = xs_replace_i(fn, ".lst", ".idx");
1979 unlink(fn);
1980
1981 fn = xs_str_cat(fn, ".bak");
1982 unlink(fn);
1983 }
1984 }
1985
1986 break;
1987
1988 case 3: /** get list name **/
1989 if (xs_is_hex(list)) {
1990 FILE *f;
1991 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
1992
1993 if ((f = fopen(fn, "r")) != NULL) {
1994 l = xs_strip_i(xs_readline(f));
1995 fclose(f);
1996 }
1997 }
1998
1999 break;
2000 }
2001
2002 return l;
2003}
2004
2005
2006xs_str *list_timeline_fn(snac *user, const char *list)
2007{
2008 return xs_fmt("%s/list/%s.idx", user->basedir, list);
2009}
2010
2011
2012xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2013/* returns the timeline of a list */
2014{
2015 xs_list *l = NULL;
2016
2017 if (!xs_is_hex(list))
2018 return NULL;
2019
2020 xs *fn = list_timeline_fn(user, list);
2021
2022 if (mtime(fn) > 0.0)
2023 l = index_list_desc(fn, skip, show);
2024
2025 return l;
2026}
2027
2028
2029xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
2030/* list content management */
2031{
2032 xs_val *l = NULL;
2033
2034 if (!xs_is_hex(list))
2035 return NULL;
2036
2037 if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2038 return NULL;
2039
2040 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2041
2042 switch (op) {
2043 case 0: /** list content **/
2044 l = index_list(fn, XS_ALL);
2045
2046 break;
2047
2048 case 1: /** append actor to list **/
2049 if (actor_md5 != NULL) {
2050 if (!index_in_md5(fn, actor_md5))
2051 index_add_md5(fn, actor_md5);
2052 }
2053
2054 break;
2055
2056 case 2: /** delete actor from list **/
2057 if (actor_md5 != NULL)
2058 index_del_md5(fn, actor_md5);
2059
2060 break;
2061
2062 default:
2063 srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2064 break;
2065 }
2066
2067 return l;
2068}
2069
2070
2071void list_distribute(snac *user, const char *who, const xs_dict *post)
2072/* distributes the post to all appropriate lists */
2073{
2074 const char *id = xs_dict_get(post, "id");
2075
2076 /* if who is not set, use the attributedTo in the message */
2077 if (xs_is_null(who))
2078 who = get_atto(post);
2079
2080 if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2081 xs *a_md5 = xs_md5_hex(who, strlen(who));
2082 xs *i_md5 = xs_md5_hex(id, strlen(id));
2083 xs *spec = xs_fmt("%s/list/" "*.lst", user->basedir);
2084 xs *ls = xs_glob(spec, 0, 0);
2085 int c = 0;
2086 const char *v;
2087
2088 while (xs_list_next(ls, &v, &c)) {
2089 /* is the actor in this list? */
2090 if (index_in_md5(v, a_md5)) {
2091 /* it is; add post md5 to its timeline */
2092 xs *idx = xs_replace(v, ".lst", ".idx");
2093 index_add_md5(idx, i_md5);
2094
2095 snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2096 }
2097 }
2098 }
2099}
2100
2101
2102/** static data **/
2103
2104static int _load_raw_file(const char *fn, xs_val **data, int *size,
2105 const char *inm, xs_str **etag)
2106/* loads a cached file */
2107{
2108 int status = HTTP_STATUS_NOT_FOUND;
2109
2110 if (fn) {
2111 double tm = mtime(fn);
2112
2113 if (tm > 0.0) {
2114 /* file exists; build the etag */
2115 xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2116
2117 /* if if-none-match is set, check if it's the same */
2118 if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2119 /* client has the newest version */
2120 status = HTTP_STATUS_NOT_MODIFIED;
2121 }
2122 else {
2123 /* newer or never downloaded; read the full file */
2124 FILE *f;
2125
2126 if ((f = fopen(fn, "rb")) != NULL) {
2127 *size = XS_ALL;
2128 *data = xs_read(f, size);
2129 fclose(f);
2130
2131 status = HTTP_STATUS_OK;
2132 }
2133 }
2134
2135 /* if caller wants the etag, return it */
2136 if (etag != NULL)
2137 *etag = xs_dup(e);
2138
2139 srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2140 }
2141 }
2142
2143 return status;
2144}
2145
2146
2147xs_str *_static_fn(snac *snac, const char *id)
2148/* gets the filename for a static file */
2149{
2150 if (strchr(id, '/'))
2151 return NULL;
2152 else
2153 return xs_fmt("%s/static/%s", snac->basedir, id);
2154}
2155
2156
2157int static_get(snac *snac, const char *id, xs_val **data, int *size,
2158 const char *inm, xs_str **etag)
2159/* returns static content */
2160{
2161 xs *fn = _static_fn(snac, id);
2162
2163 return _load_raw_file(fn, data, size, inm, etag);
2164}
2165
2166
2167void static_put(snac *snac, const char *id, const char *data, int size)
2168/* writes status content */
2169{
2170 xs *fn = _static_fn(snac, id);
2171 FILE *f;
2172
2173 if (fn && (f = fopen(fn, "wb")) != NULL) {
2174 fwrite(data, size, 1, f);
2175 fclose(f);
2176 }
2177}
2178
2179
2180void static_put_meta(snac *snac, const char *id, const char *str)
2181/* puts metadata (i.e. a media description string) to id */
2182{
2183 xs *fn = _static_fn(snac, id);
2184
2185 if (fn) {
2186 fn = xs_str_cat(fn, ".txt");
2187 FILE *f;
2188
2189 if ((f = fopen(fn, "w")) != NULL) {
2190 fprintf(f, "%s\n", str);
2191 fclose(f);
2192 }
2193 }
2194}
2195
2196
2197xs_str *static_get_meta(snac *snac, const char *id)
2198/* gets metadata from a media */
2199{
2200 xs *fn = _static_fn(snac, id);
2201 xs_str *r = NULL;
2202
2203 if (fn) {
2204 fn = xs_str_cat(fn, ".txt");
2205 FILE *f;
2206
2207 if ((f = fopen(fn, "r")) != NULL) {
2208 r = xs_strip_i(xs_readline(f));
2209 fclose(f);
2210 }
2211 }
2212 else
2213 r = xs_str_new("");
2214
2215 return r;
2216}
2217
2218
2219/** history **/
2220
2221xs_str *_history_fn(snac *snac, const char *id)
2222/* gets the filename for the history */
2223{
2224 if (strchr(id, '/'))
2225 return NULL;
2226 else
2227 return xs_fmt("%s/history/%s", snac->basedir, id);
2228}
2229
2230
2231double history_mtime(snac *snac, const char *id)
2232{
2233 double t = 0.0;
2234 xs *fn = _history_fn(snac, id);
2235
2236 if (fn != NULL)
2237 t = mtime(fn);
2238
2239 return t;
2240}
2241
2242
2243void history_add(snac *snac, const char *id, const char *content, int size,
2244 xs_str **etag)
2245/* adds something to the history */
2246{
2247 xs *fn = _history_fn(snac, id);
2248 FILE *f;
2249
2250 if (fn && (f = fopen(fn, "w")) != NULL) {
2251 fwrite(content, size, 1, f);
2252 fclose(f);
2253
2254 if (etag) {
2255 double tm = mtime(fn);
2256 *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2257 }
2258 }
2259}
2260
2261
2262int history_get(snac *snac, const char *id, xs_str **content, int *size,
2263 const char *inm, xs_str **etag)
2264{
2265 xs *fn = _history_fn(snac, id);
2266
2267 return _load_raw_file(fn, content, size, inm, etag);
2268}
2269
2270
2271int history_del(snac *snac, const char *id)
2272{
2273 xs *fn = _history_fn(snac, id);
2274
2275 if (fn)
2276 return unlink(fn);
2277 else
2278 return -1;
2279}
2280
2281
2282xs_list *history_list(snac *snac)
2283{
2284 xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2285
2286 return xs_glob(spec, 1, 1);
2287}
2288
2289
2290void lastlog_write(snac *snac, const char *source)
2291/* writes the last time the user logged in */
2292{
2293 xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2294 FILE *f;
2295
2296 if ((f = fopen(fn, "w")) != NULL) {
2297 fprintf(f, "%lf %s\n", ftime(), source);
2298 fclose(f);
2299 }
2300}
2301
2302
2303/** inbox collection **/
2304
2305void inbox_add(const char *inbox)
2306/* collects a shared inbox */
2307{
2308 /* don't collect ourselves */
2309 if (xs_startswith(inbox, srv_baseurl))
2310 return;
2311
2312 xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2313 xs *fn = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2314 FILE *f;
2315
2316 if ((f = fopen(fn, "w")) != NULL) {
2317 fprintf(f, "%s\n", inbox);
2318 fclose(f);
2319 }
2320}
2321
2322
2323void inbox_add_by_actor(const xs_dict *actor)
2324/* collects an actor's shared inbox, if it has one */
2325{
2326 const char *v;
2327
2328 if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2329 !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2330 /* only collect this inbox if its instance is not blocked */
2331 if (!is_instance_blocked(v))
2332 inbox_add(v);
2333 }
2334}
2335
2336
2337xs_list *inbox_list(void)
2338/* returns the collected inboxes as a list */
2339{
2340 xs_list *ibl = xs_list_new();
2341 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2342 xs *files = xs_glob(spec, 0, 0);
2343 xs_list *p = files;
2344 const xs_val *v;
2345
2346 while (xs_list_iter(&p, &v)) {
2347 FILE *f;
2348
2349 if ((f = fopen(v, "r")) != NULL) {
2350 xs *line = xs_readline(f);
2351
2352 if (line) {
2353 line = xs_strip_i(line);
2354 ibl = xs_list_append(ibl, line);
2355 }
2356
2357 fclose(f);
2358 }
2359 }
2360
2361 return ibl;
2362}
2363
2364
2365/** instance-wide operations **/
2366
2367xs_str *_instance_block_fn(const char *instance)
2368{
2369 xs *s = xs_replace(instance, "http:/" "/", "");
2370 xs *s1 = xs_replace(s, "https:/" "/", "");
2371 xs *l = xs_split(s1, "/");
2372 const char *p = xs_list_get(l, 0);
2373 xs *md5 = xs_md5_hex(p, strlen(p));
2374
2375 return xs_fmt("%s/block/%s", srv_basedir, md5);
2376}
2377
2378
2379int is_instance_blocked(const char *instance)
2380{
2381 xs *fn = _instance_block_fn(instance);
2382
2383 return !!(mtime(fn) != 0.0);
2384}
2385
2386
2387int instance_block(const char *instance)
2388/* blocks a full instance */
2389{
2390 int ret;
2391
2392 /* create the subdir */
2393 xs *dir = xs_fmt("%s/block/", srv_basedir);
2394 mkdirx(dir);
2395
2396 if (!is_instance_blocked(instance)) {
2397 xs *fn = _instance_block_fn(instance);
2398 FILE *f;
2399
2400 if ((f = fopen(fn, "w")) != NULL) {
2401 fprintf(f, "%s\n", instance);
2402 fclose(f);
2403
2404 ret = 0;
2405 }
2406 else
2407 ret = -1;
2408 }
2409 else
2410 ret = -2;
2411
2412 return ret;
2413}
2414
2415
2416int instance_unblock(const char *instance)
2417/* unblocks a full instance */
2418{
2419 int ret;
2420
2421 if (is_instance_blocked(instance)) {
2422 xs *fn = _instance_block_fn(instance);
2423 ret = unlink(fn);
2424 }
2425 else
2426 ret = -2;
2427
2428 return ret;
2429}
2430
2431
2432/** operations by content **/
2433
2434int content_match(const char *file, const xs_dict *msg)
2435/* checks if a message's content matches any of the regexes in file */
2436/* file format: one regex per line */
2437{
2438 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2439 FILE *f;
2440 int r = 0;
2441 const char *v = xs_dict_get(msg, "content");
2442
2443 if (xs_type(v) == XSTYPE_STRING && *v) {
2444 if ((f = fopen(fn, "r")) != NULL) {
2445 srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2446
2447 /* massage content (strip HTML tags, etc.) */
2448 xs *c = xs_regex_replace(v, "<[^>]+>", " ");
2449 c = xs_regex_replace_i(c, " {2,}", " ");
2450 c = xs_tolower_i(c);
2451
2452 while (!r && !feof(f)) {
2453 xs *rx = xs_strip_i(xs_readline(f));
2454
2455 if (*rx && xs_regex_match(c, rx)) {
2456 srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2457 r = 1;
2458 }
2459 }
2460
2461 fclose(f);
2462 }
2463 }
2464
2465 return r;
2466}
2467
2468
2469xs_list *content_search(snac *user, const char *regex,
2470 int priv, int skip, int show, int max_secs, int *timeout)
2471/* returns a list of posts which content matches the regex */
2472{
2473 if (regex == NULL || *regex == '\0')
2474 return xs_list_new();
2475
2476 xs *i_regex = xs_utf8_to_lower(regex);
2477
2478 xs_set seen;
2479
2480 xs_set_init(&seen);
2481
2482 if (max_secs == 0)
2483 max_secs = 3;
2484
2485 time_t t = time(NULL) + max_secs;
2486 *timeout = 0;
2487
2488 /* iterate all timelines simultaneously */
2489 xs_list *tls[3] = {0};
2490 const char *md5s[3] = {0};
2491 int c[3] = {0};
2492
2493 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */
2494 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2495 tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
2496
2497 /* first positioning */
2498 for (int n = 0; n < 3; n++)
2499 xs_list_next(tls[n], &md5s[n], &c[n]);
2500
2501 show += skip;
2502
2503 while (show > 0) {
2504 /* timeout? */
2505 if (time(NULL) > t) {
2506 *timeout = 1;
2507 break;
2508 }
2509
2510 /* find the newest post */
2511 int newest = -1;
2512 double mtime = 0.0;
2513
2514 for (int n = 0; n < 3; n++) {
2515 if (md5s[n] != NULL) {
2516 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2517 double mt = mtime(fn);
2518
2519 if (mt > mtime) {
2520 newest = n;
2521 mtime = mt;
2522 }
2523 }
2524 }
2525
2526 if (newest == -1)
2527 break;
2528
2529 const char *md5 = md5s[newest];
2530
2531 /* advance the chosen timeline */
2532 if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2533 md5s[newest] = NULL;
2534
2535 xs *post = NULL;
2536
2537 if (!valid_status(object_get_by_md5(md5, &post)))
2538 continue;
2539
2540 if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2541 continue;
2542
2543 const char *id = xs_dict_get(post, "id");
2544
2545 if (id == NULL || is_hidden(user, id))
2546 continue;
2547
2548 xs *c = xs_str_new(NULL);
2549 const char *content = xs_dict_get(post, "content");
2550 const char *name = xs_dict_get(post, "name");
2551
2552 if (!xs_is_null(content))
2553 c = xs_str_cat(c, content);
2554 if (!xs_is_null(name))
2555 c = xs_str_cat(c, " ", name);
2556
2557 /* add alt-texts from attachments */
2558 const xs_list *atts = xs_dict_get(post, "attachment");
2559 int tc = 0;
2560 const xs_dict *att;
2561
2562 while (xs_list_next(atts, &att, &tc)) {
2563 const char *name = xs_dict_get(att, "name");
2564
2565 if (name != NULL)
2566 c = xs_str_cat(c, " ", name);
2567 }
2568
2569 /* strip HTML */
2570 c = xs_regex_replace_i(c, "<[^>]+>", " ");
2571 c = xs_regex_replace_i(c, " {2,}", " ");
2572
2573 /* convert to lowercase */
2574 xs *lc = xs_utf8_to_lower(c);
2575
2576 /* apply regex */
2577 if (xs_regex_match(lc, i_regex)) {
2578 if (xs_set_add(&seen, md5) == 1)
2579 show--;
2580 }
2581 }
2582
2583 xs_list *r = xs_set_result(&seen);
2584
2585 if (skip) {
2586 /* BAD */
2587 while (skip--) {
2588 r = xs_list_del(r, 0);
2589 }
2590 }
2591
2592 xs_free(tls[0]);
2593 xs_free(tls[1]);
2594 xs_free(tls[2]);
2595
2596 return r;
2597}
2598
2599
2600/** notifications **/
2601
2602xs_str *notify_check_time(snac *snac, int reset)
2603/* gets or resets the latest notification check time */
2604{
2605 xs_str *t = NULL;
2606 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
2607 FILE *f;
2608
2609 if (reset) {
2610 if ((f = fopen(fn, "w")) != NULL) {
2611 t = tid(0);
2612 fprintf(f, "%s\n", t);
2613 fclose(f);
2614 }
2615 }
2616 else {
2617 if ((f = fopen(fn, "r")) != NULL) {
2618 t = xs_readline(f);
2619 fclose(f);
2620 }
2621 else
2622 /* never set before */
2623 t = xs_fmt("%16.6f", 0.0);
2624 }
2625
2626 return t;
2627}
2628
2629
2630void notify_add(snac *snac, const char *type, const char *utype,
2631 const char *actor, const char *objid, const xs_dict *msg)
2632/* adds a new notification */
2633{
2634 xs *ntid = tid(0);
2635 xs *fn = xs_fmt("%s/notify/", snac->basedir);
2636 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
2637 FILE *f;
2638
2639 /* create the directory */
2640 mkdirx(fn);
2641 fn = xs_str_cat(fn, ntid);
2642 fn = xs_str_cat(fn, ".json");
2643
2644 xs *noti = xs_dict_new();
2645
2646 noti = xs_dict_append(noti, "id", ntid);
2647 noti = xs_dict_append(noti, "type", type);
2648 noti = xs_dict_append(noti, "utype", utype);
2649 noti = xs_dict_append(noti, "actor", actor);
2650 noti = xs_dict_append(noti, "date", date);
2651 noti = xs_dict_append(noti, "msg", msg);
2652
2653 if (!xs_is_null(objid))
2654 noti = xs_dict_append(noti, "objid", objid);
2655
2656 if ((f = fopen(fn, "w")) != NULL) {
2657 xs_json_dump(noti, 4, f);
2658 fclose(f);
2659 }
2660
2661 /* add it to the index if it already exists */
2662 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2663
2664 if (mtime(idx) != 0.0) {
2665 pthread_mutex_lock(&data_mutex);
2666
2667 if ((f = fopen(idx, "a")) != NULL) {
2668 fprintf(f, "%-32s\n", ntid);
2669 fclose(f);
2670 }
2671
2672 pthread_mutex_unlock(&data_mutex);
2673 }
2674}
2675
2676
2677xs_dict *notify_get(snac *snac, const char *id)
2678/* gets a notification */
2679{
2680 /* base file */
2681 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
2682
2683 /* strip spaces and add extension */
2684 fn = xs_strip_i(fn);
2685 fn = xs_str_cat(fn, ".json");
2686
2687 FILE *f;
2688 xs_dict *out = NULL;
2689
2690 if ((f = fopen(fn, "r")) != NULL) {
2691 out = xs_json_load(f);
2692 fclose(f);
2693 }
2694
2695 return out;
2696}
2697
2698
2699xs_list *notify_list(snac *snac, int skip, int show)
2700/* returns a list of notification ids */
2701{
2702 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2703
2704 if (mtime(idx) == 0.0) {
2705 /* create the index from scratch */
2706 FILE *f;
2707
2708 pthread_mutex_lock(&data_mutex);
2709
2710 if ((f = fopen(idx, "w")) != NULL) {
2711 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
2712 xs *lst = xs_glob(spec, 1, 0);
2713 xs_list *p = lst;
2714 const char *v;
2715
2716 while (xs_list_iter(&p, &v)) {
2717 char *p = strrchr(v, '.');
2718 if (p) {
2719 *p = '\0';
2720 fprintf(f, "%-32s\n", v);
2721 }
2722 }
2723
2724 fclose(f);
2725 }
2726
2727 pthread_mutex_unlock(&data_mutex);
2728 }
2729
2730 return index_list_desc(idx, skip, show);
2731}
2732
2733
2734int notify_new_num(snac *snac)
2735/* counts the number of new notifications */
2736{
2737 xs *t = notify_check_time(snac, 0);
2738 xs *lst = notify_list(snac, 0, XS_ALL);
2739 int cnt = 0;
2740
2741 xs_list *p = lst;
2742 const xs_str *v;
2743
2744 while (xs_list_iter(&p, &v)) {
2745 xs *id = xs_strip_i(xs_dup(v));
2746
2747 /* old? count no more */
2748 if (strcmp(id, t) < 0)
2749 break;
2750
2751 cnt++;
2752 }
2753
2754 return cnt;
2755}
2756
2757
2758void notify_clear(snac *snac)
2759/* clears all notifications */
2760{
2761 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
2762 xs *lst = xs_glob(spec, 0, 0);
2763 xs_list *p = lst;
2764 const xs_str *v;
2765
2766 while (xs_list_iter(&p, &v))
2767 unlink(v);
2768
2769 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2770
2771 if (mtime(idx) != 0.0) {
2772 pthread_mutex_lock(&data_mutex);
2773 truncate(idx, 0);
2774 pthread_mutex_unlock(&data_mutex);
2775 }
2776}
2777
2778
2779/** the queue **/
2780
2781static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
2782/* writes safely to the queue */
2783{
2784 xs *tfn = xs_fmt("%s.tmp", fn);
2785 FILE *f;
2786
2787 if ((f = fopen(tfn, "w")) != NULL) {
2788 xs_json_dump(msg, 4, f);
2789 fclose(f);
2790
2791 rename(tfn, fn);
2792 }
2793
2794 return msg;
2795}
2796
2797
2798static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
2799/* creates a queue message */
2800{
2801 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
2802 xs *ntid = tid(retries * 60 * qrt);
2803 xs *rn = xs_number_new(retries);
2804
2805 xs_dict *qmsg = xs_dict_new();
2806
2807 qmsg = xs_dict_append(qmsg, "type", type);
2808 qmsg = xs_dict_append(qmsg, "message", msg);
2809 qmsg = xs_dict_append(qmsg, "retries", rn);
2810 qmsg = xs_dict_append(qmsg, "ntid", ntid);
2811
2812 return qmsg;
2813}
2814
2815
2816void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
2817/* enqueues an input message */
2818{
2819 xs *qmsg = _new_qmsg("input", msg, retries);
2820 const char *ntid = xs_dict_get(qmsg, "ntid");
2821 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2822
2823 qmsg = xs_dict_append(qmsg, "req", req);
2824
2825 qmsg = _enqueue_put(fn, qmsg);
2826
2827 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
2828}
2829
2830
2831void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
2832/* enqueues an input message from the shared input */
2833{
2834 xs *qmsg = _new_qmsg("input", msg, retries);
2835 const char *ntid = xs_dict_get(qmsg, "ntid");
2836 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2837
2838 qmsg = xs_dict_append(qmsg, "req", req);
2839
2840 qmsg = _enqueue_put(fn, qmsg);
2841
2842 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
2843}
2844
2845
2846void enqueue_output_raw(const char *keyid, const char *seckey,
2847 const xs_dict *msg, const xs_str *inbox,
2848 int retries, int p_status)
2849/* enqueues an output message to an inbox */
2850{
2851 xs *qmsg = _new_qmsg("output", msg, retries);
2852 const char *ntid = xs_dict_get(qmsg, "ntid");
2853 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2854
2855 xs *ns = xs_number_new(p_status);
2856 qmsg = xs_dict_append(qmsg, "p_status", ns);
2857
2858 qmsg = xs_dict_append(qmsg, "inbox", inbox);
2859 qmsg = xs_dict_append(qmsg, "keyid", keyid);
2860 qmsg = xs_dict_append(qmsg, "seckey", seckey);
2861
2862 /* if it's to be sent right now, bypass the disk queue and post the job */
2863 if (retries == 0 && p_state != NULL)
2864 job_post(qmsg, 0);
2865 else {
2866 qmsg = _enqueue_put(fn, qmsg);
2867 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
2868 }
2869}
2870
2871
2872void enqueue_output(snac *snac, const xs_dict *msg,
2873 const xs_str *inbox, int retries, int p_status)
2874/* enqueues an output message to an inbox */
2875{
2876 if (xs_startswith(inbox, snac->actor)) {
2877 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
2878 return;
2879 }
2880
2881 const char *seckey = xs_dict_get(snac->key, "secret");
2882
2883 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
2884}
2885
2886
2887void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
2888 const xs_str *actor, int retries)
2889/* enqueues an output message for an actor */
2890{
2891 xs *inbox = get_actor_inbox(actor);
2892
2893 if (!xs_is_null(inbox))
2894 enqueue_output(snac, msg, inbox, retries, 0);
2895 else
2896 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
2897}
2898
2899
2900void enqueue_email(const xs_str *msg, int retries)
2901/* enqueues an email message to be sent */
2902{
2903 xs *qmsg = _new_qmsg("email", msg, retries);
2904 const char *ntid = xs_dict_get(qmsg, "ntid");
2905 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2906
2907 qmsg = _enqueue_put(fn, qmsg);
2908
2909 srv_debug(1, xs_fmt("enqueue_email %d", retries));
2910}
2911
2912
2913void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
2914/* enqueues a message to be sent via Telegram */
2915{
2916 xs *qmsg = _new_qmsg("telegram", msg, 0);
2917 const char *ntid = xs_dict_get(qmsg, "ntid");
2918 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2919
2920 qmsg = xs_dict_append(qmsg, "bot", bot);
2921 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
2922
2923 qmsg = _enqueue_put(fn, qmsg);
2924
2925 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
2926}
2927
2928void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
2929/* enqueues a message to be sent via ntfy */
2930{
2931 xs *qmsg = _new_qmsg("ntfy", msg, 0);
2932 const char *ntid = xs_dict_get(qmsg, "ntid");
2933 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2934
2935 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
2936 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
2937
2938
2939 qmsg = _enqueue_put(fn, qmsg);
2940
2941 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
2942}
2943
2944void enqueue_message(snac *snac, const xs_dict *msg)
2945/* enqueues an output message */
2946{
2947 xs *qmsg = _new_qmsg("message", msg, 0);
2948 const char *ntid = xs_dict_get(qmsg, "ntid");
2949 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2950
2951 qmsg = _enqueue_put(fn, qmsg);
2952
2953 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
2954}
2955
2956
2957void enqueue_close_question(snac *user, const char *id, int end_secs)
2958/* enqueues the closing of a question */
2959{
2960 xs *qmsg = _new_qmsg("close_question", id, 0);
2961 xs *ntid = tid(end_secs);
2962 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2963
2964 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2965
2966 qmsg = _enqueue_put(fn, qmsg);
2967
2968 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
2969}
2970
2971
2972void enqueue_object_request(snac *user, const char *id, int forward_secs)
2973/* enqueues the request of an object in the future */
2974{
2975 xs *qmsg = _new_qmsg("object_request", id, 0);
2976 xs *ntid = tid(forward_secs);
2977 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2978
2979 qmsg = xs_dict_set(qmsg, "ntid", ntid);
2980
2981 qmsg = _enqueue_put(fn, qmsg);
2982
2983 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
2984}
2985
2986
2987void enqueue_verify_links(snac *user)
2988/* enqueues a link verification */
2989{
2990 xs *qmsg = _new_qmsg("verify_links", "", 0);
2991 const char *ntid = xs_dict_get(qmsg, "ntid");
2992 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
2993
2994 qmsg = _enqueue_put(fn, qmsg);
2995
2996 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
2997}
2998
2999
3000void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3001/* enqueues an actor refresh */
3002{
3003 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3004 xs *ntid = tid(forward_secs);
3005 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3006
3007 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3008 qmsg = xs_dict_append(qmsg, "actor", actor);
3009
3010 qmsg = _enqueue_put(fn, qmsg);
3011
3012 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3013}
3014
3015
3016int was_question_voted(snac *user, const char *id)
3017/* returns true if the user voted in this poll */
3018{
3019 xs *children = object_children(id);
3020 int voted = 0;
3021 xs_list *p;
3022 const xs_str *md5;
3023
3024 p = children;
3025 while (xs_list_iter(&p, &md5)) {
3026 xs *obj = NULL;
3027
3028 if (valid_status(object_get_by_md5(md5, &obj))) {
3029 const char *atto = get_atto(obj);
3030 if (atto && strcmp(atto, user->actor) == 0 &&
3031 !xs_is_null(xs_dict_get(obj, "name"))) {
3032 voted = 1;
3033 break;
3034 }
3035 }
3036 }
3037
3038 return voted;
3039}
3040
3041
3042xs_list *user_queue(snac *snac)
3043/* returns a list with filenames that can be dequeued */
3044{
3045 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
3046 xs_list *list = xs_list_new();
3047 time_t t = time(NULL);
3048 xs_list *p;
3049 const xs_val *v;
3050
3051 xs *fns = xs_glob(spec, 0, 0);
3052
3053 p = fns;
3054 while (xs_list_iter(&p, &v)) {
3055 /* get the retry time from the basename */
3056 char *bn = strrchr(v, '/');
3057 time_t t2 = atol(bn + 1);
3058
3059 if (t2 > t)
3060 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3061 else {
3062 list = xs_list_append(list, v);
3063 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3064 }
3065 }
3066
3067 return list;
3068}
3069
3070
3071xs_list *queue(void)
3072/* returns a list with filenames that can be dequeued */
3073{
3074 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
3075 xs_list *list = xs_list_new();
3076 time_t t = time(NULL);
3077 xs_list *p;
3078 const xs_val *v;
3079
3080 xs *fns = xs_glob(spec, 0, 0);
3081
3082 p = fns;
3083 while (xs_list_iter(&p, &v)) {
3084 /* get the retry time from the basename */
3085 char *bn = strrchr(v, '/');
3086 time_t t2 = atol(bn + 1);
3087
3088 if (t2 > t)
3089 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3090 else {
3091 list = xs_list_append(list, v);
3092 srv_debug(2, xs_fmt("queue ready for %s", v));
3093 }
3094 }
3095
3096 return list;
3097}
3098
3099
3100xs_dict *queue_get(const char *fn)
3101/* gets a file from a queue */
3102{
3103 FILE *f;
3104 xs_dict *obj = NULL;
3105
3106 if ((f = fopen(fn, "r")) != NULL) {
3107 obj = xs_json_load(f);
3108 fclose(f);
3109 }
3110
3111 return obj;
3112}
3113
3114
3115xs_dict *dequeue(const char *fn)
3116/* dequeues a message */
3117{
3118 xs_dict *obj = queue_get(fn);
3119
3120 unlink(fn);
3121
3122 return obj;
3123}
3124
3125
3126/** the purge **/
3127
3128static int _purge_file(const char *fn, time_t mt)
3129/* purge fn if it's older than days */
3130{
3131 int ret = 0;
3132
3133 if (mtime(fn) < mt) {
3134 /* older than the minimum time: delete it */
3135 unlink(fn);
3136 srv_debug(2, xs_fmt("purged %s", fn));
3137 ret = 1;
3138 }
3139
3140 return ret;
3141}
3142
3143
3144static void _purge_dir(const char *dir, int days)
3145/* purges all files in a directory older than days */
3146{
3147 int cnt = 0;
3148
3149 if (days) {
3150 time_t mt = time(NULL) - days * 24 * 3600;
3151 xs *spec = xs_fmt("%s/" "*", dir);
3152 xs *list = xs_glob(spec, 0, 0);
3153 xs_list *p;
3154 const xs_str *v;
3155
3156 p = list;
3157 while (xs_list_iter(&p, &v))
3158 cnt += _purge_file(v, mt);
3159
3160 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3161 }
3162}
3163
3164
3165static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3166/* purges all files in a user subdir older than days */
3167{
3168 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3169
3170 _purge_dir(u_subdir, days);
3171}
3172
3173
3174void purge_server(void)
3175/* purge global server data */
3176{
3177 xs *spec = xs_fmt("%s/object/??", srv_basedir);
3178 xs *dirs = xs_glob(spec, 0, 0);
3179 xs_list *p;
3180 const xs_str *v;
3181 int cnt = 0;
3182 int icnt = 0;
3183
3184 time_t mt = time(NULL) - 7 * 24 * 3600;
3185
3186 p = dirs;
3187 while (xs_list_iter(&p, &v)) {
3188 xs_list *p2;
3189 const xs_str *v2;
3190
3191 {
3192 xs *spec2 = xs_fmt("%s/" "*.json", v);
3193 xs *files = xs_glob(spec2, 0, 0);
3194
3195 p2 = files;
3196 while (xs_list_iter(&p2, &v2)) {
3197 int n_link;
3198
3199 /* old and with no hard links? */
3200 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3201 xs *s1 = xs_replace(v2, ".json", "");
3202 xs *l = xs_split(s1, "/");
3203 const char *md5 = xs_list_get(l, -1);
3204
3205 object_del_by_md5(md5);
3206 cnt++;
3207 }
3208 }
3209 }
3210
3211 {
3212 /* look for stray indexes */
3213 xs *speci = xs_fmt("%s/" "*_?.idx", v);
3214 xs *idxfs = xs_glob(speci, 0, 0);
3215
3216 p2 = idxfs;
3217 while (xs_list_iter(&p2, &v2)) {
3218 /* old enough to consider? */
3219 if (mtime(v2) < mt) {
3220 /* check if the indexed object is here */
3221 xs *o = xs_dup(v2);
3222 char *ext = strchr(o, '_');
3223
3224 if (ext) {
3225 *ext = '\0';
3226 o = xs_str_cat(o, ".json");
3227
3228 if (mtime(o) == 0.0) {
3229 /* delete */
3230 unlink(v2);
3231 srv_debug(1, xs_fmt("purged %s", v2));
3232 icnt++;
3233 }
3234 }
3235 }
3236 }
3237
3238 /* delete index backups */
3239 xs *specb = xs_fmt("%s/" "*.bak", v);
3240 xs *bakfs = xs_glob(specb, 0, 0);
3241
3242 p2 = bakfs;
3243 while (xs_list_iter(&p2, &v2)) {
3244 unlink(v2);
3245 srv_debug(1, xs_fmt("purged %s", v2));
3246 }
3247 }
3248 }
3249
3250 /* purge collected inboxes */
3251 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3252 _purge_dir(ib_dir, 7);
3253
3254 /* purge the instance timeline */
3255 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3256 int itl_gc = index_gc(itl_fn);
3257
3258 /* purge tag indexes */
3259 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3260 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3261 p = tag_dirs;
3262
3263 int tag_gc = 0;
3264 while (xs_list_iter(&p, &v)) {
3265 xs *spec2 = xs_fmt("%s/" "*.idx", v);
3266 xs *files = xs_glob(spec2, 0, 0);
3267 xs_list *p2;
3268 const xs_str *v2;
3269
3270 p2 = files;
3271 while (xs_list_iter(&p2, &v2)) {
3272 tag_gc += index_gc(v2);
3273 xs *bak = xs_fmt("%s.bak", v2);
3274 unlink(bak);
3275
3276 if (index_len(v2) == 0) {
3277 /* there are no longer any entry with this tag;
3278 purge it completely */
3279 unlink(v2);
3280 xs *dottag = xs_replace(v2, ".idx", ".tag");
3281 unlink(dottag);
3282 }
3283 }
3284 }
3285
3286 srv_debug(1, xs_fmt("purge: global "
3287 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3288}
3289
3290
3291void purge_user(snac *snac)
3292/* do the purge for this user */
3293{
3294 int priv_days, pub_days, user_days = 0;
3295 const char *v;
3296 int n;
3297
3298 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3299 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3300
3301 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3302 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3303 user_days = xs_number_get(v);
3304
3305 if (user_days) {
3306 /* override admin settings only if they are lesser */
3307 if (priv_days == 0 || user_days < priv_days)
3308 priv_days = user_days;
3309
3310 if (pub_days == 0 || user_days < pub_days)
3311 pub_days = user_days;
3312 }
3313
3314 _purge_user_subdir(snac, "hidden", priv_days);
3315 _purge_user_subdir(snac, "private", priv_days);
3316
3317 _purge_user_subdir(snac, "public", pub_days);
3318
3319 const char *idxs[] = { "followers.idx", "private.idx", "public.idx", "pinned.idx", NULL };
3320
3321 for (n = 0; idxs[n]; n++) {
3322 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3323 int gc = index_gc(idx);
3324 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3325 }
3326
3327 /* purge lists */
3328 {
3329 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3330 xs *lol = xs_glob(spec, 0, 0);
3331 int c = 0;
3332 const char *v;
3333
3334 while (xs_list_next(lol, &v, &c)) {
3335 int gc = index_gc(v);
3336 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3337 }
3338 }
3339
3340 /* unrelated to purging, but it's a janitorial process, so what the hell */
3341 verify_links(snac);
3342}
3343
3344
3345void purge_all(void)
3346/* purge all users */
3347{
3348 snac snac;
3349 xs *list = user_list();
3350 char *p;
3351 const char *uid;
3352
3353 p = list;
3354 while (xs_list_iter(&p, &uid)) {
3355 if (user_open(&snac, uid)) {
3356 purge_user(&snac);
3357 user_free(&snac);
3358 }
3359 }
3360
3361 purge_server();
3362
3363#ifndef NO_MASTODON_API
3364 mastoapi_purge();
3365#endif
3366}
3367
3368
3369/** archive **/
3370
3371void srv_archive(const char *direction, const char *url, xs_dict *req,
3372 const char *payload, int p_size,
3373 int status, xs_dict *headers,
3374 const char *body, int b_size)
3375/* archives a connection */
3376{
3377 /* obsessive archiving */
3378 xs *date = tid(0);
3379 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3380 FILE *f;
3381
3382 if (mkdirx(dir) != -1) {
3383 xs *meta_fn = xs_fmt("%s/_META", dir);
3384
3385 if ((f = fopen(meta_fn, "w")) != NULL) {
3386 xs *j1 = xs_json_dumps(req, 4);
3387 xs *j2 = xs_json_dumps(headers, 4);
3388
3389 fprintf(f, "dir: %s\n", direction);
3390
3391 if (url)
3392 fprintf(f, "url: %s\n", url);
3393
3394 fprintf(f, "req: %s\n", j1);
3395 fprintf(f, "p_size: %d\n", p_size);
3396 fprintf(f, "status: %d\n", status);
3397 fprintf(f, "response: %s\n", j2);
3398 fprintf(f, "b_size: %d\n", b_size);
3399 fclose(f);
3400 }
3401
3402 if (p_size && payload) {
3403 xs *payload_fn = NULL;
3404 xs *payload_fn_raw = NULL;
3405 const char *v = xs_dict_get(req, "content-type");
3406
3407 if (v && xs_str_in(v, "json") != -1) {
3408 payload_fn = xs_fmt("%s/payload.json", dir);
3409
3410 if ((f = fopen(payload_fn, "w")) != NULL) {
3411 xs *v1 = xs_json_loads(payload);
3412 xs *j1 = NULL;
3413
3414 if (v1 != NULL)
3415 j1 = xs_json_dumps(v1, 4);
3416
3417 if (j1 != NULL)
3418 fwrite(j1, strlen(j1), 1, f);
3419 else
3420 fwrite(payload, p_size, 1, f);
3421
3422 fclose(f);
3423 }
3424 }
3425
3426 payload_fn_raw = xs_fmt("%s/payload", dir);
3427
3428 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3429 fwrite(payload, p_size, 1, f);
3430 fclose(f);
3431 }
3432 }
3433
3434 if (b_size && body) {
3435 xs *body_fn = NULL;
3436 const char *v = xs_dict_get(headers, "content-type");
3437
3438 if (v && xs_str_in(v, "json") != -1) {
3439 body_fn = xs_fmt("%s/body.json", dir);
3440
3441 if ((f = fopen(body_fn, "w")) != NULL) {
3442 xs *v1 = xs_json_loads(body);
3443 xs *j1 = NULL;
3444
3445 if (v1 != NULL)
3446 j1 = xs_json_dumps(v1, 4);
3447
3448 if (j1 != NULL)
3449 fwrite(j1, strlen(j1), 1, f);
3450 else
3451 fwrite(body, b_size, 1, f);
3452
3453 fclose(f);
3454 }
3455 }
3456 else {
3457 body_fn = xs_fmt("%s/body", dir);
3458
3459 if ((f = fopen(body_fn, "w")) != NULL) {
3460 fwrite(body, b_size, 1, f);
3461 fclose(f);
3462 }
3463 }
3464 }
3465 }
3466}
3467
3468
3469void srv_archive_error(const char *prefix, const xs_str *err,
3470 const xs_dict *req, const xs_val *data)
3471/* archives an error */
3472{
3473 xs *ntid = tid(0);
3474 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
3475 FILE *f;
3476
3477 if ((f = fopen(fn, "w")) != NULL) {
3478 fprintf(f, "Error: %s\n", err);
3479
3480 if (req) {
3481 fprintf(f, "Request headers:\n");
3482
3483 xs_json_dump(req, 4, f);
3484
3485 fprintf(f, "\n");
3486 }
3487
3488 if (data) {
3489 fprintf(f, "Data:\n");
3490
3491 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
3492 xs_json_dump(data, 4, f);
3493 }
3494 else
3495 fprintf(f, "%s", data);
3496
3497 fprintf(f, "\n");
3498 }
3499
3500 fclose(f);
3501 }
3502}
3503
3504
3505void srv_archive_qitem(const char *prefix, xs_dict *q_item)
3506/* archives a q_item in the error folder */
3507{
3508 xs *ntid = tid(0);
3509 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
3510 FILE *f;
3511
3512 if ((f = fopen(fn, "w")) != NULL) {
3513 xs_json_dump(q_item, 4, f);
3514 fclose(f);
3515 }
3516}
3517
3518
3519t_announcement *announcement(const double after)
3520/* returns announcement text or NULL if none exists or it is olde than "after" */
3521{
3522 static const long int MAX_SIZE = 2048;
3523 static t_announcement a = {
3524 .text = NULL,
3525 .timestamp = 0.0,
3526 };
3527 static xs_str *fn = NULL;
3528 if (fn == NULL)
3529 fn = xs_fmt("%s/announcement.txt", srv_basedir);
3530
3531 const double ts = mtime(fn);
3532
3533 /* file does not exist or other than what was requested */
3534 if (ts == 0.0 || ts <= after)
3535 return NULL;
3536
3537 /* nothing changed, just return the current announcement */
3538 if (a.text != NULL && ts <= a.timestamp)
3539 return &a;
3540
3541 /* read and store new announcement */
3542 FILE *f;
3543
3544 if ((f = fopen(fn, "r")) != NULL) {
3545 fseek (f, 0, SEEK_END);
3546 const long int length = ftell(f);
3547
3548 if (length > MAX_SIZE) {
3549 /* this is probably unintentional */
3550 srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
3551 }
3552 else
3553 if (length > 0) {
3554 fseek (f, 0, SEEK_SET);
3555 char *buffer = malloc(length + 1);
3556 if (buffer) {
3557 fread(buffer, 1, length, f);
3558 buffer[length] = '\0';
3559
3560 free(a.text);
3561 a.text = buffer;
3562 a.timestamp = ts;
3563 }
3564 else {
3565 srv_log("Error allocating memory for announcement");
3566 }
3567 }
3568 else {
3569 /* an empty file means no announcement */
3570 free(a.text);
3571 a.text = NULL;
3572 a.timestamp = 0.0;
3573 }
3574
3575 fclose (f);
3576 }
3577
3578 if (a.text != NULL)
3579 return &a;
3580
3581 return NULL;
3582}