2.88
data.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2026 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#include "xs_random.h"
16#include "xs_po.h"
17#include "xs_http.h"
18
19#include "snac.h"
20
21#include <time.h>
22#include <sys/stat.h>
23#include <sys/file.h>
24#include <sys/time.h>
25#include <fcntl.h>
26#include <pthread.h>
27
28double disk_layout = 2.7;
29
30/* storage serializer */
31pthread_mutex_t data_mutex = {0};
32
33int snac_upgrade(xs_str **error);
34
35
36int srv_open(const char *basedir, int auto_upgrade)
37/* opens a server */
38{
39 int ret = 0;
40 xs *cfg_file = NULL;
41 FILE *f;
42 xs_str *error = NULL;
43
44 pthread_mutex_init(&data_mutex, NULL);
45
46 srv_basedir = xs_str_new(basedir);
47
48 if (xs_endswith(srv_basedir, "/"))
49 srv_basedir = xs_crop_i(srv_basedir, 0, -1);
50
51 cfg_file = xs_fmt("%s/server.json", basedir);
52
53 if ((f = fopen(cfg_file, "r")) == NULL)
54 error = xs_fmt("ERROR: cannot open '%s'", cfg_file);
55 else {
56 /* read full config file */
57 srv_config = xs_json_load(f);
58 fclose(f);
59
60 /* parse */
61
62 if (srv_config == NULL)
63 error = xs_fmt("ERROR: cannot parse '%s'", cfg_file);
64 else {
65 const char *host;
66 const char *prefix;
67 const char *dbglvl;
68 const char *proto;
69
70 host = xs_dict_get(srv_config, "host");
71 prefix = xs_dict_get(srv_config, "prefix");
72 dbglvl = xs_dict_get(srv_config, "dbglevel");
73 proto = xs_dict_get_def(srv_config, "protocol", "https");
74
75 if (host == NULL || prefix == NULL)
76 error = xs_str_new("ERROR: cannot get server data");
77 else {
78 srv_baseurl = xs_fmt("%s:/" "/%s%s", proto, host, prefix);
79
80 dbglevel = (int) xs_number_get(dbglvl);
81
82 if ((dbglvl = getenv("DEBUG")) != NULL) {
83 dbglevel = atoi(dbglvl);
84 error = xs_fmt("DEBUG level set to %d from environment", dbglevel);
85 }
86
87 if (auto_upgrade)
88 ret = snac_upgrade(&error);
89 else {
90 if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
91 error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
92 else {
93 if (!check_strip_tool()) {
94 const char *mp = xs_dict_get(srv_config, "mogrify_path");
95 if (mp == NULL) mp = "mogrify";
96 error = xs_fmt("ERROR: strip_exif enabled but '%s' not found or not working (set 'mogrify_path' in server.json)", mp);
97 }
98 else
99 ret = 1;
100 }
101 }
102 }
103
104 }
105 }
106
107 if (error != NULL)
108 srv_log(error);
109
110 if (!ret)
111 return ret;
112
113 /* create the queue/ subdir, just in case */
114 xs *qdir = xs_fmt("%s/queue", srv_basedir);
115 mkdirx(qdir);
116
117 xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
118 mkdirx(ibdir);
119
120 xs *tmpdir = xs_fmt("%s/tmp", srv_basedir);
121 mkdirx(tmpdir);
122
123 xs *faildir = xs_fmt("%s/failure", srv_basedir);
124 mkdirx(faildir);
125
126#ifdef __APPLE__
127/* Apple uses st_atimespec instead of st_atim etc */
128#define st_atim st_atimespec
129#define st_ctim st_ctimespec
130#define st_mtim st_mtimespec
131#endif
132
133 sbox_enter(srv_basedir);
134
135 /* read (and drop) emojis.json, possibly creating it */
136 xs_free(emojis());
137
138 /* if style.css does not exist, create it */
139 xs *css_fn = xs_fmt("%s/style.css", srv_basedir);
140
141 if (mtime(css_fn) == 0) {
142 srv_log(xs_fmt("Writing style.css"));
143 write_default_css();
144 }
145
146 /* create the proxy token seed */
147 {
148 char rnd[16];
149 xs_rnd_buf(rnd, sizeof(rnd));
150
151 srv_proxy_token_seed = xs_hex_enc(rnd, sizeof(rnd));
152 }
153
154 /* ensure user directories include important subdirectories */
155 xs *users = user_list();
156 const char *uid;
157
158 xs_list_foreach(users, uid) {
159 xs *impdir = xs_fmt("%s/user/%s/import", srv_basedir, uid);
160 xs *expdir = xs_fmt("%s/user/%s/export", srv_basedir, uid);
161
162 mkdirx(impdir);
163 mkdirx(expdir);
164 }
165
166 /* languages */
167 srv_langs = xs_dict_new();
168 srv_langs = xs_dict_set(srv_langs, "en", xs_stock(XSTYPE_NULL));
169
170 xs *l_dir = xs_fmt("%s/lang/", srv_basedir);
171 mkdirx(l_dir);
172
173 l_dir = xs_str_cat(l_dir, "*.po");
174 xs *pos = xs_glob(l_dir, 0, 0);
175 const char *po;
176
177 xs_list_foreach(pos, po) {
178 xs *d = xs_po_to_dict(po);
179
180 if (xs_is_dict(d)) {
181 xs *l = xs_split(po, "/");
182 xs *id = xs_dup(xs_list_get(l, -1));
183 id = xs_replace_i(id, ".po", "");
184
185 srv_langs = xs_dict_set(srv_langs, id, d);
186 }
187 }
188
189 return ret;
190}
191
192
193void srv_free(void)
194{
195 xs_free(srv_basedir);
196 xs_free(srv_config);
197 xs_free(srv_baseurl);
198
199 pthread_mutex_destroy(&data_mutex);
200}
201
202
203void user_free(snac *snac)
204/* frees a user snac */
205{
206 xs_free(snac->uid);
207 xs_free(snac->basedir);
208 xs_free(snac->config);
209 xs_free(snac->config_o);
210 xs_free(snac->key);
211 xs_free(snac->links);
212 xs_free(snac->actor);
213 xs_free(snac->md5);
214}
215
216
217int user_open(snac *user, const char *uid)
218/* opens a user */
219{
220 int ret = 0;
221
222 *user = (snac){0};
223
224 if (validate_uid(uid)) {
225 xs *cfg_file = NULL;
226 FILE *f;
227
228 xs *t = xs_fmt("%s/user/%s", srv_basedir, uid);
229
230 if (mtime(t) == 0.0) {
231 /* user folder does not exist; try with a different case */
232 xs *lcuid = xs_tolower_i(xs_dup(uid));
233 xs *ulist = user_list();
234 xs_list *p = ulist;
235 const xs_str *v;
236
237 while (xs_list_iter(&p, &v)) {
238 xs *v2 = xs_tolower_i(xs_dup(v));
239
240 if (strcmp(lcuid, v2) == 0) {
241 user->uid = xs_dup(v);
242 break;
243 }
244 }
245 }
246 else
247 user->uid = xs_str_new(uid);
248
249 if (user->uid == NULL)
250 return ret;
251
252 user->basedir = xs_fmt("%s/user/%s", srv_basedir, user->uid);
253
254 cfg_file = xs_fmt("%s/user.json", user->basedir);
255
256 if ((f = fopen(cfg_file, "r")) != NULL) {
257 /* read full config file */
258 user->config = xs_json_load(f);
259 fclose(f);
260
261 if (user->config != NULL) {
262 xs *key_file = xs_fmt("%s/key.json", user->basedir);
263
264 if ((f = fopen(key_file, "r")) != NULL) {
265 user->key = xs_json_load(f);
266 fclose(f);
267
268 if (user->key != NULL) {
269 user->actor = xs_fmt("%s/%s", srv_baseurl, user->uid);
270 user->md5 = xs_md5_hex(user->actor, strlen(user->actor));
271
272 /* everything is ok right now */
273 ret = 1;
274
275 /* does it have a configuration override? */
276 xs *cfg_file_o = xs_fmt("%s/user_o.json", user->basedir);
277 if ((f = fopen(cfg_file_o, "r")) != NULL) {
278 user->config_o = xs_json_load(f);
279 fclose(f);
280
281 if (user->config_o == NULL)
282 srv_log(xs_fmt("error parsing '%s'", cfg_file_o));
283 }
284
285 if (user->config_o == NULL)
286 user->config_o = xs_dict_new();
287 }
288 else
289 srv_log(xs_fmt("error parsing '%s'", key_file));
290 }
291 else
292 srv_log(xs_fmt("error opening '%s' %d", key_file, errno));
293 }
294 else
295 srv_log(xs_fmt("error parsing '%s'", cfg_file));
296
297 user->tz = xs_dict_get_def(user->config, "tz", "UTC");
298 }
299 else
300 srv_debug(2, xs_fmt("error opening '%s' %d", cfg_file, errno));
301
302 /* verified links */
303 xs *links_file = xs_fmt("%s/links.json", user->basedir);
304
305 if ((f = fopen(links_file, "r")) != NULL) {
306 user->links = xs_json_load(f);
307 fclose(f);
308 }
309 }
310 else
311 srv_debug(1, xs_fmt("invalid user '%s'", uid));
312
313 if (!ret)
314 user_free(user);
315
316 return ret;
317}
318
319
320xs_list *user_list(void)
321/* returns the list of user ids */
322{
323 xs *spec = xs_fmt("%s/user/" "*", srv_basedir);
324 return xs_glob(spec, 1, 0);
325}
326
327
328int user_open_by_md5(snac *snac, const char *md5)
329/* iterates all users searching by md5 */
330{
331 xs *ulist = user_list();
332 xs_list *p = ulist;
333 const xs_str *v;
334
335 while (xs_list_iter(&p, &v)) {
336 user_open(snac, v);
337
338 if (strcmp(snac->md5, md5) == 0)
339 return 1;
340
341 user_free(snac);
342 }
343
344 return 0;
345}
346
347
348int user_persist(snac *snac, int publish)
349/* store user */
350{
351 xs *fn = xs_fmt("%s/user.json", snac->basedir);
352 xs *bfn = xs_fmt("%s.bak", fn);
353 FILE *f;
354
355 if (publish) {
356 /* check if any of the relevant fields have really changed */
357 if ((f = fopen(fn, "r")) != NULL) {
358 xs *old = xs_json_load(f);
359 fclose(f);
360
361 if (old != NULL) {
362 int nw = 0;
363 const char *fields[] = { "header", "avatar", "name", "bio", "alias", "alias_raw",
364 "metadata", "latitude", "longitude", NULL };
365
366 for (int n = 0; fields[n]; n++) {
367 const char *of = xs_dict_get(old, fields[n]);
368 const char *nf = xs_dict_get(snac->config, fields[n]);
369
370 if (of == NULL && nf == NULL)
371 continue;
372
373 if (xs_type(of) != XSTYPE_STRING || xs_type(nf) != XSTYPE_STRING || strcmp(of, nf)) {
374 nw = 1;
375 break;
376 }
377 }
378
379 if (!nw)
380 publish = 0;
381 else {
382 /* uncache the actor object */
383 object_del(snac->actor);
384 }
385 }
386 }
387 }
388
389 rename(fn, bfn);
390
391 if ((f = fopen(fn, "w")) != NULL) {
392 xs_json_dump(snac->config, 4, f);
393 fclose(f);
394 }
395 else
396 rename(bfn, fn);
397
398 history_del(snac, "timeline.html_");
399 timeline_touch(snac);
400
401 if (publish) {
402 xs *a_msg = msg_actor(snac);
403 xs *u_msg = msg_update(snac, a_msg);
404
405 enqueue_message(snac, u_msg);
406
407 enqueue_verify_links(snac);
408 }
409
410 return 0;
411}
412
413
414double mtime_nl(const char *fn, int *n_link)
415/* returns the mtime and number of links of a file or directory, or 0.0 */
416{
417 struct stat st;
418 double r = 0.0;
419 int n = 0;
420
421 if (fn && stat(fn, &st) != -1) {
422 r = (double) st.st_mtim.tv_sec;
423 n = st.st_nlink;
424 }
425
426 if (n_link)
427 *n_link = n;
428
429 return r;
430}
431
432
433#define MIN(v1, v2) ((v1) < (v2) ? (v1) : (v2))
434
435double f_ctime(const char *fn)
436/* returns the ctime of a file or directory, or 0.0 */
437{
438 struct stat st;
439 double r = 0.0;
440
441 if (fn && stat(fn, &st) != -1) {
442 /* return the lowest of ctime and mtime;
443 there are operations that change the ctime, like link() */
444 r = (double) MIN(st.st_ctim.tv_sec, st.st_mtim.tv_sec);
445 }
446
447 return r;
448}
449
450
451int is_md5_hex(const char *md5)
452{
453 return xs_is_hex(md5) && strlen(md5) == MD5_HEX_SIZE - 1;
454}
455
456
457/** database 2.1+ **/
458
459/** indexes **/
460
461
462int index_add_md5(const char *fn, const char *md5)
463/* adds an md5 to an index */
464{
465 int status = HTTP_STATUS_CREATED;
466 FILE *f;
467
468 if (!is_md5_hex(md5)) {
469 srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
470 return HTTP_STATUS_BAD_REQUEST;
471 }
472
473 pthread_mutex_lock(&data_mutex);
474
475 if ((f = fopen(fn, "a")) != NULL) {
476 flock(fileno(f), LOCK_EX);
477
478 /* ensure the position is at the end after getting the lock */
479 fseek(f, 0, SEEK_END);
480
481 fprintf(f, "%s\n", md5);
482 fclose(f);
483 }
484 else
485 status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
486
487 pthread_mutex_unlock(&data_mutex);
488
489 return status;
490}
491
492
493int index_add(const char *fn, const char *id)
494/* adds an id to an index */
495{
496 xs *md5 = xs_md5_hex(id, strlen(id));
497 return index_add_md5(fn, md5);
498}
499
500
501int index_del_md5(const char *fn, const char *md5)
502/* deletes an md5 from an index */
503{
504 int status = HTTP_STATUS_NOT_FOUND;
505 FILE *f;
506
507 pthread_mutex_lock(&data_mutex);
508
509 if ((f = fopen(fn, "r+")) != NULL) {
510 char line[256];
511
512 while (fgets(line, sizeof(line), f) != NULL) {
513 line[MD5_HEX_SIZE - 1] = '\0';
514
515 if (strcmp(line, md5) == 0) {
516 /* found! just rewind, overwrite it with garbage
517 and an eventual call to index_gc() will clean it
518 [yes: this breaks index_len()] */
519 fseek(f, -MD5_HEX_SIZE, SEEK_CUR);
520 fwrite("-", 1, 1, f);
521 status = HTTP_STATUS_OK;
522
523 break;
524 }
525 }
526
527 fclose(f);
528 }
529 else
530 status = HTTP_STATUS_GONE;
531
532 pthread_mutex_unlock(&data_mutex);
533
534 return status;
535}
536
537
538int index_del(const char *fn, const char *id)
539/* deletes an id from an index */
540{
541 xs *md5 = xs_md5_hex(id, strlen(id));
542 return index_del_md5(fn, md5);
543}
544
545
546int index_gc(const char *fn)
547/* garbage-collects an index, deleting objects that are not here */
548{
549 FILE *i, *o;
550 int gc = -1;
551
552 pthread_mutex_lock(&data_mutex);
553
554 if ((i = fopen(fn, "r")) != NULL) {
555 xs *nfn = xs_fmt("%s.new", fn);
556 char line[256];
557
558 if ((o = fopen(nfn, "w")) != NULL) {
559 gc = 0;
560
561 while (fgets(line, sizeof(line), i) != NULL) {
562 line[MD5_HEX_SIZE - 1] = '\0';
563
564 if (line[0] != '-' && object_here_by_md5(line))
565 fprintf(o, "%s\n", line);
566 else
567 gc++;
568 }
569
570 fclose(o);
571
572 xs *ofn = xs_fmt("%s.bak", fn);
573
574 unlink(ofn);
575 link(fn, ofn);
576 rename(nfn, fn);
577 }
578
579 fclose(i);
580 }
581
582 pthread_mutex_unlock(&data_mutex);
583
584 return gc;
585}
586
587
588int index_in_md5(const char *fn, const char *md5)
589/* checks if the md5 is already in the index */
590{
591 FILE *f;
592 int ret = 0;
593
594 if ((f = fopen(fn, "r")) != NULL) {
595 flock(fileno(f), LOCK_SH);
596
597 char line[256];
598
599 while (!ret && fgets(line, sizeof(line), f) != NULL) {
600 line[MD5_HEX_SIZE - 1] = '\0';
601
602 if (strcmp(line, md5) == 0)
603 ret = 1;
604 }
605
606 fclose(f);
607 }
608
609 return ret;
610}
611
612
613int index_in(const char *fn, const char *id)
614/* checks if the object id is already in the index */
615{
616 xs *md5 = xs_md5_hex(id, strlen(id));
617 return index_in_md5(fn, md5);
618}
619
620
621int index_first(const char *fn, char md5[MD5_HEX_SIZE])
622/* reads the first entry of an index */
623{
624 FILE *f;
625 int ret = 0;
626
627 if ((f = fopen(fn, "r")) != NULL) {
628 if (fread(md5, MD5_HEX_SIZE, 1, f)) {
629 md5[MD5_HEX_SIZE - 1] = '\0';
630 ret = 1;
631 }
632
633 fclose(f);
634 }
635
636 return ret;
637}
638
639
640int index_len(const char *fn)
641/* returns the number of elements in an index */
642{
643 struct stat st;
644 int len = 0;
645
646 if (stat(fn, &st) != -1)
647 len = st.st_size / MD5_HEX_SIZE;
648
649 return len;
650}
651
652
653xs_list *index_list(const char *fn, int max)
654/* returns an index as a list */
655{
656 xs_list *list = xs_list_new();
657 FILE *f;
658 int n = 0;
659
660 if ((f = fopen(fn, "r")) != NULL) {
661 flock(fileno(f), LOCK_SH);
662
663 char line[256];
664
665 while (n < max && fgets(line, sizeof(line), f) != NULL) {
666 if (line[0] != '-') {
667 line[MD5_HEX_SIZE - 1] = '\0';
668 list = xs_list_append(list, line);
669 n++;
670 }
671 }
672
673 fclose(f);
674 }
675
676 return list;
677}
678
679
680int index_desc_next(FILE *f, char md5[MD5_HEX_SIZE])
681/* reads the next entry of a desc index */
682{
683 for (;;) {
684 /* move backwards 2 entries */
685 if (fseek(f, MD5_HEX_SIZE * -2, SEEK_CUR) == -1)
686 return 0;
687
688 /* read and md5 */
689 if (!fread(md5, MD5_HEX_SIZE, 1, f))
690 return 0;
691
692 if (md5[0] != '-')
693 break;
694 }
695
696 md5[MD5_HEX_SIZE - 1] = '\0';
697
698 return 1;
699}
700
701
702int index_desc_first(FILE *f, char md5[MD5_HEX_SIZE], int skip)
703/* reads the first entry of a desc index */
704{
705 /* try to position at the end and then back to the first element */
706 if (fseek(f, 0, SEEK_END) || fseek(f, (skip + 1) * -MD5_HEX_SIZE, SEEK_CUR))
707 return 0;
708
709 /* try to read an md5 */
710 if (!fread(md5, MD5_HEX_SIZE, 1, f))
711 return 0;
712
713 /* null-terminate */
714 md5[MD5_HEX_SIZE - 1] = '\0';
715
716 /* deleted? retry next */
717 if (md5[0] == '-')
718 return index_desc_next(f, md5);
719
720 return 1;
721}
722
723int index_asc_first(FILE *f,char md5[MD5_HEX_SIZE], const char *seek_md5)
724/* reads the first entry of an ascending index, starting from a given md5 */
725{
726 fseek(f, SEEK_SET, 0);
727 while (fread(md5, MD5_HEX_SIZE, 1, f)) {
728 md5[MD5_HEX_SIZE - 1] = '\0';
729 if (strcmp(md5,seek_md5) == 0) {
730 return index_asc_next(f, md5);
731 }
732 }
733 return 0;
734}
735
736int index_asc_next(FILE *f, char md5[MD5_HEX_SIZE])
737/* reads the next entry of an ascending index */
738{
739 for (;;) {
740 /* read an md5 */
741 if (!fread(md5, MD5_HEX_SIZE, 1, f))
742 return 0;
743
744 /* deleted, skip */
745 if (md5[0] != '-')
746 break;
747 }
748
749 md5[MD5_HEX_SIZE - 1] = '\0';
750
751 return 1;
752}
753
754
755xs_list *index_list_desc(const char *fn, int skip, int show)
756/* returns an index as a list, in reverse order */
757{
758 xs_list *list = xs_list_new();
759 FILE *f;
760
761 if ((f = fopen(fn, "r")) != NULL) {
762 char md5[MD5_HEX_SIZE];
763
764 if (index_desc_first(f, md5, skip)) {
765 int n = 1;
766
767 do {
768 list = xs_list_append(list, md5);
769 } while (n++ < show && index_desc_next(f, md5));
770 }
771
772 fclose(f);
773 }
774
775 return list;
776}
777
778
779/** objects **/
780
781static xs_str *_object_fn_by_md5(const char *md5, const char *func)
782{
783 xs *bfn = xs_fmt("%s/object/%c%c", srv_basedir, md5[0], md5[1]);
784 xs_str *ret;
785 int ok = 1;
786
787 /* an object deleted from an index; fail but don't bark */
788 if (md5[0] == '-')
789 ok = 0;
790 else
791 if (!is_md5_hex(md5)) {
792 srv_log(xs_fmt("_object_fn_by_md5() [from %s()]: bad md5 '%s'", func, md5));
793 ok = 0;
794 }
795
796 if (ok) {
797 mkdirx(bfn);
798 ret = xs_fmt("%s/%s.json", bfn, md5);
799 }
800 else
801 ret = xs_fmt("%s/object/invalid/invalid.json", srv_basedir);
802
803 return ret;
804}
805
806
807static xs_str *_object_fn(const char *id)
808{
809 xs *md5 = xs_md5_hex(id, strlen(id));
810 return _object_fn_by_md5(md5, "_object_fn");
811}
812
813
814int object_here_by_md5(const char *id)
815/* checks if an object is already downloaded */
816{
817 xs *fn = _object_fn_by_md5(id, "object_here_by_md5");
818 return mtime(fn) > 0.0;
819}
820
821
822int object_here(const char *id)
823/* checks if an object is already downloaded */
824{
825 xs *fn = _object_fn(id);
826 return mtime(fn) > 0.0;
827}
828
829
830int object_get_by_md5(const char *md5, xs_dict **obj)
831/* returns a stored object, optionally of the requested type */
832{
833 int status = HTTP_STATUS_NOT_FOUND;
834 xs *fn = _object_fn_by_md5(md5, "object_get_by_md5");
835 FILE *f;
836
837 if ((f = fopen(fn, "r")) != NULL) {
838 *obj = xs_json_load(f);
839 fclose(f);
840
841 if (*obj)
842 status = HTTP_STATUS_OK;
843 }
844 else
845 *obj = NULL;
846
847 return status;
848}
849
850
851int object_get(const char *id, xs_dict **obj)
852/* returns a stored object, optionally of the requested type */
853{
854 xs *md5 = xs_md5_hex(id, strlen(id));
855 return object_get_by_md5(md5, obj);
856}
857
858
859int _object_add(const char *id, const xs_dict *obj, int ow)
860/* stores an object */
861{
862 int status = HTTP_STATUS_CREATED; /* Created */
863 xs *fn = _object_fn(id);
864 FILE *f;
865
866 if (mtime(fn) > 0.0) {
867 if (!ow) {
868 /* object already here */
869 srv_debug(1, xs_fmt("object_add object already here %s", id));
870 return HTTP_STATUS_NO_CONTENT;
871 }
872 else
873 status = HTTP_STATUS_OK;
874 }
875
876 if ((f = fopen(fn, "w")) != NULL) {
877 flock(fileno(f), LOCK_EX);
878
879 xs_json_dump(obj, 4, f);
880 fclose(f);
881
882 /* does this object has a parent? */
883 const char *in_reply_to = get_in_reply_to(obj);
884
885 if (!xs_is_null(in_reply_to) && *in_reply_to) {
886 /* update the children index of the parent */
887 xs *c_idx = _object_fn(in_reply_to);
888
889 c_idx = xs_replace_i(c_idx, ".json", "_c.idx");
890
891 if (!index_in(c_idx, id)) {
892 index_add(c_idx, id);
893 srv_debug(1, xs_fmt("object_add added child %s to %s", id, c_idx));
894 }
895 else
896 srv_debug(1, xs_fmt("object_add %s child already in %s", id, c_idx));
897
898 /* create a one-element index with the parent */
899 xs *p_idx = xs_replace(fn, ".json", "_p.idx");
900
901 if (mtime(p_idx) == 0.0) {
902 index_add(p_idx, in_reply_to);
903 srv_debug(1, xs_fmt("object_add added parent %s to %s", in_reply_to, p_idx));
904 }
905 }
906 }
907 else {
908 srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
909 status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
910 }
911
912 srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
913
914 return status;
915}
916
917
918int object_add(const char *id, const xs_dict *obj)
919/* stores an object */
920{
921 return _object_add(id, obj, 0);
922}
923
924
925int object_add_ow(const char *id, const xs_dict *obj)
926/* stores an object (overwriting allowed) */
927{
928 return _object_add(id, obj, 1);
929}
930
931
932int object_del_by_md5(const char *md5)
933/* deletes an object by its md5 */
934{
935 int status = HTTP_STATUS_NOT_FOUND;
936 xs *fn = _object_fn_by_md5(md5, "object_del_by_md5");
937
938 if (unlink(fn) != -1) {
939 status = HTTP_STATUS_OK;
940
941 /* also delete associated indexes */
942 xs *spec = xs_dup(fn);
943 spec = xs_replace_i(spec, ".json", "*.idx");
944 xs *files = xs_glob(spec, 0, 0);
945 char *p;
946 const char *v;
947
948 p = files;
949 while (xs_list_iter(&p, &v)) {
950 srv_debug(1, xs_fmt("object_del index %s", v));
951 unlink(v);
952 }
953 }
954
955 srv_debug(1, xs_fmt("object_del %s %d", fn, status));
956
957 return status;
958}
959
960
961int object_del(const char *id)
962/* deletes an object */
963{
964 xs *md5 = xs_md5_hex(id, strlen(id));
965 return object_del_by_md5(md5);
966}
967
968
969int object_del_if_unref(const char *id)
970/* deletes an object if its n_links < 2 */
971{
972 xs *fn = _object_fn(id);
973 int n_links;
974 int ret = 0;
975
976 if (mtime_nl(fn, &n_links) > 0.0 && n_links < 2)
977 ret = object_del(id);
978
979 return ret;
980}
981
982
983double object_ctime_by_md5(const char *md5)
984{
985 xs *fn = _object_fn_by_md5(md5, "object_ctime_by_md5");
986 return f_ctime(fn);
987}
988
989
990double object_ctime(const char *id)
991{
992 xs *md5 = xs_md5_hex(id, strlen(id));
993 return object_ctime_by_md5(md5);
994}
995
996
997double object_mtime_by_md5(const char *md5)
998{
999 xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5");
1000 return mtime(fn);
1001}
1002
1003
1004double object_mtime(const char *id)
1005{
1006 xs *md5 = xs_md5_hex(id, strlen(id));
1007 return object_mtime_by_md5(md5);
1008}
1009
1010
1011void object_touch(const char *id)
1012{
1013 xs *md5 = xs_md5_hex(id, strlen(id));
1014 xs *fn = _object_fn_by_md5(md5, "object_touch");
1015
1016 if (mtime(fn))
1017 utimes(fn, NULL);
1018}
1019
1020
1021xs_str *_object_index_fn(const char *id, const char *idxsfx)
1022/* returns the filename of an object's index */
1023{
1024 xs_str *fn = _object_fn(id);
1025 return xs_replace_i(fn, ".json", idxsfx);
1026}
1027
1028
1029int object_likes_len(const char *id)
1030/* returns the number of likes (without reading the index) */
1031{
1032 xs *fn = _object_index_fn(id, "_l.idx");
1033 return index_len(fn);
1034}
1035
1036
1037int object_announces_len(const char *id)
1038/* returns the number of announces (without reading the index) */
1039{
1040 xs *fn = _object_index_fn(id, "_a.idx");
1041 return index_len(fn);
1042}
1043
1044
1045xs_list *object_children(const char *id)
1046/* returns the list of an object's children */
1047{
1048 xs *fn = _object_index_fn(id, "_c.idx");
1049 return index_list(fn, XS_ALL);
1050}
1051
1052
1053xs_list *object_get_emoji_reacts(const char *id)
1054/* returns the list of an object's emoji reactions */
1055{
1056 xs *fn = _object_index_fn(id, "_e.idx");
1057 return index_list(fn, XS_ALL);
1058}
1059
1060
1061xs_list *object_likes(const char *id)
1062{
1063 xs *fn = _object_index_fn(id, "_l.idx");
1064 return index_list(fn, XS_ALL);
1065}
1066
1067
1068xs_list *object_announces(const char *id)
1069{
1070 xs *fn = _object_index_fn(id, "_a.idx");
1071 return index_list(fn, XS_ALL);
1072}
1073
1074
1075int object_parent(const char *md5, char parent[MD5_HEX_SIZE])
1076/* returns the object parent, if any */
1077{
1078 xs *fn = _object_fn_by_md5(md5, "object_parent");
1079
1080 fn = xs_replace_i(fn, ".json", "_p.idx");
1081 return index_first(fn, parent);
1082}
1083
1084
1085int object_admire(const char *id, const char *actor, int like)
1086/* actor likes or announces this object */
1087{
1088 int status = HTTP_STATUS_OK;
1089 xs *fn = _object_fn(id);
1090
1091 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
1092
1093 if (!index_in(fn, actor)) {
1094 status = index_add(fn, actor);
1095
1096 srv_debug(1, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn));
1097 }
1098
1099 return status;
1100}
1101
1102
1103int object_unadmire(const char *id, const char *actor, int like)
1104/* actor retrives their likes, announces or emojis this object */
1105{
1106 switch (like) {
1107 case 0:
1108 like = 'a';
1109 break;
1110 case 1:
1111 like = 'l';
1112 break;
1113 case 2:
1114 like = 'e';
1115 break;
1116 }
1117 int status;
1118 xs *fn = _object_fn(id);
1119
1120 char sfx[7] = "_x.idx";
1121 sfx[1] = like;
1122
1123 fn = xs_replace_i(fn, ".json", sfx);
1124
1125 status = index_del(fn, actor);
1126
1127 if (valid_status(status))
1128 index_gc(fn);
1129
1130 srv_debug(0,
1131 xs_fmt("object_unadmire (%s) %s %s %d", like >= 'e' ?
1132 (like == 'l' ? "Like" : "EmojiReact") : "Announce" , actor, fn, status));
1133
1134 return status;
1135}
1136
1137int object_emoji_react(const char *mid, const char *eid)
1138/* actor reacts w/ an emoji */
1139{
1140 int status = HTTP_STATUS_OK;
1141 xs *fn = _object_fn(mid);
1142
1143 fn = xs_replace_i(fn, ".json", "_e.idx");
1144
1145 if (!index_in(fn, eid)) {
1146 status = index_add(fn, eid);
1147
1148 srv_debug(1, xs_fmt("object_emoji_react (%s) added %s to %s", "EmojiReact", eid, fn));
1149 }
1150
1151 return status;
1152}
1153
1154
1155int object_rm_emoji_react(const char *mid, const char *eid)
1156/* actor retrives their emoji reaction */
1157{
1158 int status;
1159 xs *fn = _object_fn(mid);
1160
1161 fn = xs_replace_i(fn, ".json", "_e.idx");
1162
1163 status = index_del(fn, eid);
1164 object_del(eid);
1165
1166 if (valid_status(status))
1167 index_gc(fn);
1168
1169 srv_debug(0,
1170 xs_fmt("object_unadmire (EmojiReact) %s %s %d", eid, fn, status));
1171
1172 return status;
1173}
1174
1175
1176xs_str *object_user_cache_fn_by_md5(snac *user, const char *md5, const char *cachedir)
1177{
1178 return xs_fmt("%s/%s/%s.json", user->basedir, cachedir, md5);
1179}
1180
1181
1182xs_str *object_user_cache_fn(snac *user, const char *id, const char *cachedir)
1183{
1184 xs *md5 = xs_md5_hex(id, strlen(id));
1185 return object_user_cache_fn_by_md5(user, md5, cachedir);
1186}
1187
1188
1189xs_str *object_user_cache_index_fn(snac *user, const char *cachedir)
1190{
1191 return xs_fmt("%s/%s.idx", user->basedir, cachedir);
1192}
1193
1194
1195int _object_user_cache(snac *user, const char *id, const char *cachedir, int del)
1196/* adds or deletes from a user cache */
1197{
1198 xs *ofn = _object_fn(id);
1199 xs *cfn = object_user_cache_fn(user, id, cachedir);
1200 xs *idx = object_user_cache_index_fn(user, cachedir);
1201 int ret;
1202
1203 if (del) {
1204 ret = unlink(cfn);
1205 index_del(idx, id);
1206 }
1207 else {
1208 /* create the subfolder, if it does not exist */
1209 xs *dir = xs_fmt("%s/%s/", user->basedir, cachedir);
1210 mkdirx(dir);
1211
1212 if ((ret = link(ofn, cfn)) != -1)
1213 index_add(idx, id);
1214 }
1215
1216 return ret;
1217}
1218
1219
1220int object_user_cache_add(snac *user, const char *id, const char *cachedir)
1221/* caches an object into a user cache */
1222{
1223 return _object_user_cache(user, id, cachedir, 0);
1224}
1225
1226
1227int object_user_cache_del(snac *user, const char *id, const char *cachedir)
1228/* deletes an object from a user cache */
1229{
1230 return _object_user_cache(user, id, cachedir, 1);
1231}
1232
1233
1234int object_user_cache_in(snac *user, const char *id, const char *cachedir)
1235/* checks if an object is stored in a cache */
1236{
1237 xs *cfn = object_user_cache_fn(user, id, cachedir);
1238 return !!(mtime(cfn) != 0.0);
1239}
1240
1241
1242int object_user_cache_in_by_md5(snac *user, const char *md5, const char *cachedir)
1243/* checks if an object is stored in a cache */
1244{
1245 xs *cfn = object_user_cache_fn_by_md5(user, md5, cachedir);
1246 return !!(mtime(cfn) != 0.0);
1247}
1248
1249
1250xs_list *object_user_cache_list(snac *user, const char *cachedir, int max, int inv)
1251/* returns the objects in a cache as a list */
1252{
1253 xs *idx = xs_fmt("%s/%s.idx", user->basedir, cachedir);
1254 return inv ? index_list_desc(idx, 0, max) : index_list(idx, max);
1255}
1256
1257
1258/** specialized functions **/
1259
1260/** followers **/
1261
1262int follower_add(snac *snac, const char *actor)
1263/* adds a follower */
1264{
1265 int ret = object_user_cache_add(snac, actor, "followers");
1266
1267 snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
1268
1269 return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
1270}
1271
1272
1273int follower_del(snac *snac, const char *actor)
1274/* deletes a follower */
1275{
1276 int ret = object_user_cache_del(snac, actor, "followers");
1277
1278 snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
1279
1280 return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
1281}
1282
1283
1284int follower_check(snac *snac, const char *actor)
1285/* checks if someone is a follower */
1286{
1287 return object_user_cache_in(snac, actor, "followers");
1288}
1289
1290
1291int follower_list_len(snac *snac)
1292/* returns the number of followers */
1293{
1294 xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1295 return xs_list_len(list);
1296}
1297
1298
1299xs_list *follower_list(snac *snac)
1300/* returns the list of followers */
1301{
1302 xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1303 xs_list *fwers = xs_list_new();
1304 char *p;
1305 const char *v;
1306
1307 /* resolve the list of md5 to be a list of actors */
1308 p = list;
1309 while (xs_list_iter(&p, &v)) {
1310 xs *a_obj = NULL;
1311
1312 if (valid_status(object_get_by_md5(v, &a_obj))) {
1313 const char *actor = xs_dict_get(a_obj, "id");
1314
1315 if (!xs_is_null(actor)) {
1316 /* check if the actor is still cached */
1317 xs *fn = xs_fmt("%s/followers/%s.json", snac->basedir, v);
1318
1319 if (mtime(fn) > 0.0)
1320 fwers = xs_list_append(fwers, actor);
1321 }
1322 }
1323 }
1324
1325 return fwers;
1326}
1327
1328
1329/** pending followers **/
1330
1331int pending_add(snac *user, const char *actor, const xs_dict *msg)
1332/* stores the follow message for later confirmation */
1333{
1334 xs *dir = xs_fmt("%s/pending", user->basedir);
1335 xs *md5 = xs_md5_hex(actor, strlen(actor));
1336 xs *fn = xs_fmt("%s/%s.json", dir, md5);
1337 FILE *f;
1338
1339 mkdirx(dir);
1340
1341 if ((f = fopen(fn, "w")) == NULL)
1342 return -1;
1343
1344 xs_json_dump(msg, 4, f);
1345 fclose(f);
1346
1347 return 0;
1348}
1349
1350
1351int pending_check(snac *user, const char *actor)
1352/* checks if there is a pending follow confirmation for the actor */
1353{
1354 xs *md5 = xs_md5_hex(actor, strlen(actor));
1355 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1356
1357 return mtime(fn) != 0;
1358}
1359
1360
1361xs_dict *pending_get(snac *user, const char *actor)
1362/* returns the pending follow confirmation for the actor */
1363{
1364 xs *md5 = xs_md5_hex(actor, strlen(actor));
1365 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1366 xs_dict *msg = NULL;
1367 FILE *f;
1368
1369 if ((f = fopen(fn, "r")) != NULL) {
1370 msg = xs_json_load(f);
1371 fclose(f);
1372 }
1373
1374 return msg;
1375}
1376
1377
1378void pending_del(snac *user, const char *actor)
1379/* deletes a pending follow confirmation for the actor */
1380{
1381 xs *md5 = xs_md5_hex(actor, strlen(actor));
1382 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1383
1384 unlink(fn);
1385}
1386
1387
1388xs_list *pending_list(snac *user)
1389/* returns a list of pending follow confirmations */
1390{
1391 xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1392 xs *l = xs_glob(spec, 0, 0);
1393 xs_list *r = xs_list_new();
1394 const char *v;
1395
1396 xs_list_foreach(l, v) {
1397 FILE *f;
1398 xs *msg = NULL;
1399
1400 if ((f = fopen(v, "r")) == NULL)
1401 continue;
1402
1403 msg = xs_json_load(f);
1404 fclose(f);
1405
1406 if (msg == NULL)
1407 continue;
1408
1409 const char *actor = xs_dict_get(msg, "actor");
1410
1411 if (xs_type(actor) == XSTYPE_STRING)
1412 r = xs_list_append(r, actor);
1413 }
1414
1415 return r;
1416}
1417
1418
1419int pending_count(snac *user)
1420/* returns the number of pending follow confirmations */
1421{
1422 xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1423 xs *l = xs_glob(spec, 0, 0);
1424
1425 return xs_list_len(l);
1426}
1427
1428
1429int is_msg_mine(snac *user, const char *id)
1430/* returns true if a post id is by the given user */
1431{
1432 int ret = 0;
1433
1434 if (xs_is_string(id)) {
1435 xs *s1 = xs_fmt("%s/", user->actor);
1436 ret = xs_startswith(id, s1);
1437 }
1438
1439 return ret;
1440}
1441
1442
1443/** timeline **/
1444
1445double timeline_mtime(snac *snac)
1446{
1447 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1448 return mtime(fn);
1449}
1450
1451
1452int timeline_touch(snac *snac)
1453/* changes the date of the timeline index */
1454{
1455 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1456 return utimes(fn, NULL);
1457}
1458
1459
1460xs_str *timeline_fn_by_md5(snac *snac, const char *md5)
1461/* get the filename of an entry by md5 from any timeline */
1462{
1463 xs_str *fn = NULL;
1464
1465 if (is_md5_hex(md5)) {
1466 fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
1467
1468 if (mtime(fn) == 0.0) {
1469 fn = xs_free(fn);
1470 fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
1471
1472 if (mtime(fn) == 0.0)
1473 fn = xs_free(fn);
1474 }
1475 }
1476
1477 return fn;
1478}
1479
1480
1481int timeline_here_by_md5(snac *snac, const char *md5)
1482/* checks if an object is in the user cache */
1483{
1484 xs *fn = timeline_fn_by_md5(snac, md5);
1485
1486 return !(fn == NULL);
1487}
1488
1489
1490int timeline_here(snac *user, const char *id)
1491{
1492 xs *md5 = xs_md5_hex(id, strlen(id));
1493
1494 return timeline_here_by_md5(user, md5);
1495}
1496
1497
1498int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
1499/* gets a message from the timeline */
1500{
1501 int status = HTTP_STATUS_NOT_FOUND;
1502 FILE *f = NULL;
1503
1504 xs *fn = timeline_fn_by_md5(snac, md5);
1505
1506 if (fn != NULL && (f = fopen(fn, "r")) != NULL) {
1507 *msg = xs_json_load(f);
1508 fclose(f);
1509
1510 if (*msg != NULL)
1511 status = HTTP_STATUS_OK;
1512 }
1513
1514 return status;
1515}
1516
1517
1518int timeline_del(snac *snac, const char *id)
1519/* deletes a message from the timeline */
1520{
1521 /* delete from the user's caches */
1522 object_user_cache_del(snac, id, "public");
1523 object_user_cache_del(snac, id, "private");
1524
1525 unpin(snac, id);
1526 unbookmark(snac, id);
1527
1528 /* try to delete the object if it's not used elsewhere */
1529 return object_del_if_unref(id);
1530}
1531
1532
1533void timeline_update_indexes(snac *snac, const char *id)
1534/* updates the indexes */
1535{
1536 object_user_cache_add(snac, id, "private");
1537
1538 if (is_msg_mine(snac, id)) {
1539 xs *msg = NULL;
1540
1541 if (valid_status(object_get(id, &msg))) {
1542 const int scope = get_msg_visibility(msg);
1543 /* if its ours and is public, also store in public */
1544 if (scope == SCOPE_PUBLIC) {
1545 if (object_user_cache_add(snac, id, "public") >= 0) {
1546 /* also add it to the instance public timeline */
1547 xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
1548 index_add(ipt, id);
1549 }
1550 else
1551 srv_debug(1, xs_fmt("Not added to public instance index %s, visibility %d", id, scope));
1552 }
1553 else
1554 /* also add it to public, it will be discarded later */
1555 object_user_cache_add(snac, id, "public");
1556 }
1557 }
1558}
1559
1560
1561int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
1562/* adds a message to the timeline */
1563{
1564 int ret = object_add(id, o_msg);
1565 timeline_update_indexes(snac, id);
1566
1567 tag_index(id, o_msg);
1568
1569 list_distribute(snac, NULL, o_msg);
1570
1571 snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
1572
1573 return ret;
1574}
1575
1576
1577int timeline_emoji_react(const char *act, const char *id, const xs_dict *msg_o)
1578/* adds an emoji reaction to a message */
1579{
1580 xs *msg = xs_dup(msg_o);
1581 msg = xs_dict_append(msg, "attributedTo", act);
1582 msg = xs_dict_set(msg, "type", "EmojiReact");
1583 const char *emote_id = xs_dict_get(msg, "id");
1584
1585 int ret = object_add(emote_id, msg);
1586 if (ret == HTTP_STATUS_OK || ret == HTTP_STATUS_CREATED)
1587 ret = object_emoji_react(id, emote_id);
1588
1589 return ret;
1590}
1591
1592
1593int timeline_admire(snac *snac, const char *id,
1594 const char *admirer, int like, const xs_dict *msg)
1595/* updates a timeline entry with a new admiration or emoji reaction */
1596{
1597 int ret;
1598 const char *content = xs_dict_get_path(msg, "content");
1599 const char *type = xs_dict_get_path(msg, "type");
1600
1601 /* if we are admiring this, add to both timelines */
1602 if (!like && strcmp(admirer, snac->actor) == 0) {
1603 object_user_cache_add(snac, id, "public");
1604 object_user_cache_add(snac, id, "private");
1605 }
1606
1607 /* use utf <3 as a like, as it is ugly */
1608 if (type && xs_match(type, "Like|EmojiReact|Emoji") &&
1609 content && strcmp(content, "❤") != 0) {
1610 ret = timeline_emoji_react(snac->actor, id, msg);
1611 snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id));
1612 }
1613
1614 else {
1615 ret = object_admire(id, admirer, like);
1616 snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
1617 like ? "Like" : "Announce", id, admirer));
1618 }
1619
1620 return ret;
1621}
1622
1623
1624xs_list *timeline_top_level(snac *snac, const xs_list *list)
1625/* returns the top level md5 entries from this index */
1626{
1627 xs_set seen;
1628 const xs_str *v;
1629
1630 xs_set_init(&seen);
1631
1632 int c = 0;
1633 while (xs_list_next(list, &v, &c)) {
1634 char line[MD5_HEX_SIZE] = "";
1635
1636 strncpy(line, v, sizeof(line));
1637
1638 for (;;) {
1639 char line2[MD5_HEX_SIZE];
1640
1641 /* if it doesn't have a parent, use this */
1642 if (!object_parent(line, line2))
1643 break;
1644
1645 /* well, there is a parent... but is it here? */
1646 if (!timeline_here_by_md5(snac, line2))
1647 break;
1648
1649 /* it's here! try again with its own parent */
1650 strncpy(line, line2, sizeof(line));
1651 }
1652
1653 xs_set_add(&seen, line);
1654 }
1655
1656 return xs_set_result(&seen);
1657}
1658
1659
1660xs_str *user_index_fn(snac *user, const char *idx_name)
1661/* returns the filename of a user index */
1662{
1663 return xs_fmt("%s/%s.idx", user->basedir, idx_name);
1664}
1665
1666
1667xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more)
1668/* returns a timeline (with all entries) */
1669{
1670 xs *idx = user_index_fn(user, idx_name);
1671
1672 /* if a more flag is sent, request one more */
1673 xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0));
1674
1675 if (more != NULL) {
1676 if (xs_list_len(lst) > show) {
1677 *more = 1;
1678 lst = xs_list_del(lst, -1);
1679 }
1680 else
1681 *more = 0;
1682 }
1683
1684 return lst;
1685}
1686
1687
1688xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more)
1689/* returns a timeline (only top level entries) */
1690{
1691 int c_max;
1692
1693 /* maximum number of items in the timeline */
1694 c_max = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
1695
1696 /* never more timeline entries than the configured maximum */
1697 if (show > c_max)
1698 show = c_max;
1699
1700 xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
1701
1702 return timeline_top_level(snac, list);
1703}
1704
1705
1706void timeline_add_mark(snac *user)
1707/* adds an "already seen" mark to the private timeline */
1708{
1709 xs *fn = xs_fmt("%s/private.idx", user->basedir);
1710 char last_entry[MD5_HEX_SIZE] = "";
1711 FILE *f;
1712
1713 /* get the last entry in the index */
1714 if ((f = fopen(fn, "r")) != NULL) {
1715 index_desc_first(f, last_entry, 0);
1716 fclose(f);
1717 }
1718
1719 /* is the last entry *not* a mark? */
1720 if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) {
1721 /* add it */
1722 index_add_md5(fn, MD5_ALREADY_SEEN_MARK);
1723 }
1724}
1725
1726
1727xs_str *instance_index_fn(void)
1728{
1729 return xs_fmt("%s/public.idx", srv_basedir);
1730}
1731
1732
1733xs_list *timeline_instance_list(int skip, int show)
1734/* returns the timeline for the full instance */
1735{
1736 xs *idx = instance_index_fn();
1737 xs *lst = index_list_desc(idx, skip, show);
1738
1739 /* make the list unique */
1740 xs_set rep;
1741 xs_set_init(&rep);
1742 const char *md5;
1743
1744 xs_list_foreach(lst, md5)
1745 xs_set_add(&rep, md5);
1746
1747 return xs_set_result(&rep);
1748}
1749
1750
1751/** following **/
1752
1753/* this needs special treatment and cannot use the object db as is,
1754 with a link to a cached author, because we need the Follow object
1755 in case we need to unfollow (Undo + original Follow) */
1756
1757xs_str *_following_fn(snac *snac, const char *actor)
1758{
1759 xs *md5 = xs_md5_hex(actor, strlen(actor));
1760 return xs_fmt("%s/following/%s.json", snac->basedir, md5);
1761}
1762
1763
1764int following_add(snac *snac, const char *actor, const xs_dict *msg)
1765/* adds to the following list */
1766{
1767 int ret = HTTP_STATUS_CREATED;
1768 xs *fn = _following_fn(snac, actor);
1769 FILE *f;
1770 xs *p_object = NULL;
1771
1772 if (valid_status(following_get(snac, actor, &p_object))) {
1773 /* object already exists; if it's of type Accept,
1774 the actor is already being followed and confirmed,
1775 so do nothing */
1776 const char *type = xs_dict_get(p_object, "type");
1777
1778 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1779 snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
1780 return HTTP_STATUS_OK;
1781 }
1782 }
1783
1784 if ((f = fopen(fn, "w")) != NULL) {
1785 xs_json_dump(msg, 4, f);
1786 fclose(f);
1787
1788 /* get the filename of the actor object */
1789 xs *actor_fn = _object_fn(actor);
1790
1791 /* increase its reference count */
1792 fn = xs_replace_i(fn, ".json", "_a.json");
1793 link(actor_fn, fn);
1794 }
1795 else
1796 ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
1797
1798 snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
1799
1800 return ret;
1801}
1802
1803
1804int following_del(snac *snac, const char *actor)
1805/* we're not following this actor any longer */
1806{
1807 xs *fn = _following_fn(snac, actor);
1808
1809 snac_debug(snac, 2, xs_fmt("following_del %s %s", actor, fn));
1810
1811 unlink(fn);
1812
1813 /* also delete the reference to the author */
1814 fn = xs_replace_i(fn, ".json", "_a.json");
1815 unlink(fn);
1816
1817 return HTTP_STATUS_OK;
1818}
1819
1820
1821int following_check(snac *snac, const char *actor)
1822/* checks if we are following this actor */
1823{
1824 xs *fn = _following_fn(snac, actor);
1825
1826 return !!(mtime(fn) != 0.0);
1827}
1828
1829
1830int following_get(snac *snac, const char *actor, xs_dict **data)
1831/* returns the 'Follow' object */
1832{
1833 xs *fn = _following_fn(snac, actor);
1834 FILE *f;
1835 int status = HTTP_STATUS_OK;
1836
1837 if ((f = fopen(fn, "r")) != NULL) {
1838 *data = xs_json_load(f);
1839 fclose(f);
1840 }
1841 else
1842 status = HTTP_STATUS_NOT_FOUND;
1843
1844 return status;
1845}
1846
1847
1848int following_list_len(snac *snac)
1849/* returns number of people being followed */
1850{
1851 xs *spec = xs_fmt("%s/following/" "*_a.json", snac->basedir);
1852 xs *glist = xs_glob(spec, 0, 0);
1853 return xs_list_len(glist);
1854}
1855
1856
1857xs_list *following_list(snac *snac)
1858/* returns the list of people being followed */
1859{
1860 xs *spec = xs_fmt("%s/following/" "*.json", snac->basedir);
1861 xs *glist = xs_glob(spec, 0, 0);
1862 xs_list *p;
1863 const xs_str *v;
1864 xs_list *list = xs_list_new();
1865
1866 /* iterate the list of files */
1867 p = glist;
1868 while (xs_list_iter(&p, &v)) {
1869 FILE *f;
1870
1871 /* load the follower data */
1872 if ((f = fopen(v, "r")) != NULL) {
1873 xs *o = xs_json_load(f);
1874 fclose(f);
1875
1876 if (o != NULL) {
1877 const char *type = xs_dict_get(o, "type");
1878
1879 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1880 const char *actor = xs_dict_get(o, "actor");
1881
1882 if (!xs_is_null(actor)) {
1883 list = xs_list_append(list, actor);
1884
1885 /* check if there is a link to the actor object */
1886 xs *v2 = xs_replace(v, ".json", "_a.json");
1887
1888 if (mtime(v2) == 0.0) {
1889 /* no; add a link to it */
1890 xs *actor_fn = _object_fn(actor);
1891 link(actor_fn, v2);
1892 }
1893 }
1894 }
1895 }
1896 }
1897 }
1898
1899 return list;
1900}
1901
1902
1903xs_str *_muted_fn(snac *snac, const char *actor)
1904{
1905 xs *md5 = xs_md5_hex(actor, strlen(actor));
1906 return xs_fmt("%s/muted/%s", snac->basedir, md5);
1907}
1908
1909
1910void mute(snac *snac, const char *actor)
1911/* mutes a moron */
1912{
1913 xs *fn = _muted_fn(snac, actor);
1914 FILE *f;
1915
1916 if ((f = fopen(fn, "w")) != NULL) {
1917 fprintf(f, "%s\n", actor);
1918 fclose(f);
1919
1920 snac_debug(snac, 2, xs_fmt("muted %s %s", actor, fn));
1921 }
1922}
1923
1924
1925void unmute(snac *snac, const char *actor)
1926/* actor is no longer a moron */
1927{
1928 xs *fn = _muted_fn(snac, actor);
1929
1930 unlink(fn);
1931
1932 snac_debug(snac, 2, xs_fmt("unmuted %s %s", actor, fn));
1933}
1934
1935
1936int is_muted(snac *snac, const char *actor)
1937/* check if someone is muted */
1938{
1939 xs *fn = _muted_fn(snac, actor);
1940
1941 return !!(mtime(fn) != 0.0);
1942}
1943
1944
1945xs_list *muted_list(snac *user)
1946/* returns the list (actor URLs) of the muted morons */
1947{
1948 xs_list *l = xs_list_new();
1949 xs *spec = xs_fmt("%s/muted/" "*", user->basedir);
1950 xs *files = xs_glob(spec, 0, 0);
1951 const char *fn;
1952
1953 xs_list_foreach(files, fn) {
1954 FILE *f;
1955
1956 if ((f = fopen(fn, "r")) != NULL) {
1957 xs *actor = xs_strip_i(xs_readline(f));
1958 fclose(f);
1959
1960 l = xs_list_append(l, actor);
1961 }
1962 }
1963
1964 return l;
1965}
1966
1967/** emojis react **/
1968
1969xs_str *emoji_reacted(snac *user, const char *id)
1970/* returns the emoji an user reacted to a message */
1971{
1972 xs *emojis = object_get_emoji_reacts(id);
1973 int c = 0;
1974 const char *v;
1975
1976 while (xs_list_next(emojis, &v, &c)) {
1977 xs *msg = NULL;
1978 if (object_get_by_md5(v, &msg)) {
1979 const xs_val *act = xs_dict_get(msg, "actor");
1980 if (act && strcmp(act, user->actor) == 0)
1981 return xs_dup(xs_dict_get(msg, "content"));
1982 }
1983 }
1984 return NULL;
1985}
1986
1987/** bookmarking **/
1988
1989int is_bookmarked(snac *user, const char *id)
1990/* returns true if this note is bookmarked */
1991{
1992 return object_user_cache_in(user, id, "bookmark");
1993}
1994
1995
1996int bookmark(snac *user, const char *id)
1997/* bookmarks a post */
1998{
1999 if (is_bookmarked(user, id))
2000 return -3;
2001
2002 return object_user_cache_add(user, id, "bookmark");
2003}
2004
2005
2006int unbookmark(snac *user, const char *id)
2007/* unbookmarks a post */
2008{
2009 return object_user_cache_del(user, id, "bookmark");
2010}
2011
2012
2013xs_list *bookmark_list(snac *user)
2014/* return the lists of bookmarked posts */
2015{
2016 return object_user_cache_list(user, "bookmark", XS_ALL, 1);
2017}
2018
2019
2020xs_str *bookmark_index_fn(snac *user)
2021{
2022 return object_user_cache_index_fn(user, "bookmark");
2023}
2024
2025
2026/** pinning **/
2027
2028int is_pinned(snac *user, const char *id)
2029/* returns true if this note is pinned */
2030{
2031 return object_user_cache_in(user, id, "pinned");
2032}
2033
2034
2035int is_pinned_by_md5(snac *user, const char *md5)
2036{
2037 return object_user_cache_in_by_md5(user, md5, "pinned");
2038}
2039
2040
2041int pin(snac *user, const char *id)
2042/* pins a message */
2043{
2044 int ret = -2;
2045
2046 if (is_msg_mine(user, id)) {
2047 if (is_pinned(user, id))
2048 ret = -3;
2049 else
2050 ret = object_user_cache_add(user, id, "pinned");
2051 }
2052
2053 return ret;
2054}
2055
2056
2057int unpin(snac *user, const char *id)
2058/* unpin a message */
2059{
2060 return object_user_cache_del(user, id, "pinned");
2061}
2062
2063
2064xs_list *pinned_list(snac *user)
2065/* return the lists of pinned posts */
2066{
2067 return object_user_cache_list(user, "pinned", XS_ALL, 1);
2068}
2069
2070
2071/** drafts **/
2072
2073int is_draft(snac *user, const char *id)
2074/* returns true if this note is a draft */
2075{
2076 return object_user_cache_in(user, id, "draft");
2077}
2078
2079
2080void draft_del(snac *user, const char *id)
2081/* delete a message from the draft cache */
2082{
2083 object_user_cache_del(user, id, "draft");
2084}
2085
2086
2087void draft_add(snac *user, const char *id, const xs_dict *msg)
2088/* store the message as a draft */
2089{
2090 /* delete from the index, in case it was already there */
2091 draft_del(user, id);
2092
2093 /* overwrite object */
2094 object_add_ow(id, msg);
2095
2096 /* [re]add to the index */
2097 object_user_cache_add(user, id, "draft");
2098}
2099
2100
2101xs_list *draft_list(snac *user)
2102/* return the lists of drafts */
2103{
2104 return object_user_cache_list(user, "draft", XS_ALL, 1);
2105}
2106
2107
2108/** scheduled posts **/
2109
2110int is_scheduled(snac *user, const char *id)
2111/* returns true if this note is scheduled for future sending */
2112{
2113 return object_user_cache_in(user, id, "sched");
2114}
2115
2116
2117void schedule_del(snac *user, const char *id)
2118/* deletes an scheduled post */
2119{
2120 object_user_cache_del(user, id, "sched");
2121}
2122
2123
2124void schedule_add(snac *user, const char *id, const xs_dict *msg)
2125/* schedules this post for later */
2126{
2127 /* delete from the index, in case it was already there */
2128 schedule_del(user, id);
2129
2130 /* overwrite object */
2131 object_add_ow(id, msg);
2132
2133 /* [re]add to the index */
2134 object_user_cache_add(user, id, "sched");
2135}
2136
2137
2138xs_list *scheduled_list(snac *user)
2139/* return the list of scheduled posts */
2140{
2141 return object_user_cache_list(user, "sched", XS_ALL, 1);
2142}
2143
2144
2145void scheduled_process(snac *user)
2146/* processes the scheduled list, sending those ready to be sent */
2147{
2148 xs *posts = scheduled_list(user);
2149 const char *md5;
2150 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
2151
2152 xs_list_foreach(posts, md5) {
2153 xs *msg = NULL;
2154
2155 if (valid_status(object_get_by_md5(md5, &msg))) {
2156 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
2157 /* due date! */
2158 const char *id = xs_dict_get(msg, "id");
2159
2160 timeline_add(user, id, msg);
2161
2162 xs *c_msg = msg_create(user, msg);
2163 enqueue_message(user, c_msg);
2164
2165 schedule_del(user, id);
2166 }
2167 }
2168 }
2169}
2170
2171
2172/** hiding **/
2173
2174xs_str *_hidden_fn(snac *snac, const char *id)
2175{
2176 xs *md5 = xs_md5_hex(id, strlen(id));
2177 return xs_fmt("%s/hidden/%s", snac->basedir, md5);
2178}
2179
2180
2181void hide(snac *snac, const char *id)
2182/* hides an object and its children (if it's a post) */
2183{
2184 xs *fn = _hidden_fn(snac, id);
2185 FILE *f;
2186
2187 if ((f = fopen(fn, "w")) != NULL) {
2188 fprintf(f, "%s\n", id);
2189 fclose(f);
2190
2191 snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
2192
2193 /* hide all the children */
2194 xs *chld = object_children(id);
2195 char *p;
2196 const char *v;
2197
2198 p = chld;
2199 while (xs_list_iter(&p, &v)) {
2200 xs *co = NULL;
2201
2202 /* resolve to get the id */
2203 if (valid_status(object_get_by_md5(v, &co))) {
2204 const char *id = xs_dict_get(co, "id");
2205 if (id != NULL)
2206 hide(snac, id);
2207 }
2208 }
2209 }
2210}
2211
2212
2213int is_hidden(snac *snac, const char *id)
2214/* check is id is hidden */
2215{
2216 xs *fn = _hidden_fn(snac, id);
2217
2218 return !!(mtime(fn) != 0.0);
2219}
2220
2221
2222int unhide(snac *user, const char *id)
2223/* unhides an object */
2224{
2225 xs *fn = _hidden_fn(user, id);
2226 return unlink(fn);
2227}
2228
2229
2230int actor_add(const char *actor, const xs_dict *msg)
2231/* adds an actor */
2232{
2233 return object_add_ow(actor, msg);
2234}
2235
2236
2237int actor_get(const char *actor, xs_dict **data)
2238/* returns an already downloaded actor */
2239{
2240 int status = HTTP_STATUS_OK;
2241 xs_dict *d = NULL;
2242
2243 if (xs_startswith(actor, srv_baseurl)) {
2244 /* it's a (possible) local user */
2245 xs *l = xs_split(actor, "/");
2246 const char *uid = xs_list_get(l, -1);
2247 snac user;
2248
2249 if (!xs_is_null(uid) && user_open(&user, uid)) {
2250 if (data)
2251 *data = msg_actor(&user);
2252
2253 user_free(&user);
2254 return HTTP_STATUS_OK;
2255 }
2256 else
2257 return HTTP_STATUS_NOT_FOUND;
2258 }
2259
2260 /* read the object */
2261 if (!valid_status(status = object_get(actor, &d))) {
2262 d = xs_free(d);
2263 return status;
2264 }
2265
2266 /* if the object is corrupted, discard it */
2267 if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
2268 srv_debug(1, xs_fmt("corrupted actor object %s", actor));
2269 d = xs_free(d);
2270 return HTTP_STATUS_NOT_FOUND;
2271 }
2272
2273 if (data)
2274 *data = d;
2275 else
2276 d = xs_free(d);
2277
2278 xs *fn = _object_fn(actor);
2279 double max_time;
2280
2281 /* maximum time for the actor data to be considered stale */
2282 max_time = 3600.0 * 36.0;
2283
2284 if (mtime(fn) + max_time < (double) time(NULL)) {
2285 /* actor data exists but also stinks */
2286 status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
2287 }
2288
2289 return status;
2290}
2291
2292
2293int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
2294/* gets an actor and requests a refresh if it's stale */
2295{
2296 int status = actor_get(actor, data);
2297
2298 if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
2299 enqueue_actor_refresh(user, actor, 0);
2300
2301 return status;
2302}
2303
2304
2305/** user limiting (announce blocks) **/
2306
2307int limited(snac *user, const char *id, int cmd)
2308/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
2309{
2310 int ret = 0;
2311 xs *dir = xs_fmt("%s/limited", user->basedir);
2312 xs *md5 = xs_md5_hex(id, strlen(id));
2313 xs *fn = xs_fmt("%s/%s", dir, md5);
2314
2315 switch (cmd) {
2316 case 0: /** check **/
2317 ret = !!(mtime(fn) > 0.0);
2318 break;
2319
2320 case 1: /** limit **/
2321 mkdirx(dir);
2322
2323 if (mtime(fn) > 0.0)
2324 ret = -1;
2325 else {
2326 FILE *f;
2327
2328 if ((f = fopen(fn, "w")) != NULL) {
2329 fprintf(f, "%s\n", id);
2330 fclose(f);
2331 }
2332 else
2333 ret = -2;
2334 }
2335 break;
2336
2337 case 2: /** unlimit **/
2338 if (mtime(fn) > 0.0)
2339 ret = unlink(fn);
2340 else
2341 ret = -1;
2342 break;
2343 }
2344
2345 return ret;
2346}
2347
2348
2349/** tag indexing **/
2350
2351void tag_index(const char *id, const xs_dict *obj)
2352/* update the tag indexes for this object */
2353{
2354 const xs_list *tags = xs_dict_get(obj, "tag");
2355 xs *md5_id = xs_md5_hex(id, strlen(id));
2356
2357 if (get_msg_visibility(obj) != SCOPE_PUBLIC)
2358 return;
2359
2360 if (xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
2361 xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
2362
2363 mkdirx(g_tag_dir);
2364
2365 const xs_dict *v;
2366 int ct = 0;
2367 while (xs_list_next(tags, &v, &ct)) {
2368 const char *type = xs_dict_get(v, "type");
2369 const char *name = xs_dict_get(v, "name");
2370
2371 if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
2372 while (*name == '#' || *name == '@')
2373 name++;
2374
2375 if (*name == '\0')
2376 continue;
2377
2378 name = xs_utf8_to_lower((xs_str *)name);
2379
2380 xs *md5_tag = xs_md5_hex(name, strlen(name));
2381 xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
2382 mkdirx(tag_dir);
2383
2384 xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
2385
2386 if (!index_in_md5(g_tag_idx, md5_id))
2387 index_add_md5(g_tag_idx, md5_id);
2388
2389 FILE *f;
2390 xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
2391 if ((f = fopen(g_tag_name, "w")) != NULL) {
2392 fprintf(f, "%s\n", name);
2393 fclose(f);
2394 }
2395
2396 srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
2397 }
2398 }
2399 }
2400}
2401
2402
2403xs_str *tag_fn(const char *tag)
2404{
2405 if (*tag == '#')
2406 tag++;
2407
2408 xs *lw_tag = xs_utf8_to_lower(tag);
2409 xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag));
2410
2411 return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
2412}
2413
2414
2415xs_list *tag_search(const char *tag, int skip, int show)
2416/* returns the list of posts tagged with tag */
2417{
2418 xs *idx = tag_fn(tag);
2419
2420 return index_list_desc(idx, skip, show);
2421}
2422
2423
2424/** lists **/
2425
2426xs_val *list_maint(snac *user, const char *list, int op)
2427/* list maintenance */
2428{
2429 xs_val *l = NULL;
2430
2431 switch (op) {
2432 case 0: /** list of lists **/
2433 {
2434 FILE *f;
2435 xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
2436 xs *ls = xs_glob(spec, 0, 0);
2437 int c = 0;
2438 const char *v;
2439
2440 l = xs_list_new();
2441
2442 while (xs_list_next(ls, &v, &c)) {
2443 if ((f = fopen(v, "r")) != NULL) {
2444 xs *title = xs_readline(f);
2445 fclose(f);
2446
2447 title = xs_strip_i(title);
2448
2449 xs *v2 = xs_replace(v, ".id", "");
2450 xs *l2 = xs_split(v2, "/");
2451
2452 /* return [ list_id, list_title ] */
2453 xs *tmp_list = xs_list_append(xs_list_new(), xs_list_get(l2, -1), title);
2454 l = xs_list_append(l, tmp_list);
2455 }
2456 }
2457 }
2458
2459 break;
2460
2461 case 1: /** create new list (list is the name) **/
2462 {
2463 xs *lol = list_maint(user, NULL, 0);
2464 int c = 0;
2465 const xs_list *v;
2466 int add = 1;
2467
2468 /* check if this list name already exists */
2469 while (xs_list_next(lol, &v, &c)) {
2470 if (strcmp(xs_list_get(v, 1), list) == 0) {
2471 add = 0;
2472
2473 l = xs_dup(xs_list_get(v, 0));
2474
2475 break;
2476 }
2477 }
2478
2479 if (add) {
2480 FILE *f;
2481 xs *dir = xs_fmt("%s/list/", user->basedir);
2482
2483 l = xs_fmt("%010x", time(NULL));
2484
2485 mkdirx(dir);
2486
2487 xs *fn = xs_fmt("%s%s.id", dir, l);
2488
2489 if ((f = fopen(fn, "w")) != NULL) {
2490 fprintf(f, "%s\n", list);
2491 fclose(f);
2492 }
2493 }
2494 }
2495
2496 break;
2497
2498 case 2: /** delete list (list is the id) **/
2499 {
2500 if (xs_is_hex(list)) {
2501 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2502 unlink(fn);
2503
2504 fn = xs_replace_i(fn, ".id", ".lst");
2505 unlink(fn);
2506
2507 fn = xs_replace_i(fn, ".lst", ".idx");
2508 unlink(fn);
2509
2510 fn = xs_str_cat(fn, ".bak");
2511 unlink(fn);
2512 }
2513 }
2514
2515 break;
2516
2517 case 3: /** get list name **/
2518 if (xs_is_hex(list)) {
2519 FILE *f;
2520 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2521
2522 if ((f = fopen(fn, "r")) != NULL) {
2523 l = xs_strip_i(xs_readline(f));
2524 fclose(f);
2525 }
2526 }
2527
2528 break;
2529
2530 case 4: /** find list id by name **/
2531 if (xs_is_string(list)) {
2532 xs *lol = list_maint(user, NULL, 0);
2533 const xs_list *li;
2534
2535 xs_list_foreach(lol, li) {
2536 if (strcmp(list, xs_list_get(li, 1)) == 0) {
2537 l = xs_dup(xs_list_get(li, 0));
2538 break;
2539 }
2540 }
2541 }
2542 }
2543
2544 return l;
2545}
2546
2547
2548xs_str *list_timeline_fn(snac *user, const char *list)
2549{
2550 return xs_fmt("%s/list/%s.idx", user->basedir, list);
2551}
2552
2553
2554xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2555/* returns the timeline of a list */
2556{
2557 xs_list *l = NULL;
2558
2559 if (!xs_is_hex(list))
2560 return NULL;
2561
2562 xs *fn = list_timeline_fn(user, list);
2563
2564 if (mtime(fn) > 0.0)
2565 l = index_list_desc(fn, skip, show);
2566 else
2567 l = xs_list_new();
2568
2569 return l;
2570}
2571
2572
2573xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
2574/* list member management */
2575{
2576 xs_val *l = NULL;
2577
2578 if (!xs_is_hex(list))
2579 return NULL;
2580
2581 if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2582 return NULL;
2583
2584 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2585
2586 switch (op) {
2587 case 0: /** list members **/
2588 l = index_list(fn, XS_ALL);
2589
2590 break;
2591
2592 case 1: /** append actor to list **/
2593 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) {
2594 if (!index_in_md5(fn, actor_md5))
2595 index_add_md5(fn, actor_md5);
2596 }
2597
2598 break;
2599
2600 case 2: /** delete actor from list **/
2601 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5))
2602 index_del_md5(fn, actor_md5);
2603
2604 break;
2605
2606 default:
2607 srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2608 break;
2609 }
2610
2611 return l;
2612}
2613
2614
2615void list_distribute(snac *user, const char *who, const xs_dict *post)
2616/* distributes the post to all appropriate lists */
2617{
2618 const char *id = xs_dict_get(post, "id");
2619
2620 /* if who is not set, use the attributedTo in the message */
2621 if (xs_is_null(who))
2622 who = get_atto(post);
2623
2624 if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2625 xs *a_md5 = xs_md5_hex(who, strlen(who));
2626 xs *i_md5 = xs_md5_hex(id, strlen(id));
2627 xs *spec = xs_fmt("%s/list/" "*.lst", user->basedir);
2628 xs *ls = xs_glob(spec, 0, 0);
2629 int c = 0;
2630 const char *v;
2631
2632 while (xs_list_next(ls, &v, &c)) {
2633 /* is the actor in this list? */
2634 if (index_in_md5(v, a_md5)) {
2635 /* it is; add post md5 to its timeline */
2636 xs *idx = xs_replace(v, ".lst", ".idx");
2637 index_add_md5(idx, i_md5);
2638
2639 snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2640 }
2641 }
2642 }
2643}
2644
2645
2646/** static data **/
2647
2648static int _load_raw_file(const char *fn, xs_val **data, int *size,
2649 const char *inm, xs_str **etag)
2650/* loads a cached file */
2651{
2652 int status = HTTP_STATUS_NOT_FOUND;
2653
2654 if (fn) {
2655 double tm = mtime(fn);
2656
2657 if (tm > 0.0) {
2658 /* file exists; build the etag */
2659 xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2660
2661 /* if if-none-match is set, check if it's the same */
2662 if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2663 /* client has the newest version */
2664 status = HTTP_STATUS_NOT_MODIFIED;
2665 }
2666 else {
2667 /* newer or never downloaded; read the full file */
2668 FILE *f;
2669
2670 if ((f = fopen(fn, "rb")) != NULL) {
2671 *size = XS_ALL;
2672 *data = xs_read(f, size);
2673 fclose(f);
2674
2675 status = HTTP_STATUS_OK;
2676 }
2677 }
2678
2679 /* if caller wants the etag, return it */
2680 if (etag != NULL)
2681 *etag = xs_dup(e);
2682
2683 srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2684 }
2685 }
2686
2687 return status;
2688}
2689
2690
2691xs_str *_static_fn(snac *snac, const char *id)
2692/* gets the filename for a static file */
2693{
2694 if (strchr(id, '/'))
2695 return NULL;
2696 else
2697 return xs_fmt("%s/static/%s", snac->basedir, id);
2698}
2699
2700
2701int static_get(snac *snac, const char *id, xs_val **data, int *size,
2702 const char *inm, xs_str **etag)
2703/* returns static content */
2704{
2705 xs *fn = _static_fn(snac, id);
2706
2707 return _load_raw_file(fn, data, size, inm, etag);
2708}
2709
2710
2711void static_put(snac *snac, const char *id, const char *data, int size)
2712/* writes status content */
2713{
2714 xs *fn = _static_fn(snac, id);
2715 FILE *f;
2716
2717 if (fn && (f = fopen(fn, "wb")) != NULL) {
2718 fwrite(data, size, 1, f);
2719 fclose(f);
2720
2721 strip_media(fn);
2722 }
2723}
2724
2725
2726void static_put_meta(snac *snac, const char *id, const char *str)
2727/* puts metadata (i.e. a media description string) to id */
2728{
2729 xs *fn = _static_fn(snac, id);
2730
2731 if (fn) {
2732 fn = xs_str_cat(fn, ".txt");
2733 FILE *f;
2734
2735 if ((f = fopen(fn, "w")) != NULL) {
2736 fprintf(f, "%s\n", str);
2737 fclose(f);
2738 }
2739 }
2740}
2741
2742
2743xs_str *static_get_meta(snac *snac, const char *id)
2744/* gets metadata from a media */
2745{
2746 xs *fn = _static_fn(snac, id);
2747 xs_str *r = NULL;
2748
2749 if (fn) {
2750 fn = xs_str_cat(fn, ".txt");
2751 FILE *f;
2752
2753 if ((f = fopen(fn, "r")) != NULL) {
2754 r = xs_strip_i(xs_readline(f));
2755 fclose(f);
2756 }
2757 }
2758 else
2759 r = xs_str_new("");
2760
2761 return r;
2762}
2763
2764
2765/** history **/
2766
2767xs_str *_history_fn(snac *snac, const char *id)
2768/* gets the filename for the history */
2769{
2770 if (strchr(id, '/'))
2771 return NULL;
2772 else
2773 return xs_fmt("%s/history/%s", snac->basedir, id);
2774}
2775
2776
2777double history_mtime(snac *snac, const char *id)
2778{
2779 double t = 0.0;
2780 xs *fn = _history_fn(snac, id);
2781
2782 if (fn != NULL)
2783 t = mtime(fn);
2784
2785 return t;
2786}
2787
2788
2789void history_add(snac *snac, const char *id, const char *content, int size,
2790 xs_str **etag)
2791/* adds something to the history */
2792{
2793 xs *fn = _history_fn(snac, id);
2794 FILE *f;
2795
2796 if (fn && (f = fopen(fn, "w")) != NULL) {
2797 fwrite(content, size, 1, f);
2798 fclose(f);
2799
2800 if (etag) {
2801 double tm = mtime(fn);
2802 *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2803 }
2804 }
2805}
2806
2807
2808int history_get(snac *snac, const char *id, xs_str **content, int *size,
2809 const char *inm, xs_str **etag)
2810{
2811 xs *fn = _history_fn(snac, id);
2812
2813 return _load_raw_file(fn, content, size, inm, etag);
2814}
2815
2816
2817int history_del(snac *snac, const char *id)
2818{
2819 xs *fn = _history_fn(snac, id);
2820
2821 if (fn)
2822 return unlink(fn);
2823 else
2824 return -1;
2825}
2826
2827
2828xs_list *history_list(snac *snac)
2829{
2830 xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2831
2832 return xs_glob(spec, 1, 1);
2833}
2834
2835
2836void lastlog_write(snac *snac, const char *source)
2837/* writes the last time the user logged in */
2838{
2839 xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2840 FILE *f;
2841
2842 if ((f = fopen(fn, "w")) != NULL) {
2843 fprintf(f, "%lf %s\n", ftime(), source);
2844 fclose(f);
2845 }
2846}
2847
2848
2849/** inbox collection **/
2850
2851void inbox_add(const char *inbox)
2852/* collects a shared inbox */
2853{
2854 /* don't collect ourselves */
2855 if (xs_startswith(inbox, srv_baseurl))
2856 return;
2857
2858 xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2859 xs *fn = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2860 FILE *f;
2861
2862 if ((f = fopen(fn, "w")) != NULL) {
2863 fprintf(f, "%s\n", inbox);
2864 fclose(f);
2865 }
2866}
2867
2868
2869void inbox_add_by_actor(const xs_dict *actor)
2870/* collects an actor's shared inbox, if it has one */
2871{
2872 const char *v;
2873
2874 if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2875 !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2876 /* only collect this inbox if its instance is not blocked */
2877 if (!is_instance_blocked(v))
2878 inbox_add(v);
2879 }
2880}
2881
2882
2883xs_list *inbox_list(void)
2884/* returns the collected inboxes as a list */
2885{
2886 xs_list *ibl = xs_list_new();
2887 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2888 xs *files = xs_glob(spec, 0, 0);
2889 const xs_val *v;
2890
2891 xs_list_foreach(files, v) {
2892 FILE *f;
2893
2894 if ((f = fopen(v, "r")) != NULL) {
2895 xs *line = xs_readline(f);
2896
2897 if (line && *line) {
2898 line = xs_strip_i(line);
2899
2900 if (!is_instance_blocked(line))
2901 ibl = xs_list_append(ibl, line);
2902 }
2903
2904 fclose(f);
2905 }
2906 }
2907
2908 return ibl;
2909}
2910
2911
2912/** instance-wide operations **/
2913
2914xs_str *_instance_block_fn(const char *instance)
2915{
2916 xs *s = xs_replace(instance, "http:/" "/", "");
2917 xs *s1 = xs_replace(s, "https:/" "/", "");
2918 xs *l = xs_split(s1, "/");
2919 const char *p = xs_list_get(l, 0);
2920 xs *md5 = xs_md5_hex(p, strlen(p));
2921
2922 return xs_fmt("%s/block/%s", srv_basedir, md5);
2923}
2924
2925
2926int is_instance_blocked(const char *instance)
2927{
2928 xs *fn = _instance_block_fn(instance);
2929
2930 return !!(mtime(fn) != 0.0);
2931}
2932
2933
2934int instance_block(const char *instance)
2935/* blocks a full instance */
2936{
2937 int ret;
2938
2939 /* create the subdir */
2940 xs *dir = xs_fmt("%s/block/", srv_basedir);
2941 mkdirx(dir);
2942
2943 if (!is_instance_blocked(instance)) {
2944 xs *fn = _instance_block_fn(instance);
2945 FILE *f;
2946
2947 if ((f = fopen(fn, "w")) != NULL) {
2948 fprintf(f, "%s\n", instance);
2949 fclose(f);
2950
2951 ret = 0;
2952 }
2953 else
2954 ret = -1;
2955 }
2956 else
2957 ret = -2;
2958
2959 return ret;
2960}
2961
2962
2963int instance_unblock(const char *instance)
2964/* unblocks a full instance */
2965{
2966 int ret;
2967
2968 if (is_instance_blocked(instance)) {
2969 xs *fn = _instance_block_fn(instance);
2970 ret = unlink(fn);
2971 }
2972 else
2973 ret = -2;
2974
2975 return ret;
2976}
2977
2978
2979/** operations by content **/
2980
2981int content_match(const char *file, const xs_dict *msg)
2982/* checks if a message's content matches any of the regexes in file */
2983/* file format: one regex per line */
2984{
2985 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2986 FILE *f;
2987 int r = 0;
2988 const char *v = xs_dict_get(msg, "content");
2989
2990 if (xs_type(v) == XSTYPE_STRING && *v) {
2991 if ((f = fopen(fn, "r")) != NULL) {
2992 srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2993
2994 /* massage content (strip HTML tags, etc.) */
2995 xs *c1 = xs_regex_replace(v, "<[^>]+>", " ");
2996 c1 = xs_regex_replace_i(c1, " {2,}", " ");
2997 xs *c = xs_utf8_to_lower(c1);
2998
2999 while (!r && !feof(f)) {
3000 xs *rx = xs_strip_i(xs_readline(f));
3001
3002 if (*rx && xs_regex_match(c, rx)) {
3003 srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
3004 r = 1;
3005 }
3006 }
3007
3008 fclose(f);
3009 }
3010 }
3011
3012 return r;
3013}
3014
3015
3016xs_list *content_search(snac *user, const char *regex,
3017 int priv, int skip, int show, int max_secs, int *timeout)
3018/* returns a list of posts which content matches the regex */
3019{
3020 if (regex == NULL || *regex == '\0')
3021 return xs_list_new();
3022
3023 xs *i_regex = xs_utf8_to_lower(regex);
3024
3025 xs_set seen;
3026
3027 xs_set_init(&seen);
3028
3029 if (max_secs == 0)
3030 max_secs = 3;
3031
3032 time_t t = time(NULL) + max_secs;
3033 *timeout = 0;
3034
3035 /* iterate all timelines simultaneously */
3036 xs_list *tls[3] = {0};
3037 const char *md5s[3] = {0};
3038 int c[3] = {0};
3039
3040 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */
3041 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
3042 tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
3043
3044 /* first positioning */
3045 for (int n = 0; n < 3; n++)
3046 xs_list_next(tls[n], &md5s[n], &c[n]);
3047
3048 show += skip;
3049
3050 while (show > 0) {
3051 /* timeout? */
3052 if (time(NULL) > t) {
3053 *timeout = 1;
3054 break;
3055 }
3056
3057 /* find the newest post */
3058 int newest = -1;
3059 double mtime = 0.0;
3060
3061 for (int n = 0; n < 3; n++) {
3062 if (md5s[n] != NULL) {
3063 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
3064 double mt;
3065
3066 while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
3067 /* object is not here: move to the next one */
3068 if (xs_list_next(tls[n], &md5s[n], &c[n])) {
3069 xs_free(fn);
3070 fn = _object_fn_by_md5(md5s[n], "content_search_2");
3071 }
3072 else
3073 md5s[n] = NULL;
3074 }
3075
3076 if (mt > mtime) {
3077 newest = n;
3078 mtime = mt;
3079 }
3080 }
3081 }
3082
3083 if (newest == -1)
3084 break;
3085
3086 const char *md5 = md5s[newest];
3087
3088 /* advance the chosen timeline */
3089 if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
3090 md5s[newest] = NULL;
3091
3092 xs *post = NULL;
3093
3094 if (!valid_status(object_get_by_md5(md5, &post)))
3095 continue;
3096
3097 if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
3098 continue;
3099
3100 const char *id = xs_dict_get(post, "id");
3101
3102 if (id == NULL || is_hidden(user, id))
3103 continue;
3104
3105 /* recalculate the md5 id to be sure it's not repeated
3106 (it may have been searched by the "url" field instead of "id") */
3107 xs *new_md5 = xs_md5_hex(id, strlen(id));
3108 md5 = new_md5;
3109
3110 /* test for the post URL */
3111 if (strcmp(id, regex) == 0) {
3112 if (xs_set_add(&seen, md5) == 1)
3113 show--;
3114
3115 continue;
3116 }
3117
3118 /* test for the alternate post id */
3119 const char *url = xs_dict_get(post, "url");
3120 if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
3121 if (xs_set_add(&seen, md5) == 1)
3122 show--;
3123
3124 continue;
3125 }
3126
3127 xs *c = xs_str_new(NULL);
3128 const char *content = xs_dict_get(post, "content");
3129 const char *name = xs_dict_get(post, "name");
3130 const char *atto = get_atto(post);
3131
3132 if (!xs_is_null(content))
3133 c = xs_str_cat(c, content);
3134 if (!xs_is_null(name))
3135 c = xs_str_cat(c, " ", name);
3136 if (!xs_is_null(atto))
3137 c = xs_str_cat(c, " ", atto);
3138
3139 /* add alt-texts from attachments */
3140 const xs_list *atts = xs_dict_get(post, "attachment");
3141 int tc = 0;
3142 const xs_dict *att;
3143
3144 while (xs_list_next(atts, &att, &tc)) {
3145 const char *name = xs_dict_get(att, "name");
3146
3147 if (name != NULL)
3148 c = xs_str_cat(c, " ", name);
3149 }
3150
3151 /* strip HTML */
3152 c = xs_regex_replace_i(c, "<[^>]+>", " ");
3153 c = xs_regex_replace_i(c, " {2,}", " ");
3154
3155 /* convert to lowercase */
3156 xs *lc = xs_utf8_to_lower(c);
3157
3158 /* apply regex */
3159 if (xs_regex_match(lc, i_regex)) {
3160 if (xs_set_add(&seen, md5) == 1)
3161 show--;
3162 }
3163 }
3164
3165 xs_list *r = xs_set_result(&seen);
3166
3167 if (skip) {
3168 /* BAD */
3169 while (skip--) {
3170 r = xs_list_del(r, 0);
3171 }
3172 }
3173
3174 xs_free(tls[0]);
3175 xs_free(tls[1]);
3176 xs_free(tls[2]);
3177
3178 return r;
3179}
3180
3181
3182int actor_failure(const char *actor, int op)
3183/* actor failure maintenance */
3184{
3185 int ret = 0;
3186
3187 xs *md5 = xs_md5_hex(actor, strlen(actor));
3188 xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3189
3190 switch (op) {
3191 case 0: /** check **/
3192 if (mtime(fn))
3193 ret = -1;
3194
3195 break;
3196
3197 case 1: /** register a failure **/
3198 if (mtime(fn) == 0.0) {
3199 FILE *f;
3200
3201 /* only create once, as the date will be used */
3202 if ((f = fopen(fn, "w")) != NULL) {
3203 fprintf(f, "%s\n", actor);
3204 fclose(f);
3205 }
3206 }
3207
3208 break;
3209
3210 case 2: /** clear a failure **/
3211 /* called whenever a message comes from this instance */
3212 unlink(fn);
3213
3214 break;
3215 }
3216
3217 return ret;
3218}
3219
3220
3221int instance_failure(const char *url, int op)
3222/* do some checks and accounting on instance failures */
3223{
3224 int ret = 0;
3225 xs *l = xs_split(url, "/");
3226 const char *hostname = xs_list_get(l, 2);
3227 double mt;
3228
3229 if (!xs_is_string(hostname))
3230 return 0;
3231
3232 xs *md5 = xs_md5_hex(hostname, strlen(hostname));
3233 xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3234
3235 switch (op) {
3236 case 0: /** check **/
3237 if ((mt = mtime(fn)) != 0.0) {
3238 /* grace time */
3239 double seconds_failing = xs_number_get(xs_dict_get_def(srv_config, "max_failing_days", "15"))
3240 * (24 * 60 * 60);
3241
3242 if ((double)time(NULL) - mt > seconds_failing)
3243 ret = -1;
3244 }
3245
3246 break;
3247
3248 case 1: /** register a failure **/
3249 if (mtime(fn) == 0.0) {
3250 FILE *f;
3251
3252 /* only create once, as the date will be used */
3253 if ((f = fopen(fn, "w")) != NULL) {
3254 fprintf(f, "%s\n", hostname);
3255 fclose(f);
3256 }
3257 }
3258
3259 break;
3260
3261 case 2: /** clear a failure **/
3262 /* called whenever a message comes from this instance */
3263 unlink(fn);
3264
3265 break;
3266 }
3267
3268 return ret;
3269}
3270
3271
3272int grave(const char *objid, int op)
3273/* the graveyeard of deleted objects */
3274{
3275 int ret = 0;
3276 xs *dir = xs_fmt("%s/grave", srv_basedir);
3277 xs *md5 = xs_md5_hex(objid, strlen(objid));
3278 xs *fn = xs_fmt("%s/%s", dir, md5);
3279 FILE *f;
3280
3281 switch (op) {
3282 case 0: /** check **/
3283 ret = mtime(fn) > 0.0 ? 1 : 0;
3284 break;
3285
3286 case 1: /** add **/
3287 mkdirx(dir);
3288
3289 if ((f = fopen(fn, "w")) != NULL) {
3290 fprintf(f, "%s\n", objid);
3291 fclose(f);
3292 }
3293
3294 break;
3295 }
3296
3297 return ret;
3298}
3299
3300
3301/** notifications **/
3302
3303xs_str *notify_check_time(snac *snac, int reset)
3304/* gets or resets the latest notification check time */
3305{
3306 xs_str *t = NULL;
3307 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
3308 FILE *f;
3309
3310 if (reset) {
3311 if ((f = fopen(fn, "w")) != NULL) {
3312 t = tid(0);
3313 fprintf(f, "%s\n", t);
3314 fclose(f);
3315 }
3316 }
3317 else {
3318 if ((f = fopen(fn, "r")) != NULL) {
3319 t = xs_readline(f);
3320 fclose(f);
3321 }
3322 else
3323 /* never set before */
3324 t = xs_fmt("%16.6f", 0.0);
3325 }
3326
3327 return t;
3328}
3329
3330xs_dict *markers_get(snac *snac, const xs_list *markers)
3331{
3332 xs *data = NULL;
3333 xs_dict *returns = xs_dict_new();
3334 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3335 const xs_str *v = NULL;
3336 FILE *f;
3337
3338 if ((f = fopen(fn, "r")) != NULL) {
3339 data = xs_json_load(f);
3340 fclose(f);
3341 }
3342
3343 if (xs_is_null(data))
3344 data = xs_dict_new();
3345
3346 xs_list_foreach(markers, v) {
3347 const xs_dict *mark = xs_dict_get(data, v);
3348 if (!xs_is_null(mark)) {
3349 returns = xs_dict_append(returns, v, mark);
3350 }
3351 }
3352 return returns;
3353}
3354
3355xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
3356/* gets or sets notification marker */
3357{
3358 xs *data = NULL;
3359 xs_dict *written = xs_dict_new();
3360 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3361 FILE *f;
3362
3363 if ((f = fopen(fn, "r")) != NULL) {
3364 data = xs_json_load(f);
3365 fclose(f);
3366 }
3367
3368 if (xs_is_null(data))
3369 data = xs_dict_new();
3370
3371 if (!xs_is_null(home_marker)) {
3372 xs *home = xs_dict_new();
3373 xs *s_tid = tid(0);
3374 home = xs_dict_append(home, "last_read_id", home_marker);
3375 home = xs_dict_append(home, "version", xs_stock(0));
3376 home = xs_dict_append(home, "updated_at", s_tid);
3377 data = xs_dict_set(data, "home", home);
3378 written = xs_dict_append(written, "home", home);
3379 }
3380
3381 if (!xs_is_null(notify_marker)) {
3382 xs *notify = xs_dict_new();
3383 xs *s_tid = tid(0);
3384 notify = xs_dict_append(notify, "last_read_id", notify_marker);
3385 notify = xs_dict_append(notify, "version", xs_stock(0));
3386 notify = xs_dict_append(notify, "updated_at", s_tid);
3387 data = xs_dict_set(data, "notifications", notify);
3388 written = xs_dict_append(written, "notifications", notify);
3389 }
3390
3391 if ((f = fopen(fn, "w")) != NULL) {
3392 xs_json_dump(data, 4, f);
3393 fclose(f);
3394 }
3395
3396 return written;
3397}
3398
3399void notify_add(snac *snac, const char *type, const char *utype,
3400 const char *actor, const char *objid, const xs_dict *msg)
3401/* adds a new notification */
3402{
3403 xs *ntid = tid(0);
3404 xs *fn = xs_fmt("%s/notify/", snac->basedir);
3405 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3406 FILE *f;
3407
3408 /* create the directory */
3409 mkdirx(fn);
3410 fn = xs_str_cat(fn, ntid);
3411 fn = xs_str_cat(fn, ".json");
3412
3413 xs *noti = xs_dict_new();
3414
3415 noti = xs_dict_append(noti, "id", ntid);
3416 noti = xs_dict_append(noti, "type", type);
3417 noti = xs_dict_append(noti, "utype", utype);
3418 noti = xs_dict_append(noti, "actor", actor);
3419 noti = xs_dict_append(noti, "date", date);
3420 noti = xs_dict_append(noti, "msg", msg);
3421
3422 if (!xs_is_null(objid))
3423 noti = xs_dict_append(noti, "objid", objid);
3424
3425 if ((f = fopen(fn, "w")) != NULL) {
3426 xs_json_dump(noti, 4, f);
3427 fclose(f);
3428 }
3429
3430 /* add it to the index if it already exists */
3431 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3432
3433 if (mtime(idx) != 0.0) {
3434 pthread_mutex_lock(&data_mutex);
3435
3436 if ((f = fopen(idx, "a")) != NULL) {
3437 fprintf(f, "%-32s\n", ntid);
3438 fclose(f);
3439 }
3440
3441 pthread_mutex_unlock(&data_mutex);
3442 }
3443
3444 if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook")))
3445 enqueue_notify_webhook(snac, noti, 0);
3446}
3447
3448
3449xs_dict *notify_get(snac *snac, const char *id)
3450/* gets a notification */
3451{
3452 /* base file */
3453 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
3454
3455 /* strip spaces and add extension */
3456 fn = xs_strip_i(fn);
3457 fn = xs_str_cat(fn, ".json");
3458
3459 FILE *f;
3460 xs_dict *out = NULL;
3461
3462 if ((f = fopen(fn, "r")) != NULL) {
3463 out = xs_json_load(f);
3464 fclose(f);
3465 }
3466
3467 return out;
3468}
3469
3470
3471xs_list *notify_list(snac *snac, int skip, int show)
3472/* returns a list of notification ids */
3473{
3474 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3475
3476 if (mtime(idx) == 0.0) {
3477 /* create the index from scratch */
3478 FILE *f;
3479
3480 pthread_mutex_lock(&data_mutex);
3481
3482 if ((f = fopen(idx, "w")) != NULL) {
3483 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
3484 xs *lst = xs_glob(spec, 1, 0);
3485 xs_list *p = lst;
3486 const char *v;
3487
3488 while (xs_list_iter(&p, &v)) {
3489 char *p = strrchr(v, '.');
3490 if (p) {
3491 *p = '\0';
3492 fprintf(f, "%-32s\n", v);
3493 }
3494 }
3495
3496 fclose(f);
3497 }
3498
3499 pthread_mutex_unlock(&data_mutex);
3500 }
3501
3502 return index_list_desc(idx, skip, show);
3503}
3504
3505
3506int notify_new_num(snac *snac)
3507/* counts the number of new notifications */
3508{
3509 xs *t = notify_check_time(snac, 0);
3510 xs *lst = notify_list(snac, 0, XS_ALL);
3511 int cnt = 0;
3512
3513 xs_list *p = lst;
3514 const xs_str *v;
3515
3516 while (xs_list_iter(&p, &v)) {
3517 xs *id = xs_strip_i(xs_dup(v));
3518
3519 /* old? count no more */
3520 if (strcmp(id, t) < 0)
3521 break;
3522
3523 cnt++;
3524 }
3525
3526 return cnt;
3527}
3528
3529
3530void notify_clear(snac *snac)
3531/* clears all notifications */
3532{
3533 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
3534 xs *lst = xs_glob(spec, 0, 0);
3535 xs_list *p = lst;
3536 const xs_str *v;
3537
3538 while (xs_list_iter(&p, &v))
3539 unlink(v);
3540
3541 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3542
3543 if (mtime(idx) != 0.0) {
3544 pthread_mutex_lock(&data_mutex);
3545 truncate(idx, 0);
3546 pthread_mutex_unlock(&data_mutex);
3547 }
3548}
3549
3550
3551/** the queue **/
3552
3553static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
3554/* writes safely to the queue */
3555{
3556 xs *tfn = xs_fmt("%s.tmp", fn);
3557 FILE *f;
3558
3559 if ((f = fopen(tfn, "w")) != NULL) {
3560 xs_json_dump(msg, 4, f);
3561 fclose(f);
3562
3563 rename(tfn, fn);
3564 }
3565
3566 return msg;
3567}
3568
3569
3570static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
3571/* creates a queue message */
3572{
3573 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
3574 xs *ntid = tid(retries * 60 * qrt);
3575 xs *rn = xs_number_new(retries);
3576
3577 xs_dict *qmsg = xs_dict_new();
3578
3579 qmsg = xs_dict_append(qmsg, "type", type);
3580 qmsg = xs_dict_append(qmsg, "message", msg);
3581 qmsg = xs_dict_append(qmsg, "retries", rn);
3582 qmsg = xs_dict_append(qmsg, "ntid", ntid);
3583
3584 return qmsg;
3585}
3586
3587
3588void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3589/* enqueues an input message */
3590{
3591 xs *qmsg = _new_qmsg("input", msg, retries);
3592 const char *ntid = xs_dict_get(qmsg, "ntid");
3593 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3594
3595 qmsg = xs_dict_append(qmsg, "req", req);
3596
3597 qmsg = _enqueue_put(fn, qmsg);
3598
3599 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3600}
3601
3602
3603void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3604/* enqueues an input message from the shared input */
3605{
3606 xs *qmsg = _new_qmsg("input", msg, retries);
3607 const char *ntid = xs_dict_get(qmsg, "ntid");
3608 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3609
3610 qmsg = xs_dict_append(qmsg, "req", req);
3611
3612 qmsg = _enqueue_put(fn, qmsg);
3613
3614 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3615}
3616
3617
3618void enqueue_output_raw(const char *keyid, const char *seckey,
3619 const xs_dict *msg, const xs_str *inbox,
3620 int retries, int p_status)
3621/* enqueues an output message to an inbox */
3622{
3623 xs *qmsg = _new_qmsg("output", msg, retries);
3624 const char *ntid = xs_dict_get(qmsg, "ntid");
3625 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3626
3627 xs *ns = xs_number_new(p_status);
3628 qmsg = xs_dict_append(qmsg, "p_status", ns);
3629
3630 qmsg = xs_dict_append(qmsg, "inbox", inbox);
3631 qmsg = xs_dict_append(qmsg, "keyid", keyid);
3632 qmsg = xs_dict_append(qmsg, "seckey", seckey);
3633
3634 /* if it's to be sent right now, bypass the disk queue and post the job */
3635 if (retries == 0 && p_state != NULL)
3636 job_post(qmsg, 0);
3637 else {
3638 qmsg = _enqueue_put(fn, qmsg);
3639 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3640 }
3641}
3642
3643
3644void enqueue_output(snac *snac, const xs_dict *msg,
3645 const xs_str *inbox, int retries, int p_status)
3646/* enqueues an output message to an inbox */
3647{
3648 if (is_msg_mine(snac, inbox)) {
3649 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3650 return;
3651 }
3652
3653 const char *seckey = xs_dict_get(snac->key, "secret");
3654
3655 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3656}
3657
3658
3659void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3660 const xs_str *actor, int retries)
3661/* enqueues an output message for an actor */
3662{
3663 xs *inbox = get_actor_inbox(actor, 1);
3664
3665 if (!xs_is_null(inbox))
3666 enqueue_output(snac, msg, inbox, retries, 0);
3667 else
3668 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3669}
3670
3671
3672void enqueue_email(const xs_dict *msg, int retries)
3673/* enqueues an email message to be sent */
3674{
3675 xs *qmsg = _new_qmsg("email", msg, retries);
3676 const char *ntid = xs_dict_get(qmsg, "ntid");
3677 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3678
3679 qmsg = _enqueue_put(fn, qmsg);
3680
3681 srv_debug(1, xs_fmt("enqueue_email %d", retries));
3682}
3683
3684
3685void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3686/* enqueues a message to be sent via Telegram */
3687{
3688 xs *qmsg = _new_qmsg("telegram", msg, 0);
3689 const char *ntid = xs_dict_get(qmsg, "ntid");
3690 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3691
3692 qmsg = xs_dict_append(qmsg, "bot", bot);
3693 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
3694
3695 qmsg = _enqueue_put(fn, qmsg);
3696
3697 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3698}
3699
3700void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3701/* enqueues a message to be sent via ntfy */
3702{
3703 xs *qmsg = _new_qmsg("ntfy", msg, 0);
3704 const char *ntid = xs_dict_get(qmsg, "ntid");
3705 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3706
3707 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3708 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
3709
3710
3711 qmsg = _enqueue_put(fn, qmsg);
3712
3713 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3714}
3715
3716void enqueue_message(snac *snac, const xs_dict *msg)
3717/* enqueues an output message */
3718{
3719 xs *qmsg = _new_qmsg("message", msg, 0);
3720 const char *ntid = xs_dict_get(qmsg, "ntid");
3721 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3722
3723 qmsg = _enqueue_put(fn, qmsg);
3724
3725 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3726}
3727
3728
3729void enqueue_close_question(snac *user, const char *id, int end_secs)
3730/* enqueues the closing of a question */
3731{
3732 xs *qmsg = _new_qmsg("close_question", id, 0);
3733 xs *ntid = tid(end_secs);
3734 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3735
3736 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3737
3738 qmsg = _enqueue_put(fn, qmsg);
3739
3740 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3741}
3742
3743
3744void enqueue_object_request(snac *user, const char *id, int forward_secs)
3745/* enqueues the request of an object in the future */
3746{
3747 xs *qmsg = _new_qmsg("object_request", id, 0);
3748 xs *ntid = tid(forward_secs);
3749 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3750
3751 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3752
3753 qmsg = _enqueue_put(fn, qmsg);
3754
3755 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3756}
3757
3758
3759void enqueue_verify_links(snac *user)
3760/* enqueues a link verification */
3761{
3762 xs *qmsg = _new_qmsg("verify_links", "", 0);
3763 const char *ntid = xs_dict_get(qmsg, "ntid");
3764 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3765
3766 qmsg = _enqueue_put(fn, qmsg);
3767
3768 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3769}
3770
3771
3772void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3773/* enqueues an actor refresh */
3774{
3775 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3776 xs *ntid = tid(forward_secs);
3777 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3778
3779 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3780 qmsg = xs_dict_append(qmsg, "actor", actor);
3781
3782 qmsg = _enqueue_put(fn, qmsg);
3783
3784 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3785}
3786
3787
3788void enqueue_webmention(const xs_dict *msg)
3789/* enqueues a webmention for the post */
3790{
3791 xs *qmsg = _new_qmsg("webmention", msg, 0);
3792 const char *ntid = xs_dict_get(qmsg, "ntid");
3793 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3794
3795 qmsg = _enqueue_put(fn, qmsg);
3796
3797 srv_debug(1, xs_fmt("enqueue_webmention"));
3798}
3799
3800
3801void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries)
3802/* enqueues a notification webhook */
3803{
3804 const char *webhook = xs_dict_get(user->config, "notify_webhook");
3805
3806 if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3807 xs *msg = xs_dup(noti);
3808
3809 /* add more data */
3810 msg = xs_dict_set(msg, "target", user->actor);
3811 msg = xs_dict_set(msg, "uid", user->uid);
3812 msg = xs_dict_set(msg, "basedir", srv_basedir);
3813 msg = xs_dict_set(msg, "baseurl", srv_baseurl);
3814
3815 xs *actor_obj = NULL;
3816
3817 if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj)
3818 msg = xs_dict_set(msg, "account", actor_obj);
3819
3820 /* if this post is a reply, also add the inReplyTo object */
3821 const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo");
3822
3823 if (xs_is_string(in_reply_to)) {
3824 xs *irt_obj = NULL;
3825
3826 if (valid_status(object_get(in_reply_to, &irt_obj)))
3827 msg = xs_dict_set(msg, "reply", irt_obj);
3828 }
3829
3830 xs *qmsg = _new_qmsg("notify_webhook", msg, retries);
3831 const char *ntid = xs_dict_get(qmsg, "ntid");
3832 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3833
3834 qmsg = _enqueue_put(fn, qmsg);
3835
3836 snac_debug(user, 1, xs_fmt("notify_webhook"));
3837 }
3838}
3839
3840
3841void enqueue_collect_replies(snac *user, const char *post)
3842/* enqueues a collect replies request */
3843{
3844 xs *qmsg = _new_qmsg("collect_replies", post, 0);
3845 const char *ntid = xs_dict_get(qmsg, "ntid");
3846 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3847
3848 qmsg = _enqueue_put(fn, qmsg);
3849
3850 snac_debug(user, 1, xs_fmt("enqueue_collect_replies %s", post));
3851}
3852
3853
3854void enqueue_collect_outbox(snac *user, const char *actor_id)
3855/* enqueues a collect outbox request */
3856{
3857 xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
3858 const char *ntid = xs_dict_get(qmsg, "ntid");
3859 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3860
3861 qmsg = _enqueue_put(fn, qmsg);
3862
3863 snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
3864}
3865
3866
3867void enqueue_fsck(void)
3868/* enqueues an fsck */
3869{
3870 xs *qmsg = _new_qmsg("fsck", "", 0);
3871 const char *ntid = xs_dict_get(qmsg, "ntid");
3872 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3873
3874 qmsg = _enqueue_put(fn, qmsg);
3875}
3876
3877
3878int was_question_voted(snac *user, const char *id)
3879/* returns true if the user voted in this poll */
3880{
3881 xs *children = object_children(id);
3882 int voted = 0;
3883 xs_list *p;
3884 const xs_str *md5;
3885
3886 p = children;
3887 while (xs_list_iter(&p, &md5)) {
3888 xs *obj = NULL;
3889
3890 if (valid_status(object_get_by_md5(md5, &obj))) {
3891 const char *atto = get_atto(obj);
3892 if (atto && strcmp(atto, user->actor) == 0 &&
3893 !xs_is_null(xs_dict_get(obj, "name"))) {
3894 voted = 1;
3895 break;
3896 }
3897 }
3898 }
3899
3900 return voted;
3901}
3902
3903
3904xs_list *user_queue(snac *snac)
3905/* returns a list with filenames that can be dequeued */
3906{
3907 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
3908 xs_list *list = xs_list_new();
3909 time_t t = time(NULL);
3910 xs_list *p;
3911 const xs_val *v;
3912
3913 xs *fns = xs_glob(spec, 0, 0);
3914
3915 p = fns;
3916 while (xs_list_iter(&p, &v)) {
3917 /* get the retry time from the basename */
3918 char *bn = strrchr(v, '/');
3919 time_t t2 = atol(bn + 1);
3920
3921 if (t2 > t)
3922 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3923 else {
3924 list = xs_list_append(list, v);
3925 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3926 }
3927 }
3928
3929 return list;
3930}
3931
3932
3933xs_list *queue(void)
3934/* returns a list with filenames that can be dequeued */
3935{
3936 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
3937 xs_list *list = xs_list_new();
3938 time_t t = time(NULL);
3939 xs_list *p;
3940 const xs_val *v;
3941
3942 xs *fns = xs_glob(spec, 0, 0);
3943
3944 p = fns;
3945 while (xs_list_iter(&p, &v)) {
3946 /* get the retry time from the basename */
3947 char *bn = strrchr(v, '/');
3948 time_t t2 = atol(bn + 1);
3949
3950 if (t2 > t)
3951 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3952 else {
3953 list = xs_list_append(list, v);
3954 srv_debug(2, xs_fmt("queue ready for %s", v));
3955 }
3956 }
3957
3958 return list;
3959}
3960
3961
3962xs_dict *queue_get(const char *fn)
3963/* gets a file from a queue */
3964{
3965 FILE *f;
3966 xs_dict *obj = NULL;
3967
3968 if ((f = fopen(fn, "r")) != NULL) {
3969 obj = xs_json_load(f);
3970 fclose(f);
3971 }
3972
3973 return obj;
3974}
3975
3976
3977xs_dict *dequeue(const char *fn)
3978/* dequeues a message */
3979{
3980 xs_dict *obj = queue_get(fn);
3981
3982 unlink(fn);
3983
3984 return obj;
3985}
3986
3987
3988/** the purge **/
3989
3990static int _purge_file(const char *fn, time_t mt)
3991/* purge fn if it's older than days */
3992{
3993 int ret = 0;
3994
3995 if (mtime(fn) < mt) {
3996 /* older than the minimum time: delete it */
3997 unlink(fn);
3998 srv_debug(2, xs_fmt("purged %s", fn));
3999 ret = 1;
4000 }
4001
4002 return ret;
4003}
4004
4005
4006static void _purge_dir(const char *dir, int days)
4007/* purges all files in a directory older than days */
4008{
4009 int cnt = 0;
4010
4011 if (days) {
4012 time_t mt = time(NULL) - days * 24 * 3600;
4013 xs *spec = xs_fmt("%s/" "*", dir);
4014 xs *list = xs_glob(spec, 0, 0);
4015 xs_list *p;
4016 const xs_str *v;
4017
4018 p = list;
4019 while (xs_list_iter(&p, &v))
4020 cnt += _purge_file(v, mt);
4021
4022 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
4023 }
4024}
4025
4026
4027static void _purge_user_subdir(snac *snac, const char *subdir, int days)
4028/* purges all files in a user subdir older than days */
4029{
4030 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
4031
4032 _purge_dir(u_subdir, days);
4033}
4034
4035
4036void purge_server(void)
4037/* purge global server data */
4038{
4039 xs *spec = xs_fmt("%s/object/??", srv_basedir);
4040 xs *dirs = xs_glob(spec, 0, 0);
4041 xs_list *p;
4042 const xs_str *v;
4043 int cnt = 0;
4044 int icnt = 0;
4045
4046 time_t mt = time(NULL) - 7 * 24 * 3600;
4047
4048 p = dirs;
4049 while (xs_list_iter(&p, &v)) {
4050 xs_list *p2;
4051 const xs_str *v2;
4052
4053 {
4054 xs *spec2 = xs_fmt("%s/" "*.json", v);
4055 xs *files = xs_glob(spec2, 0, 0);
4056
4057 p2 = files;
4058 while (xs_list_iter(&p2, &v2)) {
4059 int n_link;
4060
4061 /* old and with no hard links? */
4062 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
4063 xs *s1 = xs_replace(v2, ".json", "");
4064 xs *l = xs_split(s1, "/");
4065 const char *md5 = xs_list_get(l, -1);
4066
4067 object_del_by_md5(md5);
4068 cnt++;
4069 }
4070 }
4071 }
4072
4073 {
4074 /* look for stray indexes */
4075 xs *speci = xs_fmt("%s/" "*_?.idx", v);
4076 xs *idxfs = xs_glob(speci, 0, 0);
4077
4078 p2 = idxfs;
4079 while (xs_list_iter(&p2, &v2)) {
4080 /* old enough to consider? */
4081 if (mtime(v2) < mt) {
4082 /* check if the indexed object is here */
4083 xs *o = xs_dup(v2);
4084 char *ext = strchr(o, '_');
4085
4086 if (ext) {
4087 *ext = '\0';
4088 o = xs_str_cat(o, ".json");
4089
4090 if (mtime(o) == 0.0) {
4091 /* delete */
4092 unlink(v2);
4093 srv_debug(1, xs_fmt("purged %s", v2));
4094 icnt++;
4095 }
4096 }
4097 }
4098 }
4099
4100 /* delete index backups */
4101 xs *specb = xs_fmt("%s/" "*.bak", v);
4102 xs *bakfs = xs_glob(specb, 0, 0);
4103
4104 p2 = bakfs;
4105 while (xs_list_iter(&p2, &v2)) {
4106 unlink(v2);
4107 srv_debug(1, xs_fmt("purged %s", v2));
4108 }
4109 }
4110 }
4111
4112 /* purge collected inboxes */
4113 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
4114 _purge_dir(ib_dir, 7);
4115
4116 /* purge the instance timeline */
4117 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
4118 int itl_gc = index_gc(itl_fn);
4119
4120 /* purge tag indexes */
4121 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
4122 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
4123 p = tag_dirs;
4124
4125 int tag_gc = 0;
4126 while (xs_list_iter(&p, &v)) {
4127 xs *spec2 = xs_fmt("%s/" "*.idx", v);
4128 xs *files = xs_glob(spec2, 0, 0);
4129 xs_list *p2;
4130 const xs_str *v2;
4131
4132 p2 = files;
4133 while (xs_list_iter(&p2, &v2)) {
4134 tag_gc += index_gc(v2);
4135 xs *bak = xs_fmt("%s.bak", v2);
4136 unlink(bak);
4137
4138 if (index_len(v2) == 0) {
4139 /* there are no longer any entry with this tag;
4140 purge it completely */
4141 unlink(v2);
4142 xs *dottag = xs_replace(v2, ".idx", ".tag");
4143 unlink(dottag);
4144 }
4145 }
4146 }
4147
4148 srv_debug(1, xs_fmt("purge: global "
4149 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
4150}
4151
4152
4153void delete_purged_posts(snac *user, int days)
4154/* enqueues Delete activities for local purged messages */
4155{
4156 if (days == 0)
4157 return;
4158
4159 time_t mt = time(NULL) - days * 24 * 3600;
4160 xs *spec = xs_fmt("%s/public/" "*.json", user->basedir);
4161 xs *list = xs_glob(spec, 0, 0);
4162 const char *v;
4163
4164 xs_list_foreach(list, v) {
4165 if (mtime(v) < mt) {
4166 /* to be purged; is it a Note by us? */
4167 FILE *f;
4168
4169 if ((f = fopen(v, "r")) != NULL) {
4170 xs *msg = xs_json_load(f);
4171 fclose(f);
4172
4173 if (xs_is_dict(msg)) {
4174 const char *id = xs_dict_get(msg, "id");
4175
4176 if (xs_is_string(id) && is_msg_mine(user, id)) {
4177 xs *d_msg = msg_delete(user, id);
4178
4179 enqueue_message(user, d_msg);
4180
4181 snac_log(user, xs_fmt("enqueued Delete for purged message %s", id));
4182 }
4183 }
4184 }
4185 }
4186 }
4187}
4188
4189
4190void purge_user(snac *snac)
4191/* do the purge for this user */
4192{
4193 int priv_days, pub_days, user_days = 0;
4194 const char *v;
4195 int n;
4196
4197 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
4198 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
4199
4200 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
4201 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
4202 user_days = xs_number_get(v);
4203
4204 if (user_days) {
4205 /* override admin settings only if they are lesser */
4206 if (priv_days == 0 || user_days < priv_days)
4207 priv_days = user_days;
4208
4209 if (pub_days == 0 || user_days < pub_days)
4210 pub_days = user_days;
4211 }
4212
4213 if (xs_is_true(xs_dict_get(srv_config, "propagate_local_purge")))
4214 delete_purged_posts(snac, pub_days);
4215
4216 _purge_user_subdir(snac, "hidden", priv_days);
4217 _purge_user_subdir(snac, "private", priv_days);
4218
4219 _purge_user_subdir(snac, "public", pub_days);
4220
4221 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
4222 "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
4223
4224 for (n = 0; idxs[n]; n++) {
4225 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
4226 int gc = index_gc(idx);
4227 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
4228 }
4229
4230 /* purge lists */
4231 {
4232 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
4233 xs *lol = xs_glob(spec, 0, 0);
4234 int c = 0;
4235 const char *v;
4236
4237 while (xs_list_next(lol, &v, &c)) {
4238 int gc = index_gc(v);
4239 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
4240 }
4241 }
4242
4243 /* unrelated to purging, but it's a janitorial process, so what the hell */
4244 verify_links(snac);
4245}
4246
4247
4248void purge_all(void)
4249/* purge all users */
4250{
4251 snac snac;
4252 xs *list = user_list();
4253 char *p;
4254 const char *uid;
4255
4256 p = list;
4257 while (xs_list_iter(&p, &uid)) {
4258 if (user_open(&snac, uid)) {
4259 purge_user(&snac);
4260 user_free(&snac);
4261 }
4262 }
4263
4264 purge_server();
4265
4266#ifndef NO_MASTODON_API
4267 mastoapi_purge();
4268#endif
4269}
4270
4271
4272/** archive **/
4273
4274void srv_archive(const char *direction, const char *url, xs_dict *req,
4275 const char *payload, int p_size,
4276 int status, xs_dict *headers,
4277 const char *body, int b_size)
4278/* archives a connection */
4279{
4280 /* obsessive archiving */
4281 xs *date = tid(0);
4282 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
4283 FILE *f;
4284
4285 if (mkdirx(dir) != -1) {
4286 xs *meta_fn = xs_fmt("%s/_META", dir);
4287
4288 if ((f = fopen(meta_fn, "w")) != NULL) {
4289 xs *j1 = xs_json_dumps(req, 4);
4290 xs *j2 = xs_json_dumps(headers, 4);
4291
4292 fprintf(f, "dir: %s\n", direction);
4293
4294 if (url)
4295 fprintf(f, "url: %s\n", url);
4296
4297 fprintf(f, "req: %s\n", j1);
4298 fprintf(f, "p_size: %d\n", p_size);
4299 fprintf(f, "status: %d\n", status);
4300 fprintf(f, "response: %s\n", j2);
4301 fprintf(f, "b_size: %d\n", b_size);
4302 fclose(f);
4303 }
4304
4305 if (p_size && payload) {
4306 xs *payload_fn = NULL;
4307 xs *payload_fn_raw = NULL;
4308 const char *v = xs_dict_get(req, "content-type");
4309
4310 if (v && xs_str_in(v, "json") != -1) {
4311 payload_fn = xs_fmt("%s/payload.json", dir);
4312
4313 if ((f = fopen(payload_fn, "w")) != NULL) {
4314 xs *v1 = xs_json_loads(payload);
4315 xs *j1 = NULL;
4316
4317 if (v1 != NULL)
4318 j1 = xs_json_dumps(v1, 4);
4319
4320 if (j1 != NULL)
4321 fwrite(j1, strlen(j1), 1, f);
4322 else
4323 fwrite(payload, p_size, 1, f);
4324
4325 fclose(f);
4326 }
4327 }
4328
4329 payload_fn_raw = xs_fmt("%s/payload", dir);
4330
4331 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
4332 fwrite(payload, p_size, 1, f);
4333 fclose(f);
4334 }
4335 }
4336
4337 if (b_size && body) {
4338 xs *body_fn = NULL;
4339 const char *v = xs_dict_get(headers, "content-type");
4340
4341 if (v && xs_str_in(v, "json") != -1) {
4342 body_fn = xs_fmt("%s/body.json", dir);
4343
4344 if ((f = fopen(body_fn, "w")) != NULL) {
4345 xs *v1 = xs_json_loads(body);
4346 xs *j1 = NULL;
4347
4348 if (v1 != NULL)
4349 j1 = xs_json_dumps(v1, 4);
4350
4351 if (j1 != NULL)
4352 fwrite(j1, strlen(j1), 1, f);
4353 else
4354 fwrite(body, b_size, 1, f);
4355
4356 fclose(f);
4357 }
4358 }
4359 else {
4360 body_fn = xs_fmt("%s/body", dir);
4361
4362 if ((f = fopen(body_fn, "w")) != NULL) {
4363 fwrite(body, b_size, 1, f);
4364 fclose(f);
4365 }
4366 }
4367 }
4368 }
4369}
4370
4371
4372void srv_archive_error(const char *prefix, const xs_str *err,
4373 const xs_dict *req, const xs_val *data)
4374/* archives an error */
4375{
4376 xs *ntid = tid(0);
4377 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
4378 FILE *f;
4379
4380 if ((f = fopen(fn, "w")) != NULL) {
4381 fprintf(f, "Error: %s\n", err);
4382
4383 if (req) {
4384 fprintf(f, "Request headers:\n");
4385
4386 xs_json_dump(req, 4, f);
4387
4388 fprintf(f, "\n");
4389 }
4390
4391 if (data) {
4392 fprintf(f, "Data:\n");
4393
4394 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
4395 xs_json_dump(data, 4, f);
4396 }
4397 else
4398 fprintf(f, "%s", data);
4399
4400 fprintf(f, "\n");
4401 }
4402
4403 fclose(f);
4404 }
4405}
4406
4407
4408void srv_archive_qitem(const char *prefix, xs_dict *q_item)
4409/* archives a q_item in the error folder */
4410{
4411 xs *ntid = tid(0);
4412 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
4413 FILE *f;
4414
4415 if ((f = fopen(fn, "w")) != NULL) {
4416 xs_json_dump(q_item, 4, f);
4417 fclose(f);
4418 }
4419}
4420
4421
4422t_announcement *announcement(const double after)
4423/* returns announcement text or NULL if none exists or it is olde than "after" */
4424{
4425 static const long int MAX_SIZE = 2048;
4426 static t_announcement a = {
4427 .text = NULL,
4428 .timestamp = 0.0,
4429 };
4430 static xs_str *fn = NULL;
4431 if (fn == NULL)
4432 fn = xs_fmt("%s/announcement.txt", srv_basedir);
4433
4434 const double ts = mtime(fn);
4435
4436 /* file does not exist or other than what was requested */
4437 if (ts == 0.0 || ts <= after)
4438 return NULL;
4439
4440 /* nothing changed, just return the current announcement */
4441 if (a.text != NULL && ts <= a.timestamp)
4442 return &a;
4443
4444 /* read and store new announcement */
4445 FILE *f;
4446
4447 if ((f = fopen(fn, "r")) != NULL) {
4448 fseek (f, 0, SEEK_END);
4449 const long int length = ftell(f);
4450
4451 if (length > MAX_SIZE) {
4452 /* this is probably unintentional */
4453 srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
4454 }
4455 else
4456 if (length > 0) {
4457 fseek (f, 0, SEEK_SET);
4458 char *buffer = malloc(length + 1);
4459 if (buffer) {
4460 fread(buffer, 1, length, f);
4461 buffer[length] = '\0';
4462
4463 free(a.text);
4464 a.text = buffer;
4465 a.timestamp = ts;
4466 }
4467 else {
4468 srv_log("Error allocating memory for announcement");
4469 }
4470 }
4471 else {
4472 /* an empty file means no announcement */
4473 free(a.text);
4474 a.text = NULL;
4475 a.timestamp = 0.0;
4476 }
4477
4478 fclose (f);
4479 }
4480
4481 if (a.text != NULL)
4482 return &a;
4483
4484 return NULL;
4485}
4486
4487
4488xs_str *make_url(const char *href, const char *proxy, int by_token)
4489/* makes an URL, possibly including proxying */
4490{
4491 xs_str *url = NULL;
4492
4493 if (proxy && !xs_startswith(href, srv_baseurl)) {
4494 xs *p = NULL;
4495
4496 if (by_token) {
4497 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
4498 xs *tk = xs_md5_hex(tks, strlen(tks));
4499
4500 p = xs_fmt("%s/y/%s/", proxy, tk);
4501 }
4502 else
4503 p = xs_fmt("%s/x/", proxy);
4504
4505 url = xs_replace(href, "https:/" "/", p);
4506 }
4507 else
4508 url = xs_dup(href);
4509
4510 return url;
4511}
4512
4513
4514/** bad login throttle **/
4515
4516xs_str *_badlogin_fn(const char *addr)
4517{
4518 xs *md5 = xs_md5_hex(addr, strlen(addr));
4519 xs *dir = xs_fmt("%s/badlogin", srv_basedir);
4520
4521 mkdirx(dir);
4522
4523 return xs_fmt("%s/%s", dir, md5);
4524}
4525
4526
4527int _badlogin_read(const char *fn, int *failures)
4528/* reads a badlogin file */
4529{
4530 int ok = 0;
4531 FILE *f;
4532
4533 pthread_mutex_lock(&data_mutex);
4534
4535 if ((f = fopen(fn, "r")) != NULL) {
4536 xs *l = xs_readline(f);
4537 fclose(f);
4538
4539 if (sscanf(l, "%d", failures) == 1)
4540 ok = 1;
4541 }
4542
4543 pthread_mutex_unlock(&data_mutex);
4544
4545 return ok;
4546}
4547
4548
4549int badlogin_check(const char *user, const char *addr)
4550/* checks if this address is authorized to try a login */
4551{
4552 int valid = 1;
4553
4554 if (xs_type(addr) == XSTYPE_STRING) {
4555 xs *fn = _badlogin_fn(addr);
4556 double mt = mtime(fn);
4557
4558 if (mt > 0) {
4559 int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
4560 "badlogin_expire", "300"));
4561
4562 mt += badlogin_expire;
4563
4564 /* if file is expired, delete and give pass */
4565 if (mt < time(NULL)) {
4566 srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
4567 unlink(fn);
4568 }
4569 else {
4570 int failures;
4571
4572 if (_badlogin_read(fn, &failures)) {
4573 int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
4574 "badlogin_retries", "5"));
4575
4576 if (failures >= badlogin_max) {
4577 valid = 0;
4578
4579 xs *d = xs_str_iso_date((time_t) mt);
4580
4581 srv_debug(1,
4582 xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
4583 }
4584 }
4585 }
4586 }
4587 }
4588
4589 return valid;
4590}
4591
4592
4593void badlogin_inc(const char *user, const char *addr)
4594/* increments a bad login from this address */
4595{
4596 if (xs_type(addr) == XSTYPE_STRING) {
4597 int failures = 0;
4598 xs *fn = _badlogin_fn(addr);
4599 FILE *f;
4600
4601 _badlogin_read(fn, &failures);
4602
4603 pthread_mutex_lock(&data_mutex);
4604
4605 if ((f = fopen(fn, "w")) != NULL) {
4606 failures++;
4607
4608 fprintf(f, "%d %s %s\n", failures, addr, user);
4609 fclose(f);
4610
4611 srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
4612 }
4613
4614 pthread_mutex_unlock(&data_mutex);
4615 }
4616}
4617
4618
4619/** language strings **/
4620
4621const char *lang_str(const char *str, const snac *user)
4622/* returns a translated string */
4623{
4624 const char *n_str = str;
4625
4626 if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4627 n_str = xs_dict_get(user->lang, str);
4628
4629 if (xs_is_null(n_str) || *n_str == '\0')
4630 n_str = str;
4631 }
4632
4633 return n_str;
4634}
4635
4636
4637/** integrity checks **/
4638
4639void data_fsck(void)
4640{
4641 xs *list = user_list();
4642 const char *uid;
4643
4644 xs_list_foreach(list, uid) {
4645 snac user;
4646
4647 if (!user_open(&user, uid))
4648 continue;
4649
4650 {
4651 /* iterate all private posts and check that non-public posts
4652 from this user are also linked into the public directory,
4653 to avoid the don't-fucking-delete-my-own-private-posts purge bug */
4654
4655 xs *priv_spec = xs_fmt("%s/private/""*.json", user.basedir);
4656 xs *posts = xs_glob(priv_spec, 0, 0);
4657 const char *priv_fn;
4658
4659 xs_list_foreach(posts, priv_fn) {
4660 xs *pub_fn = xs_replace(priv_fn, "/private/", "/public/");
4661
4662 /* already there? look no more */
4663 if (mtime(pub_fn))
4664 continue;
4665
4666 /* read the post */
4667 FILE *f;
4668 if ((f = fopen(priv_fn, "r")) == NULL)
4669 continue;
4670
4671 xs *post = xs_json_load(f);
4672 fclose(f);
4673
4674 if (!xs_is_dict(post))
4675 continue;
4676
4677 const char *attr_to = get_atto(post);
4678
4679 if (!xs_is_string(attr_to) || strcmp(attr_to, user.actor) != 0) {
4680 /* not from this user */
4681 continue;
4682 }
4683
4684 /* link */
4685 snac_debug(&user, 1, xs_fmt("fsck: fixed missing link %s", xs_dict_get(post, "id")));
4686 link(priv_fn, pub_fn);
4687 }
4688 }
4689
4690 user_free(&user);
4691 }
4692}