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