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