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