2.84
data.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 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_likes(const char *id)
1047{
1048 xs *fn = _object_index_fn(id, "_l.idx");
1049 return index_list(fn, XS_ALL);
1050}
1051
1052
1053xs_list *object_announces(const char *id)
1054{
1055 xs *fn = _object_index_fn(id, "_a.idx");
1056 return index_list(fn, XS_ALL);
1057}
1058
1059
1060int object_parent(const char *md5, char parent[MD5_HEX_SIZE])
1061/* returns the object parent, if any */
1062{
1063 xs *fn = _object_fn_by_md5(md5, "object_parent");
1064
1065 fn = xs_replace_i(fn, ".json", "_p.idx");
1066 return index_first(fn, parent);
1067}
1068
1069
1070int object_admire(const char *id, const char *actor, int like)
1071/* actor likes or announces this object */
1072{
1073 int status = HTTP_STATUS_OK;
1074 xs *fn = _object_fn(id);
1075
1076 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
1077
1078 if (!index_in(fn, actor)) {
1079 status = index_add(fn, actor);
1080
1081 srv_debug(1, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn));
1082 }
1083
1084 return status;
1085}
1086
1087
1088int object_unadmire(const char *id, const char *actor, int like)
1089/* actor no longer likes or announces this object */
1090{
1091 int status;
1092 xs *fn = _object_fn(id);
1093
1094 fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
1095
1096 status = index_del(fn, actor);
1097
1098 if (valid_status(status))
1099 index_gc(fn);
1100
1101 srv_debug(0,
1102 xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
1103
1104 return status;
1105}
1106
1107
1108xs_str *object_user_cache_fn_by_md5(snac *user, const char *md5, const char *cachedir)
1109{
1110 return xs_fmt("%s/%s/%s.json", user->basedir, cachedir, md5);
1111}
1112
1113
1114xs_str *object_user_cache_fn(snac *user, const char *id, const char *cachedir)
1115{
1116 xs *md5 = xs_md5_hex(id, strlen(id));
1117 return object_user_cache_fn_by_md5(user, md5, cachedir);
1118}
1119
1120
1121xs_str *object_user_cache_index_fn(snac *user, const char *cachedir)
1122{
1123 return xs_fmt("%s/%s.idx", user->basedir, cachedir);
1124}
1125
1126
1127int _object_user_cache(snac *user, const char *id, const char *cachedir, int del)
1128/* adds or deletes from a user cache */
1129{
1130 xs *ofn = _object_fn(id);
1131 xs *cfn = object_user_cache_fn(user, id, cachedir);
1132 xs *idx = object_user_cache_index_fn(user, cachedir);
1133 int ret;
1134
1135 if (del) {
1136 ret = unlink(cfn);
1137 index_del(idx, id);
1138 }
1139 else {
1140 /* create the subfolder, if it does not exist */
1141 xs *dir = xs_fmt("%s/%s/", user->basedir, cachedir);
1142 mkdirx(dir);
1143
1144 if ((ret = link(ofn, cfn)) != -1)
1145 index_add(idx, id);
1146 }
1147
1148 return ret;
1149}
1150
1151
1152int object_user_cache_add(snac *user, const char *id, const char *cachedir)
1153/* caches an object into a user cache */
1154{
1155 return _object_user_cache(user, id, cachedir, 0);
1156}
1157
1158
1159int object_user_cache_del(snac *user, const char *id, const char *cachedir)
1160/* deletes an object from a user cache */
1161{
1162 return _object_user_cache(user, id, cachedir, 1);
1163}
1164
1165
1166int object_user_cache_in(snac *user, const char *id, const char *cachedir)
1167/* checks if an object is stored in a cache */
1168{
1169 xs *cfn = object_user_cache_fn(user, id, cachedir);
1170 return !!(mtime(cfn) != 0.0);
1171}
1172
1173
1174int object_user_cache_in_by_md5(snac *user, const char *md5, const char *cachedir)
1175/* checks if an object is stored in a cache */
1176{
1177 xs *cfn = object_user_cache_fn_by_md5(user, md5, cachedir);
1178 return !!(mtime(cfn) != 0.0);
1179}
1180
1181
1182xs_list *object_user_cache_list(snac *user, const char *cachedir, int max, int inv)
1183/* returns the objects in a cache as a list */
1184{
1185 xs *idx = xs_fmt("%s/%s.idx", user->basedir, cachedir);
1186 return inv ? index_list_desc(idx, 0, max) : index_list(idx, max);
1187}
1188
1189
1190/** specialized functions **/
1191
1192/** followers **/
1193
1194int follower_add(snac *snac, const char *actor)
1195/* adds a follower */
1196{
1197 int ret = object_user_cache_add(snac, actor, "followers");
1198
1199 snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
1200
1201 return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
1202}
1203
1204
1205int follower_del(snac *snac, const char *actor)
1206/* deletes a follower */
1207{
1208 int ret = object_user_cache_del(snac, actor, "followers");
1209
1210 snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
1211
1212 return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
1213}
1214
1215
1216int follower_check(snac *snac, const char *actor)
1217/* checks if someone is a follower */
1218{
1219 return object_user_cache_in(snac, actor, "followers");
1220}
1221
1222
1223int follower_list_len(snac *snac)
1224/* returns the number of followers */
1225{
1226 xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1227 return xs_list_len(list);
1228}
1229
1230
1231xs_list *follower_list(snac *snac)
1232/* returns the list of followers */
1233{
1234 xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1235 xs_list *fwers = xs_list_new();
1236 char *p;
1237 const char *v;
1238
1239 /* resolve the list of md5 to be a list of actors */
1240 p = list;
1241 while (xs_list_iter(&p, &v)) {
1242 xs *a_obj = NULL;
1243
1244 if (valid_status(object_get_by_md5(v, &a_obj))) {
1245 const char *actor = xs_dict_get(a_obj, "id");
1246
1247 if (!xs_is_null(actor)) {
1248 /* check if the actor is still cached */
1249 xs *fn = xs_fmt("%s/followers/%s.json", snac->basedir, v);
1250
1251 if (mtime(fn) > 0.0)
1252 fwers = xs_list_append(fwers, actor);
1253 }
1254 }
1255 }
1256
1257 return fwers;
1258}
1259
1260
1261/** pending followers **/
1262
1263int pending_add(snac *user, const char *actor, const xs_dict *msg)
1264/* stores the follow message for later confirmation */
1265{
1266 xs *dir = xs_fmt("%s/pending", user->basedir);
1267 xs *md5 = xs_md5_hex(actor, strlen(actor));
1268 xs *fn = xs_fmt("%s/%s.json", dir, md5);
1269 FILE *f;
1270
1271 mkdirx(dir);
1272
1273 if ((f = fopen(fn, "w")) == NULL)
1274 return -1;
1275
1276 xs_json_dump(msg, 4, f);
1277 fclose(f);
1278
1279 return 0;
1280}
1281
1282
1283int pending_check(snac *user, const char *actor)
1284/* checks if there is a pending follow confirmation for the actor */
1285{
1286 xs *md5 = xs_md5_hex(actor, strlen(actor));
1287 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1288
1289 return mtime(fn) != 0;
1290}
1291
1292
1293xs_dict *pending_get(snac *user, const char *actor)
1294/* returns the pending follow confirmation for the actor */
1295{
1296 xs *md5 = xs_md5_hex(actor, strlen(actor));
1297 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1298 xs_dict *msg = NULL;
1299 FILE *f;
1300
1301 if ((f = fopen(fn, "r")) != NULL) {
1302 msg = xs_json_load(f);
1303 fclose(f);
1304 }
1305
1306 return msg;
1307}
1308
1309
1310void pending_del(snac *user, const char *actor)
1311/* deletes a pending follow confirmation for the actor */
1312{
1313 xs *md5 = xs_md5_hex(actor, strlen(actor));
1314 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1315
1316 unlink(fn);
1317}
1318
1319
1320xs_list *pending_list(snac *user)
1321/* returns a list of pending follow confirmations */
1322{
1323 xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1324 xs *l = xs_glob(spec, 0, 0);
1325 xs_list *r = xs_list_new();
1326 const char *v;
1327
1328 xs_list_foreach(l, v) {
1329 FILE *f;
1330 xs *msg = NULL;
1331
1332 if ((f = fopen(v, "r")) == NULL)
1333 continue;
1334
1335 msg = xs_json_load(f);
1336 fclose(f);
1337
1338 if (msg == NULL)
1339 continue;
1340
1341 const char *actor = xs_dict_get(msg, "actor");
1342
1343 if (xs_type(actor) == XSTYPE_STRING)
1344 r = xs_list_append(r, actor);
1345 }
1346
1347 return r;
1348}
1349
1350
1351int pending_count(snac *user)
1352/* returns the number of pending follow confirmations */
1353{
1354 xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1355 xs *l = xs_glob(spec, 0, 0);
1356
1357 return xs_list_len(l);
1358}
1359
1360
1361/** timeline **/
1362
1363double timeline_mtime(snac *snac)
1364{
1365 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1366 return mtime(fn);
1367}
1368
1369
1370int timeline_touch(snac *snac)
1371/* changes the date of the timeline index */
1372{
1373 xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1374 return utimes(fn, NULL);
1375}
1376
1377
1378xs_str *timeline_fn_by_md5(snac *snac, const char *md5)
1379/* get the filename of an entry by md5 from any timeline */
1380{
1381 xs_str *fn = NULL;
1382
1383 if (is_md5_hex(md5)) {
1384 fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
1385
1386 if (mtime(fn) == 0.0) {
1387 fn = xs_free(fn);
1388 fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
1389
1390 if (mtime(fn) == 0.0)
1391 fn = xs_free(fn);
1392 }
1393 }
1394
1395 return fn;
1396}
1397
1398
1399int timeline_here_by_md5(snac *snac, const char *md5)
1400/* checks if an object is in the user cache */
1401{
1402 xs *fn = timeline_fn_by_md5(snac, md5);
1403
1404 return !(fn == NULL);
1405}
1406
1407
1408int timeline_here(snac *user, const char *id)
1409{
1410 xs *md5 = xs_md5_hex(id, strlen(id));
1411
1412 return timeline_here_by_md5(user, md5);
1413}
1414
1415
1416int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
1417/* gets a message from the timeline */
1418{
1419 int status = HTTP_STATUS_NOT_FOUND;
1420 FILE *f = NULL;
1421
1422 xs *fn = timeline_fn_by_md5(snac, md5);
1423
1424 if (fn != NULL && (f = fopen(fn, "r")) != NULL) {
1425 *msg = xs_json_load(f);
1426 fclose(f);
1427
1428 if (*msg != NULL)
1429 status = HTTP_STATUS_OK;
1430 }
1431
1432 return status;
1433}
1434
1435
1436int timeline_del(snac *snac, const char *id)
1437/* deletes a message from the timeline */
1438{
1439 /* delete from the user's caches */
1440 object_user_cache_del(snac, id, "public");
1441 object_user_cache_del(snac, id, "private");
1442
1443 unpin(snac, id);
1444 unbookmark(snac, id);
1445
1446 /* try to delete the object if it's not used elsewhere */
1447 return object_del_if_unref(id);
1448}
1449
1450
1451void timeline_update_indexes(snac *snac, const char *id)
1452/* updates the indexes */
1453{
1454 object_user_cache_add(snac, id, "private");
1455
1456 if (xs_startswith(id, snac->actor)) {
1457 xs *msg = NULL;
1458
1459 if (valid_status(object_get(id, &msg))) {
1460 const int scope = get_msg_visibility(msg);
1461 /* if its ours and is public, also store in public */
1462 if (scope == SCOPE_PUBLIC) {
1463 if (object_user_cache_add(snac, id, "public") >= 0) {
1464 /* also add it to the instance public timeline */
1465 xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
1466 index_add(ipt, id);
1467 }
1468 else
1469 srv_debug(1, xs_fmt("Not added to public instance index %s, visibility %d", id, scope));
1470 }
1471 else
1472 /* also add it to public, it will be discarded later */
1473 object_user_cache_add(snac, id, "public");
1474 }
1475 }
1476}
1477
1478
1479int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
1480/* adds a message to the timeline */
1481{
1482 int ret = object_add(id, o_msg);
1483 timeline_update_indexes(snac, id);
1484
1485 tag_index(id, o_msg);
1486
1487 list_distribute(snac, NULL, o_msg);
1488
1489 snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
1490
1491 return ret;
1492}
1493
1494
1495int timeline_admire(snac *snac, const char *id, const char *admirer, int like)
1496/* updates a timeline entry with a new admiration */
1497{
1498 /* if we are admiring this, add to both timelines */
1499 if (!like && strcmp(admirer, snac->actor) == 0) {
1500 object_user_cache_add(snac, id, "public");
1501 object_user_cache_add(snac, id, "private");
1502 }
1503
1504 int ret = object_admire(id, admirer, like);
1505
1506 snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
1507 like ? "Like" : "Announce", id, admirer));
1508
1509 return ret;
1510}
1511
1512
1513xs_list *timeline_top_level(snac *snac, const xs_list *list)
1514/* returns the top level md5 entries from this index */
1515{
1516 xs_set seen;
1517 const xs_str *v;
1518
1519 xs_set_init(&seen);
1520
1521 int c = 0;
1522 while (xs_list_next(list, &v, &c)) {
1523 char line[MD5_HEX_SIZE] = "";
1524
1525 strncpy(line, v, sizeof(line));
1526
1527 for (;;) {
1528 char line2[MD5_HEX_SIZE];
1529
1530 /* if it doesn't have a parent, use this */
1531 if (!object_parent(line, line2))
1532 break;
1533
1534 /* well, there is a parent... but is it here? */
1535 if (!timeline_here_by_md5(snac, line2))
1536 break;
1537
1538 /* it's here! try again with its own parent */
1539 strncpy(line, line2, sizeof(line));
1540 }
1541
1542 xs_set_add(&seen, line);
1543 }
1544
1545 return xs_set_result(&seen);
1546}
1547
1548
1549xs_str *user_index_fn(snac *user, const char *idx_name)
1550/* returns the filename of a user index */
1551{
1552 return xs_fmt("%s/%s.idx", user->basedir, idx_name);
1553}
1554
1555
1556xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more)
1557/* returns a timeline (with all entries) */
1558{
1559 xs *idx = user_index_fn(user, idx_name);
1560
1561 /* if a more flag is sent, request one more */
1562 xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0));
1563
1564 if (more != NULL) {
1565 if (xs_list_len(lst) > show) {
1566 *more = 1;
1567 lst = xs_list_del(lst, -1);
1568 }
1569 else
1570 *more = 0;
1571 }
1572
1573 return lst;
1574}
1575
1576
1577xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more)
1578/* returns a timeline (only top level entries) */
1579{
1580 int c_max;
1581
1582 /* maximum number of items in the timeline */
1583 c_max = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
1584
1585 /* never more timeline entries than the configured maximum */
1586 if (show > c_max)
1587 show = c_max;
1588
1589 xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
1590
1591 return timeline_top_level(snac, list);
1592}
1593
1594
1595void timeline_add_mark(snac *user)
1596/* adds an "already seen" mark to the private timeline */
1597{
1598 xs *fn = xs_fmt("%s/private.idx", user->basedir);
1599 char last_entry[MD5_HEX_SIZE] = "";
1600 FILE *f;
1601
1602 /* get the last entry in the index */
1603 if ((f = fopen(fn, "r")) != NULL) {
1604 index_desc_first(f, last_entry, 0);
1605 fclose(f);
1606 }
1607
1608 /* is the last entry *not* a mark? */
1609 if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) {
1610 /* add it */
1611 index_add_md5(fn, MD5_ALREADY_SEEN_MARK);
1612 }
1613}
1614
1615
1616xs_str *instance_index_fn(void)
1617{
1618 return xs_fmt("%s/public.idx", srv_basedir);
1619}
1620
1621
1622xs_list *timeline_instance_list(int skip, int show)
1623/* returns the timeline for the full instance */
1624{
1625 xs *idx = instance_index_fn();
1626 xs *lst = index_list_desc(idx, skip, show);
1627
1628 /* make the list unique */
1629 xs_set rep;
1630 xs_set_init(&rep);
1631 const char *md5;
1632
1633 xs_list_foreach(lst, md5)
1634 xs_set_add(&rep, md5);
1635
1636 return xs_set_result(&rep);
1637}
1638
1639
1640/** following **/
1641
1642/* this needs special treatment and cannot use the object db as is,
1643 with a link to a cached author, because we need the Follow object
1644 in case we need to unfollow (Undo + original Follow) */
1645
1646xs_str *_following_fn(snac *snac, const char *actor)
1647{
1648 xs *md5 = xs_md5_hex(actor, strlen(actor));
1649 return xs_fmt("%s/following/%s.json", snac->basedir, md5);
1650}
1651
1652
1653int following_add(snac *snac, const char *actor, const xs_dict *msg)
1654/* adds to the following list */
1655{
1656 int ret = HTTP_STATUS_CREATED;
1657 xs *fn = _following_fn(snac, actor);
1658 FILE *f;
1659 xs *p_object = NULL;
1660
1661 if (valid_status(following_get(snac, actor, &p_object))) {
1662 /* object already exists; if it's of type Accept,
1663 the actor is already being followed and confirmed,
1664 so do nothing */
1665 const char *type = xs_dict_get(p_object, "type");
1666
1667 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1668 snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
1669 return HTTP_STATUS_OK;
1670 }
1671 }
1672
1673 if ((f = fopen(fn, "w")) != NULL) {
1674 xs_json_dump(msg, 4, f);
1675 fclose(f);
1676
1677 /* get the filename of the actor object */
1678 xs *actor_fn = _object_fn(actor);
1679
1680 /* increase its reference count */
1681 fn = xs_replace_i(fn, ".json", "_a.json");
1682 link(actor_fn, fn);
1683 }
1684 else
1685 ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
1686
1687 snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
1688
1689 return ret;
1690}
1691
1692
1693int following_del(snac *snac, const char *actor)
1694/* we're not following this actor any longer */
1695{
1696 xs *fn = _following_fn(snac, actor);
1697
1698 snac_debug(snac, 2, xs_fmt("following_del %s %s", actor, fn));
1699
1700 unlink(fn);
1701
1702 /* also delete the reference to the author */
1703 fn = xs_replace_i(fn, ".json", "_a.json");
1704 unlink(fn);
1705
1706 return HTTP_STATUS_OK;
1707}
1708
1709
1710int following_check(snac *snac, const char *actor)
1711/* checks if we are following this actor */
1712{
1713 xs *fn = _following_fn(snac, actor);
1714
1715 return !!(mtime(fn) != 0.0);
1716}
1717
1718
1719int following_get(snac *snac, const char *actor, xs_dict **data)
1720/* returns the 'Follow' object */
1721{
1722 xs *fn = _following_fn(snac, actor);
1723 FILE *f;
1724 int status = HTTP_STATUS_OK;
1725
1726 if ((f = fopen(fn, "r")) != NULL) {
1727 *data = xs_json_load(f);
1728 fclose(f);
1729 }
1730 else
1731 status = HTTP_STATUS_NOT_FOUND;
1732
1733 return status;
1734}
1735
1736
1737int following_list_len(snac *snac)
1738/* returns number of people being followed */
1739{
1740 xs *spec = xs_fmt("%s/following/" "*_a.json", snac->basedir);
1741 xs *glist = xs_glob(spec, 0, 0);
1742 return xs_list_len(glist);
1743}
1744
1745
1746xs_list *following_list(snac *snac)
1747/* returns the list of people being followed */
1748{
1749 xs *spec = xs_fmt("%s/following/" "*.json", snac->basedir);
1750 xs *glist = xs_glob(spec, 0, 0);
1751 xs_list *p;
1752 const xs_str *v;
1753 xs_list *list = xs_list_new();
1754
1755 /* iterate the list of files */
1756 p = glist;
1757 while (xs_list_iter(&p, &v)) {
1758 FILE *f;
1759
1760 /* load the follower data */
1761 if ((f = fopen(v, "r")) != NULL) {
1762 xs *o = xs_json_load(f);
1763 fclose(f);
1764
1765 if (o != NULL) {
1766 const char *type = xs_dict_get(o, "type");
1767
1768 if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1769 const char *actor = xs_dict_get(o, "actor");
1770
1771 if (!xs_is_null(actor)) {
1772 list = xs_list_append(list, actor);
1773
1774 /* check if there is a link to the actor object */
1775 xs *v2 = xs_replace(v, ".json", "_a.json");
1776
1777 if (mtime(v2) == 0.0) {
1778 /* no; add a link to it */
1779 xs *actor_fn = _object_fn(actor);
1780 link(actor_fn, v2);
1781 }
1782 }
1783 }
1784 }
1785 }
1786 }
1787
1788 return list;
1789}
1790
1791
1792xs_str *_muted_fn(snac *snac, const char *actor)
1793{
1794 xs *md5 = xs_md5_hex(actor, strlen(actor));
1795 return xs_fmt("%s/muted/%s", snac->basedir, md5);
1796}
1797
1798
1799void mute(snac *snac, const char *actor)
1800/* mutes a moron */
1801{
1802 xs *fn = _muted_fn(snac, actor);
1803 FILE *f;
1804
1805 if ((f = fopen(fn, "w")) != NULL) {
1806 fprintf(f, "%s\n", actor);
1807 fclose(f);
1808
1809 snac_debug(snac, 2, xs_fmt("muted %s %s", actor, fn));
1810 }
1811}
1812
1813
1814void unmute(snac *snac, const char *actor)
1815/* actor is no longer a moron */
1816{
1817 xs *fn = _muted_fn(snac, actor);
1818
1819 unlink(fn);
1820
1821 snac_debug(snac, 2, xs_fmt("unmuted %s %s", actor, fn));
1822}
1823
1824
1825int is_muted(snac *snac, const char *actor)
1826/* check if someone is muted */
1827{
1828 xs *fn = _muted_fn(snac, actor);
1829
1830 return !!(mtime(fn) != 0.0);
1831}
1832
1833
1834xs_list *muted_list(snac *user)
1835/* returns the list (actor URLs) of the muted morons */
1836{
1837 xs_list *l = xs_list_new();
1838 xs *spec = xs_fmt("%s/muted/" "*", user->basedir);
1839 xs *files = xs_glob(spec, 0, 0);
1840 const char *fn;
1841
1842 xs_list_foreach(files, fn) {
1843 FILE *f;
1844
1845 if ((f = fopen(fn, "r")) != NULL) {
1846 xs *actor = xs_strip_i(xs_readline(f));
1847 fclose(f);
1848
1849 l = xs_list_append(l, actor);
1850 }
1851 }
1852
1853 return l;
1854}
1855
1856
1857/** bookmarking **/
1858
1859int is_bookmarked(snac *user, const char *id)
1860/* returns true if this note is bookmarked */
1861{
1862 return object_user_cache_in(user, id, "bookmark");
1863}
1864
1865
1866int bookmark(snac *user, const char *id)
1867/* bookmarks a post */
1868{
1869 if (is_bookmarked(user, id))
1870 return -3;
1871
1872 return object_user_cache_add(user, id, "bookmark");
1873}
1874
1875
1876int unbookmark(snac *user, const char *id)
1877/* unbookmarks a post */
1878{
1879 return object_user_cache_del(user, id, "bookmark");
1880}
1881
1882
1883xs_list *bookmark_list(snac *user)
1884/* return the lists of bookmarked posts */
1885{
1886 return object_user_cache_list(user, "bookmark", XS_ALL, 1);
1887}
1888
1889
1890xs_str *bookmark_index_fn(snac *user)
1891{
1892 return object_user_cache_index_fn(user, "bookmark");
1893}
1894
1895
1896/** pinning **/
1897
1898int is_pinned(snac *user, const char *id)
1899/* returns true if this note is pinned */
1900{
1901 return object_user_cache_in(user, id, "pinned");
1902}
1903
1904
1905int is_pinned_by_md5(snac *user, const char *md5)
1906{
1907 return object_user_cache_in_by_md5(user, md5, "pinned");
1908}
1909
1910
1911int pin(snac *user, const char *id)
1912/* pins a message */
1913{
1914 int ret = -2;
1915
1916 if (xs_startswith(id, user->actor)) {
1917 if (is_pinned(user, id))
1918 ret = -3;
1919 else
1920 ret = object_user_cache_add(user, id, "pinned");
1921 }
1922
1923 return ret;
1924}
1925
1926
1927int unpin(snac *user, const char *id)
1928/* unpin a message */
1929{
1930 return object_user_cache_del(user, id, "pinned");
1931}
1932
1933
1934xs_list *pinned_list(snac *user)
1935/* return the lists of pinned posts */
1936{
1937 return object_user_cache_list(user, "pinned", XS_ALL, 1);
1938}
1939
1940
1941/** drafts **/
1942
1943int is_draft(snac *user, const char *id)
1944/* returns true if this note is a draft */
1945{
1946 return object_user_cache_in(user, id, "draft");
1947}
1948
1949
1950void draft_del(snac *user, const char *id)
1951/* delete a message from the draft cache */
1952{
1953 object_user_cache_del(user, id, "draft");
1954}
1955
1956
1957void draft_add(snac *user, const char *id, const xs_dict *msg)
1958/* store the message as a draft */
1959{
1960 /* delete from the index, in case it was already there */
1961 draft_del(user, id);
1962
1963 /* overwrite object */
1964 object_add_ow(id, msg);
1965
1966 /* [re]add to the index */
1967 object_user_cache_add(user, id, "draft");
1968}
1969
1970
1971xs_list *draft_list(snac *user)
1972/* return the lists of drafts */
1973{
1974 return object_user_cache_list(user, "draft", XS_ALL, 1);
1975}
1976
1977
1978/** scheduled posts **/
1979
1980int is_scheduled(snac *user, const char *id)
1981/* returns true if this note is scheduled for future sending */
1982{
1983 return object_user_cache_in(user, id, "sched");
1984}
1985
1986
1987void schedule_del(snac *user, const char *id)
1988/* deletes an scheduled post */
1989{
1990 object_user_cache_del(user, id, "sched");
1991}
1992
1993
1994void schedule_add(snac *user, const char *id, const xs_dict *msg)
1995/* schedules this post for later */
1996{
1997 /* delete from the index, in case it was already there */
1998 schedule_del(user, id);
1999
2000 /* overwrite object */
2001 object_add_ow(id, msg);
2002
2003 /* [re]add to the index */
2004 object_user_cache_add(user, id, "sched");
2005}
2006
2007
2008xs_list *scheduled_list(snac *user)
2009/* return the list of scheduled posts */
2010{
2011 return object_user_cache_list(user, "sched", XS_ALL, 1);
2012}
2013
2014
2015void scheduled_process(snac *user)
2016/* processes the scheduled list, sending those ready to be sent */
2017{
2018 xs *posts = scheduled_list(user);
2019 const char *md5;
2020 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
2021
2022 xs_list_foreach(posts, md5) {
2023 xs *msg = NULL;
2024
2025 if (valid_status(object_get_by_md5(md5, &msg))) {
2026 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
2027 /* due date! */
2028 const char *id = xs_dict_get(msg, "id");
2029
2030 timeline_add(user, id, msg);
2031
2032 xs *c_msg = msg_create(user, msg);
2033 enqueue_message(user, c_msg);
2034
2035 schedule_del(user, id);
2036 }
2037 }
2038 }
2039}
2040
2041
2042/** hiding **/
2043
2044xs_str *_hidden_fn(snac *snac, const char *id)
2045{
2046 xs *md5 = xs_md5_hex(id, strlen(id));
2047 return xs_fmt("%s/hidden/%s", snac->basedir, md5);
2048}
2049
2050
2051void hide(snac *snac, const char *id)
2052/* hides an object and its children (if it's a post) */
2053{
2054 xs *fn = _hidden_fn(snac, id);
2055 FILE *f;
2056
2057 if ((f = fopen(fn, "w")) != NULL) {
2058 fprintf(f, "%s\n", id);
2059 fclose(f);
2060
2061 snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
2062
2063 /* hide all the children */
2064 xs *chld = object_children(id);
2065 char *p;
2066 const char *v;
2067
2068 p = chld;
2069 while (xs_list_iter(&p, &v)) {
2070 xs *co = NULL;
2071
2072 /* resolve to get the id */
2073 if (valid_status(object_get_by_md5(v, &co))) {
2074 const char *id = xs_dict_get(co, "id");
2075 if (id != NULL)
2076 hide(snac, id);
2077 }
2078 }
2079 }
2080}
2081
2082
2083int is_hidden(snac *snac, const char *id)
2084/* check is id is hidden */
2085{
2086 xs *fn = _hidden_fn(snac, id);
2087
2088 return !!(mtime(fn) != 0.0);
2089}
2090
2091
2092int unhide(snac *user, const char *id)
2093/* unhides an object */
2094{
2095 xs *fn = _hidden_fn(user, id);
2096 return unlink(fn);
2097}
2098
2099
2100int actor_add(const char *actor, const xs_dict *msg)
2101/* adds an actor */
2102{
2103 return object_add_ow(actor, msg);
2104}
2105
2106
2107int actor_get(const char *actor, xs_dict **data)
2108/* returns an already downloaded actor */
2109{
2110 int status = HTTP_STATUS_OK;
2111 xs_dict *d = NULL;
2112
2113 if (xs_startswith(actor, srv_baseurl)) {
2114 /* it's a (possible) local user */
2115 xs *l = xs_split(actor, "/");
2116 const char *uid = xs_list_get(l, -1);
2117 snac user;
2118
2119 if (!xs_is_null(uid) && user_open(&user, uid)) {
2120 if (data)
2121 *data = msg_actor(&user);
2122
2123 user_free(&user);
2124 return HTTP_STATUS_OK;
2125 }
2126 else
2127 return HTTP_STATUS_NOT_FOUND;
2128 }
2129
2130 /* read the object */
2131 if (!valid_status(status = object_get(actor, &d))) {
2132 d = xs_free(d);
2133 return status;
2134 }
2135
2136 /* if the object is corrupted, discard it */
2137 if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
2138 srv_debug(1, xs_fmt("corrupted actor object %s", actor));
2139 d = xs_free(d);
2140 return HTTP_STATUS_NOT_FOUND;
2141 }
2142
2143 if (data)
2144 *data = d;
2145 else
2146 d = xs_free(d);
2147
2148 xs *fn = _object_fn(actor);
2149 double max_time;
2150
2151 /* maximum time for the actor data to be considered stale */
2152 max_time = 3600.0 * 36.0;
2153
2154 if (mtime(fn) + max_time < (double) time(NULL)) {
2155 /* actor data exists but also stinks */
2156 status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
2157 }
2158
2159 return status;
2160}
2161
2162
2163int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
2164/* gets an actor and requests a refresh if it's stale */
2165{
2166 int status = actor_get(actor, data);
2167
2168 if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
2169 enqueue_actor_refresh(user, actor, 0);
2170
2171 return status;
2172}
2173
2174
2175/** user limiting (announce blocks) **/
2176
2177int limited(snac *user, const char *id, int cmd)
2178/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
2179{
2180 int ret = 0;
2181 xs *dir = xs_fmt("%s/limited", user->basedir);
2182 xs *md5 = xs_md5_hex(id, strlen(id));
2183 xs *fn = xs_fmt("%s/%s", dir, md5);
2184
2185 switch (cmd) {
2186 case 0: /** check **/
2187 ret = !!(mtime(fn) > 0.0);
2188 break;
2189
2190 case 1: /** limit **/
2191 mkdirx(dir);
2192
2193 if (mtime(fn) > 0.0)
2194 ret = -1;
2195 else {
2196 FILE *f;
2197
2198 if ((f = fopen(fn, "w")) != NULL) {
2199 fprintf(f, "%s\n", id);
2200 fclose(f);
2201 }
2202 else
2203 ret = -2;
2204 }
2205 break;
2206
2207 case 2: /** unlimit **/
2208 if (mtime(fn) > 0.0)
2209 ret = unlink(fn);
2210 else
2211 ret = -1;
2212 break;
2213 }
2214
2215 return ret;
2216}
2217
2218
2219/** tag indexing **/
2220
2221void tag_index(const char *id, const xs_dict *obj)
2222/* update the tag indexes for this object */
2223{
2224 const xs_list *tags = xs_dict_get(obj, "tag");
2225 xs *md5_id = xs_md5_hex(id, strlen(id));
2226
2227 if (get_msg_visibility(obj) != SCOPE_PUBLIC)
2228 return;
2229
2230 if (xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
2231 xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
2232
2233 mkdirx(g_tag_dir);
2234
2235 const xs_dict *v;
2236 int ct = 0;
2237 while (xs_list_next(tags, &v, &ct)) {
2238 const char *type = xs_dict_get(v, "type");
2239 const char *name = xs_dict_get(v, "name");
2240
2241 if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
2242 while (*name == '#' || *name == '@')
2243 name++;
2244
2245 if (*name == '\0')
2246 continue;
2247
2248 name = xs_utf8_to_lower((xs_str *)name);
2249
2250 xs *md5_tag = xs_md5_hex(name, strlen(name));
2251 xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
2252 mkdirx(tag_dir);
2253
2254 xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
2255
2256 if (!index_in_md5(g_tag_idx, md5_id))
2257 index_add_md5(g_tag_idx, md5_id);
2258
2259 FILE *f;
2260 xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
2261 if ((f = fopen(g_tag_name, "w")) != NULL) {
2262 fprintf(f, "%s\n", name);
2263 fclose(f);
2264 }
2265
2266 srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
2267 }
2268 }
2269 }
2270}
2271
2272
2273xs_str *tag_fn(const char *tag)
2274{
2275 if (*tag == '#')
2276 tag++;
2277
2278 xs *lw_tag = xs_utf8_to_lower(tag);
2279 xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag));
2280
2281 return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
2282}
2283
2284
2285xs_list *tag_search(const char *tag, int skip, int show)
2286/* returns the list of posts tagged with tag */
2287{
2288 xs *idx = tag_fn(tag);
2289
2290 return index_list_desc(idx, skip, show);
2291}
2292
2293
2294/** lists **/
2295
2296xs_val *list_maint(snac *user, const char *list, int op)
2297/* list maintenance */
2298{
2299 xs_val *l = NULL;
2300
2301 switch (op) {
2302 case 0: /** list of lists **/
2303 {
2304 FILE *f;
2305 xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
2306 xs *ls = xs_glob(spec, 0, 0);
2307 int c = 0;
2308 const char *v;
2309
2310 l = xs_list_new();
2311
2312 while (xs_list_next(ls, &v, &c)) {
2313 if ((f = fopen(v, "r")) != NULL) {
2314 xs *title = xs_readline(f);
2315 fclose(f);
2316
2317 title = xs_strip_i(title);
2318
2319 xs *v2 = xs_replace(v, ".id", "");
2320 xs *l2 = xs_split(v2, "/");
2321
2322 /* return [ list_id, list_title ] */
2323 xs *tmp_list = xs_list_append(xs_list_new(), xs_list_get(l2, -1), title);
2324 l = xs_list_append(l, tmp_list);
2325 }
2326 }
2327 }
2328
2329 break;
2330
2331 case 1: /** create new list (list is the name) **/
2332 {
2333 xs *lol = list_maint(user, NULL, 0);
2334 int c = 0;
2335 const xs_list *v;
2336 int add = 1;
2337
2338 /* check if this list name already exists */
2339 while (xs_list_next(lol, &v, &c)) {
2340 if (strcmp(xs_list_get(v, 1), list) == 0) {
2341 add = 0;
2342
2343 l = xs_dup(xs_list_get(v, 0));
2344
2345 break;
2346 }
2347 }
2348
2349 if (add) {
2350 FILE *f;
2351 xs *dir = xs_fmt("%s/list/", user->basedir);
2352
2353 l = xs_fmt("%010x", time(NULL));
2354
2355 mkdirx(dir);
2356
2357 xs *fn = xs_fmt("%s%s.id", dir, l);
2358
2359 if ((f = fopen(fn, "w")) != NULL) {
2360 fprintf(f, "%s\n", list);
2361 fclose(f);
2362 }
2363 }
2364 }
2365
2366 break;
2367
2368 case 2: /** delete list (list is the id) **/
2369 {
2370 if (xs_is_hex(list)) {
2371 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2372 unlink(fn);
2373
2374 fn = xs_replace_i(fn, ".id", ".lst");
2375 unlink(fn);
2376
2377 fn = xs_replace_i(fn, ".lst", ".idx");
2378 unlink(fn);
2379
2380 fn = xs_str_cat(fn, ".bak");
2381 unlink(fn);
2382 }
2383 }
2384
2385 break;
2386
2387 case 3: /** get list name **/
2388 if (xs_is_hex(list)) {
2389 FILE *f;
2390 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2391
2392 if ((f = fopen(fn, "r")) != NULL) {
2393 l = xs_strip_i(xs_readline(f));
2394 fclose(f);
2395 }
2396 }
2397
2398 break;
2399
2400 case 4: /** find list id by name **/
2401 if (xs_is_string(list)) {
2402 xs *lol = list_maint(user, NULL, 0);
2403 const xs_list *li;
2404
2405 xs_list_foreach(lol, li) {
2406 if (strcmp(list, xs_list_get(li, 1)) == 0) {
2407 l = xs_dup(xs_list_get(li, 0));
2408 break;
2409 }
2410 }
2411 }
2412 }
2413
2414 return l;
2415}
2416
2417
2418xs_str *list_timeline_fn(snac *user, const char *list)
2419{
2420 return xs_fmt("%s/list/%s.idx", user->basedir, list);
2421}
2422
2423
2424xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2425/* returns the timeline of a list */
2426{
2427 xs_list *l = NULL;
2428
2429 if (!xs_is_hex(list))
2430 return NULL;
2431
2432 xs *fn = list_timeline_fn(user, list);
2433
2434 if (mtime(fn) > 0.0)
2435 l = index_list_desc(fn, skip, show);
2436 else
2437 l = xs_list_new();
2438
2439 return l;
2440}
2441
2442
2443xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
2444/* list member management */
2445{
2446 xs_val *l = NULL;
2447
2448 if (!xs_is_hex(list))
2449 return NULL;
2450
2451 if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2452 return NULL;
2453
2454 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2455
2456 switch (op) {
2457 case 0: /** list members **/
2458 l = index_list(fn, XS_ALL);
2459
2460 break;
2461
2462 case 1: /** append actor to list **/
2463 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) {
2464 if (!index_in_md5(fn, actor_md5))
2465 index_add_md5(fn, actor_md5);
2466 }
2467
2468 break;
2469
2470 case 2: /** delete actor from list **/
2471 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5))
2472 index_del_md5(fn, actor_md5);
2473
2474 break;
2475
2476 default:
2477 srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2478 break;
2479 }
2480
2481 return l;
2482}
2483
2484
2485void list_distribute(snac *user, const char *who, const xs_dict *post)
2486/* distributes the post to all appropriate lists */
2487{
2488 const char *id = xs_dict_get(post, "id");
2489
2490 /* if who is not set, use the attributedTo in the message */
2491 if (xs_is_null(who))
2492 who = get_atto(post);
2493
2494 if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2495 xs *a_md5 = xs_md5_hex(who, strlen(who));
2496 xs *i_md5 = xs_md5_hex(id, strlen(id));
2497 xs *spec = xs_fmt("%s/list/" "*.lst", user->basedir);
2498 xs *ls = xs_glob(spec, 0, 0);
2499 int c = 0;
2500 const char *v;
2501
2502 while (xs_list_next(ls, &v, &c)) {
2503 /* is the actor in this list? */
2504 if (index_in_md5(v, a_md5)) {
2505 /* it is; add post md5 to its timeline */
2506 xs *idx = xs_replace(v, ".lst", ".idx");
2507 index_add_md5(idx, i_md5);
2508
2509 snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2510 }
2511 }
2512 }
2513}
2514
2515
2516/** static data **/
2517
2518static int _load_raw_file(const char *fn, xs_val **data, int *size,
2519 const char *inm, xs_str **etag)
2520/* loads a cached file */
2521{
2522 int status = HTTP_STATUS_NOT_FOUND;
2523
2524 if (fn) {
2525 double tm = mtime(fn);
2526
2527 if (tm > 0.0) {
2528 /* file exists; build the etag */
2529 xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2530
2531 /* if if-none-match is set, check if it's the same */
2532 if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2533 /* client has the newest version */
2534 status = HTTP_STATUS_NOT_MODIFIED;
2535 }
2536 else {
2537 /* newer or never downloaded; read the full file */
2538 FILE *f;
2539
2540 if ((f = fopen(fn, "rb")) != NULL) {
2541 *size = XS_ALL;
2542 *data = xs_read(f, size);
2543 fclose(f);
2544
2545 status = HTTP_STATUS_OK;
2546 }
2547 }
2548
2549 /* if caller wants the etag, return it */
2550 if (etag != NULL)
2551 *etag = xs_dup(e);
2552
2553 srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2554 }
2555 }
2556
2557 return status;
2558}
2559
2560
2561xs_str *_static_fn(snac *snac, const char *id)
2562/* gets the filename for a static file */
2563{
2564 if (strchr(id, '/'))
2565 return NULL;
2566 else
2567 return xs_fmt("%s/static/%s", snac->basedir, id);
2568}
2569
2570
2571int static_get(snac *snac, const char *id, xs_val **data, int *size,
2572 const char *inm, xs_str **etag)
2573/* returns static content */
2574{
2575 xs *fn = _static_fn(snac, id);
2576
2577 return _load_raw_file(fn, data, size, inm, etag);
2578}
2579
2580
2581void static_put(snac *snac, const char *id, const char *data, int size)
2582/* writes status content */
2583{
2584 xs *fn = _static_fn(snac, id);
2585 FILE *f;
2586
2587 if (fn && (f = fopen(fn, "wb")) != NULL) {
2588 fwrite(data, size, 1, f);
2589 fclose(f);
2590 }
2591}
2592
2593
2594void static_put_meta(snac *snac, const char *id, const char *str)
2595/* puts metadata (i.e. a media description string) to id */
2596{
2597 xs *fn = _static_fn(snac, id);
2598
2599 if (fn) {
2600 fn = xs_str_cat(fn, ".txt");
2601 FILE *f;
2602
2603 if ((f = fopen(fn, "w")) != NULL) {
2604 fprintf(f, "%s\n", str);
2605 fclose(f);
2606 }
2607 }
2608}
2609
2610
2611xs_str *static_get_meta(snac *snac, const char *id)
2612/* gets metadata from a media */
2613{
2614 xs *fn = _static_fn(snac, id);
2615 xs_str *r = NULL;
2616
2617 if (fn) {
2618 fn = xs_str_cat(fn, ".txt");
2619 FILE *f;
2620
2621 if ((f = fopen(fn, "r")) != NULL) {
2622 r = xs_strip_i(xs_readline(f));
2623 fclose(f);
2624 }
2625 }
2626 else
2627 r = xs_str_new("");
2628
2629 return r;
2630}
2631
2632
2633/** history **/
2634
2635xs_str *_history_fn(snac *snac, const char *id)
2636/* gets the filename for the history */
2637{
2638 if (strchr(id, '/'))
2639 return NULL;
2640 else
2641 return xs_fmt("%s/history/%s", snac->basedir, id);
2642}
2643
2644
2645double history_mtime(snac *snac, const char *id)
2646{
2647 double t = 0.0;
2648 xs *fn = _history_fn(snac, id);
2649
2650 if (fn != NULL)
2651 t = mtime(fn);
2652
2653 return t;
2654}
2655
2656
2657void history_add(snac *snac, const char *id, const char *content, int size,
2658 xs_str **etag)
2659/* adds something to the history */
2660{
2661 xs *fn = _history_fn(snac, id);
2662 FILE *f;
2663
2664 if (fn && (f = fopen(fn, "w")) != NULL) {
2665 fwrite(content, size, 1, f);
2666 fclose(f);
2667
2668 if (etag) {
2669 double tm = mtime(fn);
2670 *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2671 }
2672 }
2673}
2674
2675
2676int history_get(snac *snac, const char *id, xs_str **content, int *size,
2677 const char *inm, xs_str **etag)
2678{
2679 xs *fn = _history_fn(snac, id);
2680
2681 return _load_raw_file(fn, content, size, inm, etag);
2682}
2683
2684
2685int history_del(snac *snac, const char *id)
2686{
2687 xs *fn = _history_fn(snac, id);
2688
2689 if (fn)
2690 return unlink(fn);
2691 else
2692 return -1;
2693}
2694
2695
2696xs_list *history_list(snac *snac)
2697{
2698 xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2699
2700 return xs_glob(spec, 1, 1);
2701}
2702
2703
2704void lastlog_write(snac *snac, const char *source)
2705/* writes the last time the user logged in */
2706{
2707 xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2708 FILE *f;
2709
2710 if ((f = fopen(fn, "w")) != NULL) {
2711 fprintf(f, "%lf %s\n", ftime(), source);
2712 fclose(f);
2713 }
2714}
2715
2716
2717/** inbox collection **/
2718
2719void inbox_add(const char *inbox)
2720/* collects a shared inbox */
2721{
2722 /* don't collect ourselves */
2723 if (xs_startswith(inbox, srv_baseurl))
2724 return;
2725
2726 xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2727 xs *fn = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2728 FILE *f;
2729
2730 if ((f = fopen(fn, "w")) != NULL) {
2731 fprintf(f, "%s\n", inbox);
2732 fclose(f);
2733 }
2734}
2735
2736
2737void inbox_add_by_actor(const xs_dict *actor)
2738/* collects an actor's shared inbox, if it has one */
2739{
2740 const char *v;
2741
2742 if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2743 !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2744 /* only collect this inbox if its instance is not blocked */
2745 if (!is_instance_blocked(v))
2746 inbox_add(v);
2747 }
2748}
2749
2750
2751xs_list *inbox_list(void)
2752/* returns the collected inboxes as a list */
2753{
2754 xs_list *ibl = xs_list_new();
2755 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2756 xs *files = xs_glob(spec, 0, 0);
2757 const xs_val *v;
2758
2759 xs_list_foreach(files, v) {
2760 FILE *f;
2761
2762 if ((f = fopen(v, "r")) != NULL) {
2763 xs *line = xs_readline(f);
2764
2765 if (line && *line) {
2766 line = xs_strip_i(line);
2767
2768 if (!is_instance_blocked(line))
2769 ibl = xs_list_append(ibl, line);
2770 }
2771
2772 fclose(f);
2773 }
2774 }
2775
2776 return ibl;
2777}
2778
2779
2780/** instance-wide operations **/
2781
2782xs_str *_instance_block_fn(const char *instance)
2783{
2784 xs *s = xs_replace(instance, "http:/" "/", "");
2785 xs *s1 = xs_replace(s, "https:/" "/", "");
2786 xs *l = xs_split(s1, "/");
2787 const char *p = xs_list_get(l, 0);
2788 xs *md5 = xs_md5_hex(p, strlen(p));
2789
2790 return xs_fmt("%s/block/%s", srv_basedir, md5);
2791}
2792
2793
2794int is_instance_blocked(const char *instance)
2795{
2796 xs *fn = _instance_block_fn(instance);
2797
2798 return !!(mtime(fn) != 0.0);
2799}
2800
2801
2802int instance_block(const char *instance)
2803/* blocks a full instance */
2804{
2805 int ret;
2806
2807 /* create the subdir */
2808 xs *dir = xs_fmt("%s/block/", srv_basedir);
2809 mkdirx(dir);
2810
2811 if (!is_instance_blocked(instance)) {
2812 xs *fn = _instance_block_fn(instance);
2813 FILE *f;
2814
2815 if ((f = fopen(fn, "w")) != NULL) {
2816 fprintf(f, "%s\n", instance);
2817 fclose(f);
2818
2819 ret = 0;
2820 }
2821 else
2822 ret = -1;
2823 }
2824 else
2825 ret = -2;
2826
2827 return ret;
2828}
2829
2830
2831int instance_unblock(const char *instance)
2832/* unblocks a full instance */
2833{
2834 int ret;
2835
2836 if (is_instance_blocked(instance)) {
2837 xs *fn = _instance_block_fn(instance);
2838 ret = unlink(fn);
2839 }
2840 else
2841 ret = -2;
2842
2843 return ret;
2844}
2845
2846
2847/** operations by content **/
2848
2849int content_match(const char *file, const xs_dict *msg)
2850/* checks if a message's content matches any of the regexes in file */
2851/* file format: one regex per line */
2852{
2853 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2854 FILE *f;
2855 int r = 0;
2856 const char *v = xs_dict_get(msg, "content");
2857
2858 if (xs_type(v) == XSTYPE_STRING && *v) {
2859 if ((f = fopen(fn, "r")) != NULL) {
2860 srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2861
2862 /* massage content (strip HTML tags, etc.) */
2863 xs *c1 = xs_regex_replace(v, "<[^>]+>", " ");
2864 c1 = xs_regex_replace_i(c1, " {2,}", " ");
2865 xs *c = xs_utf8_to_lower(c1);
2866
2867 while (!r && !feof(f)) {
2868 xs *rx = xs_strip_i(xs_readline(f));
2869
2870 if (*rx && xs_regex_match(c, rx)) {
2871 srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2872 r = 1;
2873 }
2874 }
2875
2876 fclose(f);
2877 }
2878 }
2879
2880 return r;
2881}
2882
2883
2884xs_list *content_search(snac *user, const char *regex,
2885 int priv, int skip, int show, int max_secs, int *timeout)
2886/* returns a list of posts which content matches the regex */
2887{
2888 if (regex == NULL || *regex == '\0')
2889 return xs_list_new();
2890
2891 xs *i_regex = xs_utf8_to_lower(regex);
2892
2893 xs_set seen;
2894
2895 xs_set_init(&seen);
2896
2897 if (max_secs == 0)
2898 max_secs = 3;
2899
2900 time_t t = time(NULL) + max_secs;
2901 *timeout = 0;
2902
2903 /* iterate all timelines simultaneously */
2904 xs_list *tls[3] = {0};
2905 const char *md5s[3] = {0};
2906 int c[3] = {0};
2907
2908 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */
2909 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2910 tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
2911
2912 /* first positioning */
2913 for (int n = 0; n < 3; n++)
2914 xs_list_next(tls[n], &md5s[n], &c[n]);
2915
2916 show += skip;
2917
2918 while (show > 0) {
2919 /* timeout? */
2920 if (time(NULL) > t) {
2921 *timeout = 1;
2922 break;
2923 }
2924
2925 /* find the newest post */
2926 int newest = -1;
2927 double mtime = 0.0;
2928
2929 for (int n = 0; n < 3; n++) {
2930 if (md5s[n] != NULL) {
2931 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2932 double mt;
2933
2934 while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
2935 /* object is not here: move to the next one */
2936 if (xs_list_next(tls[n], &md5s[n], &c[n])) {
2937 xs_free(fn);
2938 fn = _object_fn_by_md5(md5s[n], "content_search_2");
2939 }
2940 else
2941 md5s[n] = NULL;
2942 }
2943
2944 if (mt > mtime) {
2945 newest = n;
2946 mtime = mt;
2947 }
2948 }
2949 }
2950
2951 if (newest == -1)
2952 break;
2953
2954 const char *md5 = md5s[newest];
2955
2956 /* advance the chosen timeline */
2957 if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2958 md5s[newest] = NULL;
2959
2960 xs *post = NULL;
2961
2962 if (!valid_status(object_get_by_md5(md5, &post)))
2963 continue;
2964
2965 if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2966 continue;
2967
2968 const char *id = xs_dict_get(post, "id");
2969
2970 if (id == NULL || is_hidden(user, id))
2971 continue;
2972
2973 /* recalculate the md5 id to be sure it's not repeated
2974 (it may have been searched by the "url" field instead of "id") */
2975 xs *new_md5 = xs_md5_hex(id, strlen(id));
2976 md5 = new_md5;
2977
2978 /* test for the post URL */
2979 if (strcmp(id, regex) == 0) {
2980 if (xs_set_add(&seen, md5) == 1)
2981 show--;
2982
2983 continue;
2984 }
2985
2986 /* test for the alternate post id */
2987 const char *url = xs_dict_get(post, "url");
2988 if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
2989 if (xs_set_add(&seen, md5) == 1)
2990 show--;
2991
2992 continue;
2993 }
2994
2995 xs *c = xs_str_new(NULL);
2996 const char *content = xs_dict_get(post, "content");
2997 const char *name = xs_dict_get(post, "name");
2998 const char *atto = get_atto(post);
2999
3000 if (!xs_is_null(content))
3001 c = xs_str_cat(c, content);
3002 if (!xs_is_null(name))
3003 c = xs_str_cat(c, " ", name);
3004 if (!xs_is_null(atto))
3005 c = xs_str_cat(c, " ", atto);
3006
3007 /* add alt-texts from attachments */
3008 const xs_list *atts = xs_dict_get(post, "attachment");
3009 int tc = 0;
3010 const xs_dict *att;
3011
3012 while (xs_list_next(atts, &att, &tc)) {
3013 const char *name = xs_dict_get(att, "name");
3014
3015 if (name != NULL)
3016 c = xs_str_cat(c, " ", name);
3017 }
3018
3019 /* strip HTML */
3020 c = xs_regex_replace_i(c, "<[^>]+>", " ");
3021 c = xs_regex_replace_i(c, " {2,}", " ");
3022
3023 /* convert to lowercase */
3024 xs *lc = xs_utf8_to_lower(c);
3025
3026 /* apply regex */
3027 if (xs_regex_match(lc, i_regex)) {
3028 if (xs_set_add(&seen, md5) == 1)
3029 show--;
3030 }
3031 }
3032
3033 xs_list *r = xs_set_result(&seen);
3034
3035 if (skip) {
3036 /* BAD */
3037 while (skip--) {
3038 r = xs_list_del(r, 0);
3039 }
3040 }
3041
3042 xs_free(tls[0]);
3043 xs_free(tls[1]);
3044 xs_free(tls[2]);
3045
3046 return r;
3047}
3048
3049
3050int actor_failure(const char *actor, int op)
3051/* actor failure maintenance */
3052{
3053 int ret = 0;
3054
3055 xs *md5 = xs_md5_hex(actor, strlen(actor));
3056 xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3057
3058 switch (op) {
3059 case 0: /** check **/
3060 if (mtime(fn))
3061 ret = -1;
3062
3063 break;
3064
3065 case 1: /** register a failure **/
3066 if (mtime(fn) == 0.0) {
3067 FILE *f;
3068
3069 /* only create once, as the date will be used */
3070 if ((f = fopen(fn, "w")) != NULL) {
3071 fprintf(f, "%s\n", actor);
3072 fclose(f);
3073 }
3074 }
3075
3076 break;
3077
3078 case 2: /** clear a failure **/
3079 /* called whenever a message comes from this instance */
3080 unlink(fn);
3081
3082 break;
3083 }
3084
3085 return ret;
3086}
3087
3088
3089int instance_failure(const char *url, int op)
3090/* do some checks and accounting on instance failures */
3091{
3092 int ret = 0;
3093 xs *l = xs_split(url, "/");
3094 const char *hostname = xs_list_get(l, 2);
3095 double mt;
3096
3097 if (!xs_is_string(hostname))
3098 return 0;
3099
3100 xs *md5 = xs_md5_hex(hostname, strlen(hostname));
3101 xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3102
3103 switch (op) {
3104 case 0: /** check **/
3105 if ((mt = mtime(fn)) != 0.0) {
3106 /* grace time */
3107 double seconds_failing = xs_number_get(xs_dict_get_def(srv_config, "max_failing_days", "15"))
3108 * (24 * 60 * 60);
3109
3110 if ((double)time(NULL) - mt > seconds_failing)
3111 ret = -1;
3112 }
3113
3114 break;
3115
3116 case 1: /** register a failure **/
3117 if (mtime(fn) == 0.0) {
3118 FILE *f;
3119
3120 /* only create once, as the date will be used */
3121 if ((f = fopen(fn, "w")) != NULL) {
3122 fprintf(f, "%s\n", hostname);
3123 fclose(f);
3124 }
3125 }
3126
3127 break;
3128
3129 case 2: /** clear a failure **/
3130 /* called whenever a message comes from this instance */
3131 unlink(fn);
3132
3133 break;
3134 }
3135
3136 return ret;
3137}
3138
3139
3140int grave(const char *objid, int op)
3141/* the graveyeard of deleted objects */
3142{
3143 int ret = 0;
3144 xs *dir = xs_fmt("%s/grave", srv_basedir);
3145 xs *md5 = xs_md5_hex(objid, strlen(objid));
3146 xs *fn = xs_fmt("%s/%s", dir, md5);
3147 FILE *f;
3148
3149 switch (op) {
3150 case 0: /** check **/
3151 ret = mtime(fn) > 0.0 ? 1 : 0;
3152 break;
3153
3154 case 1: /** add **/
3155 mkdirx(dir);
3156
3157 if ((f = fopen(fn, "w")) != NULL) {
3158 fprintf(f, "%s\n", objid);
3159 fclose(f);
3160 }
3161
3162 break;
3163 }
3164
3165 return ret;
3166}
3167
3168
3169/** notifications **/
3170
3171xs_str *notify_check_time(snac *snac, int reset)
3172/* gets or resets the latest notification check time */
3173{
3174 xs_str *t = NULL;
3175 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
3176 FILE *f;
3177
3178 if (reset) {
3179 if ((f = fopen(fn, "w")) != NULL) {
3180 t = tid(0);
3181 fprintf(f, "%s\n", t);
3182 fclose(f);
3183 }
3184 }
3185 else {
3186 if ((f = fopen(fn, "r")) != NULL) {
3187 t = xs_readline(f);
3188 fclose(f);
3189 }
3190 else
3191 /* never set before */
3192 t = xs_fmt("%16.6f", 0.0);
3193 }
3194
3195 return t;
3196}
3197
3198xs_dict *markers_get(snac *snac, const xs_list *markers)
3199{
3200 xs *data = NULL;
3201 xs_dict *returns = xs_dict_new();
3202 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3203 const xs_str *v = NULL;
3204 FILE *f;
3205
3206 if ((f = fopen(fn, "r")) != NULL) {
3207 data = xs_json_load(f);
3208 fclose(f);
3209 }
3210
3211 if (xs_is_null(data))
3212 data = xs_dict_new();
3213
3214 xs_list_foreach(markers, v) {
3215 const xs_dict *mark = xs_dict_get(data, v);
3216 if (!xs_is_null(mark)) {
3217 returns = xs_dict_append(returns, v, mark);
3218 }
3219 }
3220 return returns;
3221}
3222
3223xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
3224/* gets or sets notification marker */
3225{
3226 xs *data = NULL;
3227 xs_dict *written = xs_dict_new();
3228 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3229 FILE *f;
3230
3231 if ((f = fopen(fn, "r")) != NULL) {
3232 data = xs_json_load(f);
3233 fclose(f);
3234 }
3235
3236 if (xs_is_null(data))
3237 data = xs_dict_new();
3238
3239 if (!xs_is_null(home_marker)) {
3240 xs *home = xs_dict_new();
3241 xs *s_tid = tid(0);
3242 home = xs_dict_append(home, "last_read_id", home_marker);
3243 home = xs_dict_append(home, "version", xs_stock(0));
3244 home = xs_dict_append(home, "updated_at", s_tid);
3245 data = xs_dict_set(data, "home", home);
3246 written = xs_dict_append(written, "home", home);
3247 }
3248
3249 if (!xs_is_null(notify_marker)) {
3250 xs *notify = xs_dict_new();
3251 xs *s_tid = tid(0);
3252 notify = xs_dict_append(notify, "last_read_id", notify_marker);
3253 notify = xs_dict_append(notify, "version", xs_stock(0));
3254 notify = xs_dict_append(notify, "updated_at", s_tid);
3255 data = xs_dict_set(data, "notifications", notify);
3256 written = xs_dict_append(written, "notifications", notify);
3257 }
3258
3259 if ((f = fopen(fn, "w")) != NULL) {
3260 xs_json_dump(data, 4, f);
3261 fclose(f);
3262 }
3263
3264 return written;
3265}
3266
3267void notify_add(snac *snac, const char *type, const char *utype,
3268 const char *actor, const char *objid, const xs_dict *msg)
3269/* adds a new notification */
3270{
3271 xs *ntid = tid(0);
3272 xs *fn = xs_fmt("%s/notify/", snac->basedir);
3273 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3274 FILE *f;
3275
3276 /* create the directory */
3277 mkdirx(fn);
3278 fn = xs_str_cat(fn, ntid);
3279 fn = xs_str_cat(fn, ".json");
3280
3281 xs *noti = xs_dict_new();
3282
3283 noti = xs_dict_append(noti, "id", ntid);
3284 noti = xs_dict_append(noti, "type", type);
3285 noti = xs_dict_append(noti, "utype", utype);
3286 noti = xs_dict_append(noti, "actor", actor);
3287 noti = xs_dict_append(noti, "date", date);
3288 noti = xs_dict_append(noti, "msg", msg);
3289
3290 if (!xs_is_null(objid))
3291 noti = xs_dict_append(noti, "objid", objid);
3292
3293 if ((f = fopen(fn, "w")) != NULL) {
3294 xs_json_dump(noti, 4, f);
3295 fclose(f);
3296 }
3297
3298 /* add it to the index if it already exists */
3299 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3300
3301 if (mtime(idx) != 0.0) {
3302 pthread_mutex_lock(&data_mutex);
3303
3304 if ((f = fopen(idx, "a")) != NULL) {
3305 fprintf(f, "%-32s\n", ntid);
3306 fclose(f);
3307 }
3308
3309 pthread_mutex_unlock(&data_mutex);
3310 }
3311
3312 if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook")))
3313 enqueue_notify_webhook(snac, noti, 0);
3314}
3315
3316
3317xs_dict *notify_get(snac *snac, const char *id)
3318/* gets a notification */
3319{
3320 /* base file */
3321 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
3322
3323 /* strip spaces and add extension */
3324 fn = xs_strip_i(fn);
3325 fn = xs_str_cat(fn, ".json");
3326
3327 FILE *f;
3328 xs_dict *out = NULL;
3329
3330 if ((f = fopen(fn, "r")) != NULL) {
3331 out = xs_json_load(f);
3332 fclose(f);
3333 }
3334
3335 return out;
3336}
3337
3338
3339xs_list *notify_list(snac *snac, int skip, int show)
3340/* returns a list of notification ids */
3341{
3342 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3343
3344 if (mtime(idx) == 0.0) {
3345 /* create the index from scratch */
3346 FILE *f;
3347
3348 pthread_mutex_lock(&data_mutex);
3349
3350 if ((f = fopen(idx, "w")) != NULL) {
3351 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
3352 xs *lst = xs_glob(spec, 1, 0);
3353 xs_list *p = lst;
3354 const char *v;
3355
3356 while (xs_list_iter(&p, &v)) {
3357 char *p = strrchr(v, '.');
3358 if (p) {
3359 *p = '\0';
3360 fprintf(f, "%-32s\n", v);
3361 }
3362 }
3363
3364 fclose(f);
3365 }
3366
3367 pthread_mutex_unlock(&data_mutex);
3368 }
3369
3370 return index_list_desc(idx, skip, show);
3371}
3372
3373
3374int notify_new_num(snac *snac)
3375/* counts the number of new notifications */
3376{
3377 xs *t = notify_check_time(snac, 0);
3378 xs *lst = notify_list(snac, 0, XS_ALL);
3379 int cnt = 0;
3380
3381 xs_list *p = lst;
3382 const xs_str *v;
3383
3384 while (xs_list_iter(&p, &v)) {
3385 xs *id = xs_strip_i(xs_dup(v));
3386
3387 /* old? count no more */
3388 if (strcmp(id, t) < 0)
3389 break;
3390
3391 cnt++;
3392 }
3393
3394 return cnt;
3395}
3396
3397
3398void notify_clear(snac *snac)
3399/* clears all notifications */
3400{
3401 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
3402 xs *lst = xs_glob(spec, 0, 0);
3403 xs_list *p = lst;
3404 const xs_str *v;
3405
3406 while (xs_list_iter(&p, &v))
3407 unlink(v);
3408
3409 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3410
3411 if (mtime(idx) != 0.0) {
3412 pthread_mutex_lock(&data_mutex);
3413 truncate(idx, 0);
3414 pthread_mutex_unlock(&data_mutex);
3415 }
3416}
3417
3418
3419/** the queue **/
3420
3421static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
3422/* writes safely to the queue */
3423{
3424 xs *tfn = xs_fmt("%s.tmp", fn);
3425 FILE *f;
3426
3427 if ((f = fopen(tfn, "w")) != NULL) {
3428 xs_json_dump(msg, 4, f);
3429 fclose(f);
3430
3431 rename(tfn, fn);
3432 }
3433
3434 return msg;
3435}
3436
3437
3438static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
3439/* creates a queue message */
3440{
3441 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
3442 xs *ntid = tid(retries * 60 * qrt);
3443 xs *rn = xs_number_new(retries);
3444
3445 xs_dict *qmsg = xs_dict_new();
3446
3447 qmsg = xs_dict_append(qmsg, "type", type);
3448 qmsg = xs_dict_append(qmsg, "message", msg);
3449 qmsg = xs_dict_append(qmsg, "retries", rn);
3450 qmsg = xs_dict_append(qmsg, "ntid", ntid);
3451
3452 return qmsg;
3453}
3454
3455
3456void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3457/* enqueues an input message */
3458{
3459 xs *qmsg = _new_qmsg("input", msg, retries);
3460 const char *ntid = xs_dict_get(qmsg, "ntid");
3461 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3462
3463 qmsg = xs_dict_append(qmsg, "req", req);
3464
3465 qmsg = _enqueue_put(fn, qmsg);
3466
3467 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3468}
3469
3470
3471void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3472/* enqueues an input message from the shared input */
3473{
3474 xs *qmsg = _new_qmsg("input", msg, retries);
3475 const char *ntid = xs_dict_get(qmsg, "ntid");
3476 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3477
3478 qmsg = xs_dict_append(qmsg, "req", req);
3479
3480 qmsg = _enqueue_put(fn, qmsg);
3481
3482 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3483}
3484
3485
3486void enqueue_output_raw(const char *keyid, const char *seckey,
3487 const xs_dict *msg, const xs_str *inbox,
3488 int retries, int p_status)
3489/* enqueues an output message to an inbox */
3490{
3491 xs *qmsg = _new_qmsg("output", msg, retries);
3492 const char *ntid = xs_dict_get(qmsg, "ntid");
3493 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3494
3495 xs *ns = xs_number_new(p_status);
3496 qmsg = xs_dict_append(qmsg, "p_status", ns);
3497
3498 qmsg = xs_dict_append(qmsg, "inbox", inbox);
3499 qmsg = xs_dict_append(qmsg, "keyid", keyid);
3500 qmsg = xs_dict_append(qmsg, "seckey", seckey);
3501
3502 /* if it's to be sent right now, bypass the disk queue and post the job */
3503 if (retries == 0 && p_state != NULL)
3504 job_post(qmsg, 0);
3505 else {
3506 qmsg = _enqueue_put(fn, qmsg);
3507 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3508 }
3509}
3510
3511
3512void enqueue_output(snac *snac, const xs_dict *msg,
3513 const xs_str *inbox, int retries, int p_status)
3514/* enqueues an output message to an inbox */
3515{
3516 if (xs_startswith(inbox, snac->actor)) {
3517 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3518 return;
3519 }
3520
3521 const char *seckey = xs_dict_get(snac->key, "secret");
3522
3523 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3524}
3525
3526
3527void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3528 const xs_str *actor, int retries)
3529/* enqueues an output message for an actor */
3530{
3531 xs *inbox = get_actor_inbox(actor, 1);
3532
3533 if (!xs_is_null(inbox))
3534 enqueue_output(snac, msg, inbox, retries, 0);
3535 else
3536 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3537}
3538
3539
3540void enqueue_email(const xs_dict *msg, int retries)
3541/* enqueues an email message to be sent */
3542{
3543 xs *qmsg = _new_qmsg("email", msg, retries);
3544 const char *ntid = xs_dict_get(qmsg, "ntid");
3545 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3546
3547 qmsg = _enqueue_put(fn, qmsg);
3548
3549 srv_debug(1, xs_fmt("enqueue_email %d", retries));
3550}
3551
3552
3553void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3554/* enqueues a message to be sent via Telegram */
3555{
3556 xs *qmsg = _new_qmsg("telegram", msg, 0);
3557 const char *ntid = xs_dict_get(qmsg, "ntid");
3558 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3559
3560 qmsg = xs_dict_append(qmsg, "bot", bot);
3561 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
3562
3563 qmsg = _enqueue_put(fn, qmsg);
3564
3565 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3566}
3567
3568void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3569/* enqueues a message to be sent via ntfy */
3570{
3571 xs *qmsg = _new_qmsg("ntfy", msg, 0);
3572 const char *ntid = xs_dict_get(qmsg, "ntid");
3573 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3574
3575 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3576 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
3577
3578
3579 qmsg = _enqueue_put(fn, qmsg);
3580
3581 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3582}
3583
3584void enqueue_message(snac *snac, const xs_dict *msg)
3585/* enqueues an output message */
3586{
3587 xs *qmsg = _new_qmsg("message", msg, 0);
3588 const char *ntid = xs_dict_get(qmsg, "ntid");
3589 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3590
3591 qmsg = _enqueue_put(fn, qmsg);
3592
3593 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3594}
3595
3596
3597void enqueue_close_question(snac *user, const char *id, int end_secs)
3598/* enqueues the closing of a question */
3599{
3600 xs *qmsg = _new_qmsg("close_question", id, 0);
3601 xs *ntid = tid(end_secs);
3602 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3603
3604 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3605
3606 qmsg = _enqueue_put(fn, qmsg);
3607
3608 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3609}
3610
3611
3612void enqueue_object_request(snac *user, const char *id, int forward_secs)
3613/* enqueues the request of an object in the future */
3614{
3615 xs *qmsg = _new_qmsg("object_request", id, 0);
3616 xs *ntid = tid(forward_secs);
3617 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3618
3619 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3620
3621 qmsg = _enqueue_put(fn, qmsg);
3622
3623 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3624}
3625
3626
3627void enqueue_verify_links(snac *user)
3628/* enqueues a link verification */
3629{
3630 xs *qmsg = _new_qmsg("verify_links", "", 0);
3631 const char *ntid = xs_dict_get(qmsg, "ntid");
3632 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3633
3634 qmsg = _enqueue_put(fn, qmsg);
3635
3636 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3637}
3638
3639
3640void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3641/* enqueues an actor refresh */
3642{
3643 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3644 xs *ntid = tid(forward_secs);
3645 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3646
3647 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3648 qmsg = xs_dict_append(qmsg, "actor", actor);
3649
3650 qmsg = _enqueue_put(fn, qmsg);
3651
3652 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3653}
3654
3655
3656void enqueue_webmention(const xs_dict *msg)
3657/* enqueues a webmention for the post */
3658{
3659 xs *qmsg = _new_qmsg("webmention", msg, 0);
3660 const char *ntid = xs_dict_get(qmsg, "ntid");
3661 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3662
3663 qmsg = _enqueue_put(fn, qmsg);
3664
3665 srv_debug(1, xs_fmt("enqueue_webmention"));
3666}
3667
3668
3669void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries)
3670/* enqueues a notification webhook */
3671{
3672 const char *webhook = xs_dict_get(user->config, "notify_webhook");
3673
3674 if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3675 xs *msg = xs_dup(noti);
3676
3677 /* add more data */
3678 msg = xs_dict_set(msg, "target", user->actor);
3679 msg = xs_dict_set(msg, "uid", user->uid);
3680 msg = xs_dict_set(msg, "basedir", srv_basedir);
3681 msg = xs_dict_set(msg, "baseurl", srv_baseurl);
3682
3683 xs *actor_obj = NULL;
3684
3685 if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj)
3686 msg = xs_dict_set(msg, "account", actor_obj);
3687
3688 /* if this post is a reply, also add the inReplyTo object */
3689 const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo");
3690
3691 if (xs_is_string(in_reply_to)) {
3692 xs *irt_obj = NULL;
3693
3694 if (valid_status(object_get(in_reply_to, &irt_obj)))
3695 msg = xs_dict_set(msg, "reply", irt_obj);
3696 }
3697
3698 xs *qmsg = _new_qmsg("notify_webhook", msg, retries);
3699 const char *ntid = xs_dict_get(qmsg, "ntid");
3700 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3701
3702 qmsg = _enqueue_put(fn, qmsg);
3703
3704 snac_debug(user, 1, xs_fmt("notify_webhook"));
3705 }
3706}
3707
3708
3709void enqueue_collect_replies(snac *user, const char *post)
3710/* enqueues a collect replies request */
3711{
3712 xs *qmsg = _new_qmsg("collect_replies", post, 0);
3713 const char *ntid = xs_dict_get(qmsg, "ntid");
3714 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3715
3716 qmsg = _enqueue_put(fn, qmsg);
3717
3718 snac_debug(user, 1, xs_fmt("enqueue_collect_replies %s", post));
3719}
3720
3721
3722void enqueue_collect_outbox(snac *user, const char *actor_id)
3723/* enqueues a collect outbox request */
3724{
3725 xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
3726 const char *ntid = xs_dict_get(qmsg, "ntid");
3727 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3728
3729 qmsg = _enqueue_put(fn, qmsg);
3730
3731 snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
3732}
3733
3734
3735void enqueue_fsck(void)
3736/* enqueues an fsck */
3737{
3738 xs *qmsg = _new_qmsg("fsck", "", 0);
3739 const char *ntid = xs_dict_get(qmsg, "ntid");
3740 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3741
3742 qmsg = _enqueue_put(fn, qmsg);
3743}
3744
3745
3746int was_question_voted(snac *user, const char *id)
3747/* returns true if the user voted in this poll */
3748{
3749 xs *children = object_children(id);
3750 int voted = 0;
3751 xs_list *p;
3752 const xs_str *md5;
3753
3754 p = children;
3755 while (xs_list_iter(&p, &md5)) {
3756 xs *obj = NULL;
3757
3758 if (valid_status(object_get_by_md5(md5, &obj))) {
3759 const char *atto = get_atto(obj);
3760 if (atto && strcmp(atto, user->actor) == 0 &&
3761 !xs_is_null(xs_dict_get(obj, "name"))) {
3762 voted = 1;
3763 break;
3764 }
3765 }
3766 }
3767
3768 return voted;
3769}
3770
3771
3772xs_list *user_queue(snac *snac)
3773/* returns a list with filenames that can be dequeued */
3774{
3775 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
3776 xs_list *list = xs_list_new();
3777 time_t t = time(NULL);
3778 xs_list *p;
3779 const xs_val *v;
3780
3781 xs *fns = xs_glob(spec, 0, 0);
3782
3783 p = fns;
3784 while (xs_list_iter(&p, &v)) {
3785 /* get the retry time from the basename */
3786 char *bn = strrchr(v, '/');
3787 time_t t2 = atol(bn + 1);
3788
3789 if (t2 > t)
3790 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3791 else {
3792 list = xs_list_append(list, v);
3793 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3794 }
3795 }
3796
3797 return list;
3798}
3799
3800
3801xs_list *queue(void)
3802/* returns a list with filenames that can be dequeued */
3803{
3804 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
3805 xs_list *list = xs_list_new();
3806 time_t t = time(NULL);
3807 xs_list *p;
3808 const xs_val *v;
3809
3810 xs *fns = xs_glob(spec, 0, 0);
3811
3812 p = fns;
3813 while (xs_list_iter(&p, &v)) {
3814 /* get the retry time from the basename */
3815 char *bn = strrchr(v, '/');
3816 time_t t2 = atol(bn + 1);
3817
3818 if (t2 > t)
3819 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3820 else {
3821 list = xs_list_append(list, v);
3822 srv_debug(2, xs_fmt("queue ready for %s", v));
3823 }
3824 }
3825
3826 return list;
3827}
3828
3829
3830xs_dict *queue_get(const char *fn)
3831/* gets a file from a queue */
3832{
3833 FILE *f;
3834 xs_dict *obj = NULL;
3835
3836 if ((f = fopen(fn, "r")) != NULL) {
3837 obj = xs_json_load(f);
3838 fclose(f);
3839 }
3840
3841 return obj;
3842}
3843
3844
3845xs_dict *dequeue(const char *fn)
3846/* dequeues a message */
3847{
3848 xs_dict *obj = queue_get(fn);
3849
3850 unlink(fn);
3851
3852 return obj;
3853}
3854
3855
3856/** the purge **/
3857
3858static int _purge_file(const char *fn, time_t mt)
3859/* purge fn if it's older than days */
3860{
3861 int ret = 0;
3862
3863 if (mtime(fn) < mt) {
3864 /* older than the minimum time: delete it */
3865 unlink(fn);
3866 srv_debug(2, xs_fmt("purged %s", fn));
3867 ret = 1;
3868 }
3869
3870 return ret;
3871}
3872
3873
3874static void _purge_dir(const char *dir, int days)
3875/* purges all files in a directory older than days */
3876{
3877 int cnt = 0;
3878
3879 if (days) {
3880 time_t mt = time(NULL) - days * 24 * 3600;
3881 xs *spec = xs_fmt("%s/" "*", dir);
3882 xs *list = xs_glob(spec, 0, 0);
3883 xs_list *p;
3884 const xs_str *v;
3885
3886 p = list;
3887 while (xs_list_iter(&p, &v))
3888 cnt += _purge_file(v, mt);
3889
3890 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3891 }
3892}
3893
3894
3895static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3896/* purges all files in a user subdir older than days */
3897{
3898 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3899
3900 _purge_dir(u_subdir, days);
3901}
3902
3903
3904void purge_server(void)
3905/* purge global server data */
3906{
3907 xs *spec = xs_fmt("%s/object/??", srv_basedir);
3908 xs *dirs = xs_glob(spec, 0, 0);
3909 xs_list *p;
3910 const xs_str *v;
3911 int cnt = 0;
3912 int icnt = 0;
3913
3914 time_t mt = time(NULL) - 7 * 24 * 3600;
3915
3916 p = dirs;
3917 while (xs_list_iter(&p, &v)) {
3918 xs_list *p2;
3919 const xs_str *v2;
3920
3921 {
3922 xs *spec2 = xs_fmt("%s/" "*.json", v);
3923 xs *files = xs_glob(spec2, 0, 0);
3924
3925 p2 = files;
3926 while (xs_list_iter(&p2, &v2)) {
3927 int n_link;
3928
3929 /* old and with no hard links? */
3930 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3931 xs *s1 = xs_replace(v2, ".json", "");
3932 xs *l = xs_split(s1, "/");
3933 const char *md5 = xs_list_get(l, -1);
3934
3935 object_del_by_md5(md5);
3936 cnt++;
3937 }
3938 }
3939 }
3940
3941 {
3942 /* look for stray indexes */
3943 xs *speci = xs_fmt("%s/" "*_?.idx", v);
3944 xs *idxfs = xs_glob(speci, 0, 0);
3945
3946 p2 = idxfs;
3947 while (xs_list_iter(&p2, &v2)) {
3948 /* old enough to consider? */
3949 if (mtime(v2) < mt) {
3950 /* check if the indexed object is here */
3951 xs *o = xs_dup(v2);
3952 char *ext = strchr(o, '_');
3953
3954 if (ext) {
3955 *ext = '\0';
3956 o = xs_str_cat(o, ".json");
3957
3958 if (mtime(o) == 0.0) {
3959 /* delete */
3960 unlink(v2);
3961 srv_debug(1, xs_fmt("purged %s", v2));
3962 icnt++;
3963 }
3964 }
3965 }
3966 }
3967
3968 /* delete index backups */
3969 xs *specb = xs_fmt("%s/" "*.bak", v);
3970 xs *bakfs = xs_glob(specb, 0, 0);
3971
3972 p2 = bakfs;
3973 while (xs_list_iter(&p2, &v2)) {
3974 unlink(v2);
3975 srv_debug(1, xs_fmt("purged %s", v2));
3976 }
3977 }
3978 }
3979
3980 /* purge collected inboxes */
3981 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3982 _purge_dir(ib_dir, 7);
3983
3984 /* purge the instance timeline */
3985 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3986 int itl_gc = index_gc(itl_fn);
3987
3988 /* purge tag indexes */
3989 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3990 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3991 p = tag_dirs;
3992
3993 int tag_gc = 0;
3994 while (xs_list_iter(&p, &v)) {
3995 xs *spec2 = xs_fmt("%s/" "*.idx", v);
3996 xs *files = xs_glob(spec2, 0, 0);
3997 xs_list *p2;
3998 const xs_str *v2;
3999
4000 p2 = files;
4001 while (xs_list_iter(&p2, &v2)) {
4002 tag_gc += index_gc(v2);
4003 xs *bak = xs_fmt("%s.bak", v2);
4004 unlink(bak);
4005
4006 if (index_len(v2) == 0) {
4007 /* there are no longer any entry with this tag;
4008 purge it completely */
4009 unlink(v2);
4010 xs *dottag = xs_replace(v2, ".idx", ".tag");
4011 unlink(dottag);
4012 }
4013 }
4014 }
4015
4016 srv_debug(1, xs_fmt("purge: global "
4017 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
4018}
4019
4020
4021void delete_purged_posts(snac *user, int days)
4022/* enqueues Delete activities for local purged messages */
4023{
4024 if (days == 0)
4025 return;
4026
4027 time_t mt = time(NULL) - days * 24 * 3600;
4028 xs *spec = xs_fmt("%s/public/" "*.json", user->basedir);
4029 xs *list = xs_glob(spec, 0, 0);
4030 const char *v;
4031
4032 xs_list_foreach(list, v) {
4033 if (mtime(v) < mt) {
4034 /* to be purged; is it a Note by us? */
4035 FILE *f;
4036
4037 if ((f = fopen(v, "r")) != NULL) {
4038 xs *msg = xs_json_load(f);
4039 fclose(f);
4040
4041 if (xs_is_dict(msg)) {
4042 const char *id = xs_dict_get(msg, "id");
4043
4044 if (xs_is_string(id) && xs_startswith(id, user->actor)) {
4045 xs *d_msg = msg_delete(user, id);
4046
4047 enqueue_message(user, d_msg);
4048
4049 snac_log(user, xs_fmt("enqueued Delete for purged message %s", id));
4050 }
4051 }
4052 }
4053 }
4054 }
4055}
4056
4057
4058void purge_user(snac *snac)
4059/* do the purge for this user */
4060{
4061 int priv_days, pub_days, user_days = 0;
4062 const char *v;
4063 int n;
4064
4065 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
4066 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
4067
4068 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
4069 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
4070 user_days = xs_number_get(v);
4071
4072 if (user_days) {
4073 /* override admin settings only if they are lesser */
4074 if (priv_days == 0 || user_days < priv_days)
4075 priv_days = user_days;
4076
4077 if (pub_days == 0 || user_days < pub_days)
4078 pub_days = user_days;
4079 }
4080
4081 if (xs_is_true(xs_dict_get(srv_config, "propagate_local_purge")))
4082 delete_purged_posts(snac, pub_days);
4083
4084 _purge_user_subdir(snac, "hidden", priv_days);
4085 _purge_user_subdir(snac, "private", priv_days);
4086
4087 _purge_user_subdir(snac, "public", pub_days);
4088
4089 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
4090 "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
4091
4092 for (n = 0; idxs[n]; n++) {
4093 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
4094 int gc = index_gc(idx);
4095 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
4096 }
4097
4098 /* purge lists */
4099 {
4100 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
4101 xs *lol = xs_glob(spec, 0, 0);
4102 int c = 0;
4103 const char *v;
4104
4105 while (xs_list_next(lol, &v, &c)) {
4106 int gc = index_gc(v);
4107 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
4108 }
4109 }
4110
4111 /* unrelated to purging, but it's a janitorial process, so what the hell */
4112 verify_links(snac);
4113}
4114
4115
4116void purge_all(void)
4117/* purge all users */
4118{
4119 snac snac;
4120 xs *list = user_list();
4121 char *p;
4122 const char *uid;
4123
4124 p = list;
4125 while (xs_list_iter(&p, &uid)) {
4126 if (user_open(&snac, uid)) {
4127 purge_user(&snac);
4128 user_free(&snac);
4129 }
4130 }
4131
4132 purge_server();
4133
4134#ifndef NO_MASTODON_API
4135 mastoapi_purge();
4136#endif
4137}
4138
4139
4140/** archive **/
4141
4142void srv_archive(const char *direction, const char *url, xs_dict *req,
4143 const char *payload, int p_size,
4144 int status, xs_dict *headers,
4145 const char *body, int b_size)
4146/* archives a connection */
4147{
4148 /* obsessive archiving */
4149 xs *date = tid(0);
4150 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
4151 FILE *f;
4152
4153 if (mkdirx(dir) != -1) {
4154 xs *meta_fn = xs_fmt("%s/_META", dir);
4155
4156 if ((f = fopen(meta_fn, "w")) != NULL) {
4157 xs *j1 = xs_json_dumps(req, 4);
4158 xs *j2 = xs_json_dumps(headers, 4);
4159
4160 fprintf(f, "dir: %s\n", direction);
4161
4162 if (url)
4163 fprintf(f, "url: %s\n", url);
4164
4165 fprintf(f, "req: %s\n", j1);
4166 fprintf(f, "p_size: %d\n", p_size);
4167 fprintf(f, "status: %d\n", status);
4168 fprintf(f, "response: %s\n", j2);
4169 fprintf(f, "b_size: %d\n", b_size);
4170 fclose(f);
4171 }
4172
4173 if (p_size && payload) {
4174 xs *payload_fn = NULL;
4175 xs *payload_fn_raw = NULL;
4176 const char *v = xs_dict_get(req, "content-type");
4177
4178 if (v && xs_str_in(v, "json") != -1) {
4179 payload_fn = xs_fmt("%s/payload.json", dir);
4180
4181 if ((f = fopen(payload_fn, "w")) != NULL) {
4182 xs *v1 = xs_json_loads(payload);
4183 xs *j1 = NULL;
4184
4185 if (v1 != NULL)
4186 j1 = xs_json_dumps(v1, 4);
4187
4188 if (j1 != NULL)
4189 fwrite(j1, strlen(j1), 1, f);
4190 else
4191 fwrite(payload, p_size, 1, f);
4192
4193 fclose(f);
4194 }
4195 }
4196
4197 payload_fn_raw = xs_fmt("%s/payload", dir);
4198
4199 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
4200 fwrite(payload, p_size, 1, f);
4201 fclose(f);
4202 }
4203 }
4204
4205 if (b_size && body) {
4206 xs *body_fn = NULL;
4207 const char *v = xs_dict_get(headers, "content-type");
4208
4209 if (v && xs_str_in(v, "json") != -1) {
4210 body_fn = xs_fmt("%s/body.json", dir);
4211
4212 if ((f = fopen(body_fn, "w")) != NULL) {
4213 xs *v1 = xs_json_loads(body);
4214 xs *j1 = NULL;
4215
4216 if (v1 != NULL)
4217 j1 = xs_json_dumps(v1, 4);
4218
4219 if (j1 != NULL)
4220 fwrite(j1, strlen(j1), 1, f);
4221 else
4222 fwrite(body, b_size, 1, f);
4223
4224 fclose(f);
4225 }
4226 }
4227 else {
4228 body_fn = xs_fmt("%s/body", dir);
4229
4230 if ((f = fopen(body_fn, "w")) != NULL) {
4231 fwrite(body, b_size, 1, f);
4232 fclose(f);
4233 }
4234 }
4235 }
4236 }
4237}
4238
4239
4240void srv_archive_error(const char *prefix, const xs_str *err,
4241 const xs_dict *req, const xs_val *data)
4242/* archives an error */
4243{
4244 xs *ntid = tid(0);
4245 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
4246 FILE *f;
4247
4248 if ((f = fopen(fn, "w")) != NULL) {
4249 fprintf(f, "Error: %s\n", err);
4250
4251 if (req) {
4252 fprintf(f, "Request headers:\n");
4253
4254 xs_json_dump(req, 4, f);
4255
4256 fprintf(f, "\n");
4257 }
4258
4259 if (data) {
4260 fprintf(f, "Data:\n");
4261
4262 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
4263 xs_json_dump(data, 4, f);
4264 }
4265 else
4266 fprintf(f, "%s", data);
4267
4268 fprintf(f, "\n");
4269 }
4270
4271 fclose(f);
4272 }
4273}
4274
4275
4276void srv_archive_qitem(const char *prefix, xs_dict *q_item)
4277/* archives a q_item in the error folder */
4278{
4279 xs *ntid = tid(0);
4280 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
4281 FILE *f;
4282
4283 if ((f = fopen(fn, "w")) != NULL) {
4284 xs_json_dump(q_item, 4, f);
4285 fclose(f);
4286 }
4287}
4288
4289
4290t_announcement *announcement(const double after)
4291/* returns announcement text or NULL if none exists or it is olde than "after" */
4292{
4293 static const long int MAX_SIZE = 2048;
4294 static t_announcement a = {
4295 .text = NULL,
4296 .timestamp = 0.0,
4297 };
4298 static xs_str *fn = NULL;
4299 if (fn == NULL)
4300 fn = xs_fmt("%s/announcement.txt", srv_basedir);
4301
4302 const double ts = mtime(fn);
4303
4304 /* file does not exist or other than what was requested */
4305 if (ts == 0.0 || ts <= after)
4306 return NULL;
4307
4308 /* nothing changed, just return the current announcement */
4309 if (a.text != NULL && ts <= a.timestamp)
4310 return &a;
4311
4312 /* read and store new announcement */
4313 FILE *f;
4314
4315 if ((f = fopen(fn, "r")) != NULL) {
4316 fseek (f, 0, SEEK_END);
4317 const long int length = ftell(f);
4318
4319 if (length > MAX_SIZE) {
4320 /* this is probably unintentional */
4321 srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
4322 }
4323 else
4324 if (length > 0) {
4325 fseek (f, 0, SEEK_SET);
4326 char *buffer = malloc(length + 1);
4327 if (buffer) {
4328 fread(buffer, 1, length, f);
4329 buffer[length] = '\0';
4330
4331 free(a.text);
4332 a.text = buffer;
4333 a.timestamp = ts;
4334 }
4335 else {
4336 srv_log("Error allocating memory for announcement");
4337 }
4338 }
4339 else {
4340 /* an empty file means no announcement */
4341 free(a.text);
4342 a.text = NULL;
4343 a.timestamp = 0.0;
4344 }
4345
4346 fclose (f);
4347 }
4348
4349 if (a.text != NULL)
4350 return &a;
4351
4352 return NULL;
4353}
4354
4355
4356xs_str *make_url(const char *href, const char *proxy, int by_token)
4357/* makes an URL, possibly including proxying */
4358{
4359 xs_str *url = NULL;
4360
4361 if (proxy && !xs_startswith(href, srv_baseurl)) {
4362 xs *p = NULL;
4363
4364 if (by_token) {
4365 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
4366 xs *tk = xs_md5_hex(tks, strlen(tks));
4367
4368 p = xs_fmt("%s/y/%s/", proxy, tk);
4369 }
4370 else
4371 p = xs_fmt("%s/x/", proxy);
4372
4373 url = xs_replace(href, "https:/" "/", p);
4374 }
4375 else
4376 url = xs_dup(href);
4377
4378 return url;
4379}
4380
4381
4382/** bad login throttle **/
4383
4384xs_str *_badlogin_fn(const char *addr)
4385{
4386 xs *md5 = xs_md5_hex(addr, strlen(addr));
4387 xs *dir = xs_fmt("%s/badlogin", srv_basedir);
4388
4389 mkdirx(dir);
4390
4391 return xs_fmt("%s/%s", dir, md5);
4392}
4393
4394
4395int _badlogin_read(const char *fn, int *failures)
4396/* reads a badlogin file */
4397{
4398 int ok = 0;
4399 FILE *f;
4400
4401 pthread_mutex_lock(&data_mutex);
4402
4403 if ((f = fopen(fn, "r")) != NULL) {
4404 xs *l = xs_readline(f);
4405 fclose(f);
4406
4407 if (sscanf(l, "%d", failures) == 1)
4408 ok = 1;
4409 }
4410
4411 pthread_mutex_unlock(&data_mutex);
4412
4413 return ok;
4414}
4415
4416
4417int badlogin_check(const char *user, const char *addr)
4418/* checks if this address is authorized to try a login */
4419{
4420 int valid = 1;
4421
4422 if (xs_type(addr) == XSTYPE_STRING) {
4423 xs *fn = _badlogin_fn(addr);
4424 double mt = mtime(fn);
4425
4426 if (mt > 0) {
4427 int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
4428 "badlogin_expire", "300"));
4429
4430 mt += badlogin_expire;
4431
4432 /* if file is expired, delete and give pass */
4433 if (mt < time(NULL)) {
4434 srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
4435 unlink(fn);
4436 }
4437 else {
4438 int failures;
4439
4440 if (_badlogin_read(fn, &failures)) {
4441 int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
4442 "badlogin_retries", "5"));
4443
4444 if (failures >= badlogin_max) {
4445 valid = 0;
4446
4447 xs *d = xs_str_iso_date((time_t) mt);
4448
4449 srv_debug(1,
4450 xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
4451 }
4452 }
4453 }
4454 }
4455 }
4456
4457 return valid;
4458}
4459
4460
4461void badlogin_inc(const char *user, const char *addr)
4462/* increments a bad login from this address */
4463{
4464 if (xs_type(addr) == XSTYPE_STRING) {
4465 int failures = 0;
4466 xs *fn = _badlogin_fn(addr);
4467 FILE *f;
4468
4469 _badlogin_read(fn, &failures);
4470
4471 pthread_mutex_lock(&data_mutex);
4472
4473 if ((f = fopen(fn, "w")) != NULL) {
4474 failures++;
4475
4476 fprintf(f, "%d %s %s\n", failures, addr, user);
4477 fclose(f);
4478
4479 srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
4480 }
4481
4482 pthread_mutex_unlock(&data_mutex);
4483 }
4484}
4485
4486
4487/** language strings **/
4488
4489const char *lang_str(const char *str, const snac *user)
4490/* returns a translated string */
4491{
4492 const char *n_str = str;
4493
4494 if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4495 n_str = xs_dict_get(user->lang, str);
4496
4497 if (xs_is_null(n_str) || *n_str == '\0')
4498 n_str = str;
4499 }
4500
4501 return n_str;
4502}
4503
4504
4505/** integrity checks **/
4506
4507void data_fsck(void)
4508{
4509 xs *list = user_list();
4510 const char *uid;
4511
4512 xs_list_foreach(list, uid) {
4513 snac user;
4514
4515 if (!user_open(&user, uid))
4516 continue;
4517
4518 {
4519 /* iterate all private posts and check that non-public posts
4520 from this user are also linked into the public directory,
4521 to avoid the don't-fucking-delete-my-own-private-posts purge bug */
4522
4523 xs *priv_spec = xs_fmt("%s/private/""*.json", user.basedir);
4524 xs *posts = xs_glob(priv_spec, 0, 0);
4525 const char *priv_fn;
4526
4527 xs_list_foreach(posts, priv_fn) {
4528 xs *pub_fn = xs_replace(priv_fn, "/private/", "/public/");
4529
4530 /* already there? look no more */
4531 if (mtime(pub_fn))
4532 continue;
4533
4534 /* read the post */
4535 FILE *f;
4536 if ((f = fopen(priv_fn, "r")) == NULL)
4537 continue;
4538
4539 xs *post = xs_json_load(f);
4540 fclose(f);
4541
4542 if (!xs_is_dict(post))
4543 continue;
4544
4545 const char *attr_to = get_atto(post);
4546
4547 if (!xs_is_string(attr_to) || strcmp(attr_to, user.actor) != 0) {
4548 /* not from this user */
4549 continue;
4550 }
4551
4552 /* link */
4553 snac_debug(&user, 1, xs_fmt("fsck: fixed missing link %s", xs_dict_get(post, "id")));
4554 link(priv_fn, pub_fn);
4555 }
4556 }
4557
4558 user_free(&user);
4559 }
4560}