2.80
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 a message tree */
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 actor_add(const char *actor, const xs_dict *msg)
2085/* adds an actor */
2086{
2087 return object_add_ow(actor, msg);
2088}
2089
2090
2091int actor_get(const char *actor, xs_dict **data)
2092/* returns an already downloaded actor */
2093{
2094 int status = HTTP_STATUS_OK;
2095 xs_dict *d = NULL;
2096
2097 if (xs_startswith(actor, srv_baseurl)) {
2098 /* it's a (possible) local user */
2099 xs *l = xs_split(actor, "/");
2100 const char *uid = xs_list_get(l, -1);
2101 snac user;
2102
2103 if (!xs_is_null(uid) && user_open(&user, uid)) {
2104 if (data)
2105 *data = msg_actor(&user);
2106
2107 user_free(&user);
2108 return HTTP_STATUS_OK;
2109 }
2110 else
2111 return HTTP_STATUS_NOT_FOUND;
2112 }
2113
2114 /* read the object */
2115 if (!valid_status(status = object_get(actor, &d))) {
2116 d = xs_free(d);
2117 return status;
2118 }
2119
2120 /* if the object is corrupted, discard it */
2121 if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
2122 srv_debug(1, xs_fmt("corrupted actor object %s", actor));
2123 d = xs_free(d);
2124 return HTTP_STATUS_NOT_FOUND;
2125 }
2126
2127 if (data)
2128 *data = d;
2129 else
2130 d = xs_free(d);
2131
2132 xs *fn = _object_fn(actor);
2133 double max_time;
2134
2135 /* maximum time for the actor data to be considered stale */
2136 max_time = 3600.0 * 36.0;
2137
2138 if (mtime(fn) + max_time < (double) time(NULL)) {
2139 /* actor data exists but also stinks */
2140 status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
2141 }
2142
2143 return status;
2144}
2145
2146
2147int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
2148/* gets an actor and requests a refresh if it's stale */
2149{
2150 int status = actor_get(actor, data);
2151
2152 if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
2153 enqueue_actor_refresh(user, actor, 0);
2154
2155 return status;
2156}
2157
2158
2159/** user limiting (announce blocks) **/
2160
2161int limited(snac *user, const char *id, int cmd)
2162/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
2163{
2164 int ret = 0;
2165 xs *dir = xs_fmt("%s/limited", user->basedir);
2166 xs *md5 = xs_md5_hex(id, strlen(id));
2167 xs *fn = xs_fmt("%s/%s", dir, md5);
2168
2169 switch (cmd) {
2170 case 0: /** check **/
2171 ret = !!(mtime(fn) > 0.0);
2172 break;
2173
2174 case 1: /** limit **/
2175 mkdirx(dir);
2176
2177 if (mtime(fn) > 0.0)
2178 ret = -1;
2179 else {
2180 FILE *f;
2181
2182 if ((f = fopen(fn, "w")) != NULL) {
2183 fprintf(f, "%s\n", id);
2184 fclose(f);
2185 }
2186 else
2187 ret = -2;
2188 }
2189 break;
2190
2191 case 2: /** unlimit **/
2192 if (mtime(fn) > 0.0)
2193 ret = unlink(fn);
2194 else
2195 ret = -1;
2196 break;
2197 }
2198
2199 return ret;
2200}
2201
2202
2203/** tag indexing **/
2204
2205void tag_index(const char *id, const xs_dict *obj)
2206/* update the tag indexes for this object */
2207{
2208 const xs_list *tags = xs_dict_get(obj, "tag");
2209 xs *md5_id = xs_md5_hex(id, strlen(id));
2210
2211 if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
2212 xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
2213
2214 mkdirx(g_tag_dir);
2215
2216 const xs_dict *v;
2217 int ct = 0;
2218 while (xs_list_next(tags, &v, &ct)) {
2219 const char *type = xs_dict_get(v, "type");
2220 const char *name = xs_dict_get(v, "name");
2221
2222 if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
2223 while (*name == '#' || *name == '@')
2224 name++;
2225
2226 if (*name == '\0')
2227 continue;
2228
2229 name = xs_utf8_to_lower((xs_str *)name);
2230
2231 xs *md5_tag = xs_md5_hex(name, strlen(name));
2232 xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
2233 mkdirx(tag_dir);
2234
2235 xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
2236
2237 if (!index_in_md5(g_tag_idx, md5_id))
2238 index_add_md5(g_tag_idx, md5_id);
2239
2240 FILE *f;
2241 xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
2242 if ((f = fopen(g_tag_name, "w")) != NULL) {
2243 fprintf(f, "%s\n", name);
2244 fclose(f);
2245 }
2246
2247 srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
2248 }
2249 }
2250 }
2251}
2252
2253
2254xs_str *tag_fn(const char *tag)
2255{
2256 if (*tag == '#')
2257 tag++;
2258
2259 xs *lw_tag = xs_utf8_to_lower(tag);
2260 xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag));
2261
2262 return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
2263}
2264
2265
2266xs_list *tag_search(const char *tag, int skip, int show)
2267/* returns the list of posts tagged with tag */
2268{
2269 xs *idx = tag_fn(tag);
2270
2271 return index_list_desc(idx, skip, show);
2272}
2273
2274
2275/** lists **/
2276
2277xs_val *list_maint(snac *user, const char *list, int op)
2278/* list maintenance */
2279{
2280 xs_val *l = NULL;
2281
2282 switch (op) {
2283 case 0: /** list of lists **/
2284 {
2285 FILE *f;
2286 xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
2287 xs *ls = xs_glob(spec, 0, 0);
2288 int c = 0;
2289 const char *v;
2290
2291 l = xs_list_new();
2292
2293 while (xs_list_next(ls, &v, &c)) {
2294 if ((f = fopen(v, "r")) != NULL) {
2295 xs *title = xs_readline(f);
2296 fclose(f);
2297
2298 title = xs_strip_i(title);
2299
2300 xs *v2 = xs_replace(v, ".id", "");
2301 xs *l2 = xs_split(v2, "/");
2302
2303 /* return [ list_id, list_title ] */
2304 xs *tmp_list = xs_list_append(xs_list_new(), xs_list_get(l2, -1), title);
2305 l = xs_list_append(l, tmp_list);
2306 }
2307 }
2308 }
2309
2310 break;
2311
2312 case 1: /** create new list (list is the name) **/
2313 {
2314 xs *lol = list_maint(user, NULL, 0);
2315 int c = 0;
2316 const xs_list *v;
2317 int add = 1;
2318
2319 /* check if this list name already exists */
2320 while (xs_list_next(lol, &v, &c)) {
2321 if (strcmp(xs_list_get(v, 1), list) == 0) {
2322 add = 0;
2323
2324 l = xs_dup(xs_list_get(v, 0));
2325
2326 break;
2327 }
2328 }
2329
2330 if (add) {
2331 FILE *f;
2332 xs *dir = xs_fmt("%s/list/", user->basedir);
2333
2334 l = xs_fmt("%010x", time(NULL));
2335
2336 mkdirx(dir);
2337
2338 xs *fn = xs_fmt("%s%s.id", dir, l);
2339
2340 if ((f = fopen(fn, "w")) != NULL) {
2341 fprintf(f, "%s\n", list);
2342 fclose(f);
2343 }
2344 }
2345 }
2346
2347 break;
2348
2349 case 2: /** delete list (list is the id) **/
2350 {
2351 if (xs_is_hex(list)) {
2352 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2353 unlink(fn);
2354
2355 fn = xs_replace_i(fn, ".id", ".lst");
2356 unlink(fn);
2357
2358 fn = xs_replace_i(fn, ".lst", ".idx");
2359 unlink(fn);
2360
2361 fn = xs_str_cat(fn, ".bak");
2362 unlink(fn);
2363 }
2364 }
2365
2366 break;
2367
2368 case 3: /** get list name **/
2369 if (xs_is_hex(list)) {
2370 FILE *f;
2371 xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2372
2373 if ((f = fopen(fn, "r")) != NULL) {
2374 l = xs_strip_i(xs_readline(f));
2375 fclose(f);
2376 }
2377 }
2378
2379 break;
2380
2381 case 4: /** find list id by name **/
2382 if (xs_is_string(list)) {
2383 xs *lol = list_maint(user, NULL, 0);
2384 const xs_list *li;
2385
2386 xs_list_foreach(lol, li) {
2387 if (strcmp(list, xs_list_get(li, 1)) == 0) {
2388 l = xs_dup(xs_list_get(li, 0));
2389 break;
2390 }
2391 }
2392 }
2393 }
2394
2395 return l;
2396}
2397
2398
2399xs_str *list_timeline_fn(snac *user, const char *list)
2400{
2401 return xs_fmt("%s/list/%s.idx", user->basedir, list);
2402}
2403
2404
2405xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2406/* returns the timeline of a list */
2407{
2408 xs_list *l = NULL;
2409
2410 if (!xs_is_hex(list))
2411 return NULL;
2412
2413 xs *fn = list_timeline_fn(user, list);
2414
2415 if (mtime(fn) > 0.0)
2416 l = index_list_desc(fn, skip, show);
2417 else
2418 l = xs_list_new();
2419
2420 return l;
2421}
2422
2423
2424xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
2425/* list content management */
2426{
2427 xs_val *l = NULL;
2428
2429 if (!xs_is_hex(list))
2430 return NULL;
2431
2432 if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2433 return NULL;
2434
2435 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2436
2437 switch (op) {
2438 case 0: /** list content **/
2439 l = index_list(fn, XS_ALL);
2440
2441 break;
2442
2443 case 1: /** append actor to list **/
2444 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) {
2445 if (!index_in_md5(fn, actor_md5))
2446 index_add_md5(fn, actor_md5);
2447 }
2448
2449 break;
2450
2451 case 2: /** delete actor from list **/
2452 if (xs_is_string(actor_md5) && xs_is_hex(actor_md5))
2453 index_del_md5(fn, actor_md5);
2454
2455 break;
2456
2457 default:
2458 srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2459 break;
2460 }
2461
2462 return l;
2463}
2464
2465
2466void list_distribute(snac *user, const char *who, const xs_dict *post)
2467/* distributes the post to all appropriate lists */
2468{
2469 const char *id = xs_dict_get(post, "id");
2470
2471 /* if who is not set, use the attributedTo in the message */
2472 if (xs_is_null(who))
2473 who = get_atto(post);
2474
2475 if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2476 xs *a_md5 = xs_md5_hex(who, strlen(who));
2477 xs *i_md5 = xs_md5_hex(id, strlen(id));
2478 xs *spec = xs_fmt("%s/list/" "*.lst", user->basedir);
2479 xs *ls = xs_glob(spec, 0, 0);
2480 int c = 0;
2481 const char *v;
2482
2483 while (xs_list_next(ls, &v, &c)) {
2484 /* is the actor in this list? */
2485 if (index_in_md5(v, a_md5)) {
2486 /* it is; add post md5 to its timeline */
2487 xs *idx = xs_replace(v, ".lst", ".idx");
2488 index_add_md5(idx, i_md5);
2489
2490 snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2491 }
2492 }
2493 }
2494}
2495
2496
2497/** static data **/
2498
2499static int _load_raw_file(const char *fn, xs_val **data, int *size,
2500 const char *inm, xs_str **etag)
2501/* loads a cached file */
2502{
2503 int status = HTTP_STATUS_NOT_FOUND;
2504
2505 if (fn) {
2506 double tm = mtime(fn);
2507
2508 if (tm > 0.0) {
2509 /* file exists; build the etag */
2510 xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2511
2512 /* if if-none-match is set, check if it's the same */
2513 if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2514 /* client has the newest version */
2515 status = HTTP_STATUS_NOT_MODIFIED;
2516 }
2517 else {
2518 /* newer or never downloaded; read the full file */
2519 FILE *f;
2520
2521 if ((f = fopen(fn, "rb")) != NULL) {
2522 *size = XS_ALL;
2523 *data = xs_read(f, size);
2524 fclose(f);
2525
2526 status = HTTP_STATUS_OK;
2527 }
2528 }
2529
2530 /* if caller wants the etag, return it */
2531 if (etag != NULL)
2532 *etag = xs_dup(e);
2533
2534 srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2535 }
2536 }
2537
2538 return status;
2539}
2540
2541
2542xs_str *_static_fn(snac *snac, const char *id)
2543/* gets the filename for a static file */
2544{
2545 if (strchr(id, '/'))
2546 return NULL;
2547 else
2548 return xs_fmt("%s/static/%s", snac->basedir, id);
2549}
2550
2551
2552int static_get(snac *snac, const char *id, xs_val **data, int *size,
2553 const char *inm, xs_str **etag)
2554/* returns static content */
2555{
2556 xs *fn = _static_fn(snac, id);
2557
2558 return _load_raw_file(fn, data, size, inm, etag);
2559}
2560
2561
2562void static_put(snac *snac, const char *id, const char *data, int size)
2563/* writes status content */
2564{
2565 xs *fn = _static_fn(snac, id);
2566 FILE *f;
2567
2568 if (fn && (f = fopen(fn, "wb")) != NULL) {
2569 fwrite(data, size, 1, f);
2570 fclose(f);
2571 }
2572}
2573
2574
2575void static_put_meta(snac *snac, const char *id, const char *str)
2576/* puts metadata (i.e. a media description string) to id */
2577{
2578 xs *fn = _static_fn(snac, id);
2579
2580 if (fn) {
2581 fn = xs_str_cat(fn, ".txt");
2582 FILE *f;
2583
2584 if ((f = fopen(fn, "w")) != NULL) {
2585 fprintf(f, "%s\n", str);
2586 fclose(f);
2587 }
2588 }
2589}
2590
2591
2592xs_str *static_get_meta(snac *snac, const char *id)
2593/* gets metadata from a media */
2594{
2595 xs *fn = _static_fn(snac, id);
2596 xs_str *r = NULL;
2597
2598 if (fn) {
2599 fn = xs_str_cat(fn, ".txt");
2600 FILE *f;
2601
2602 if ((f = fopen(fn, "r")) != NULL) {
2603 r = xs_strip_i(xs_readline(f));
2604 fclose(f);
2605 }
2606 }
2607 else
2608 r = xs_str_new("");
2609
2610 return r;
2611}
2612
2613
2614/** history **/
2615
2616xs_str *_history_fn(snac *snac, const char *id)
2617/* gets the filename for the history */
2618{
2619 if (strchr(id, '/'))
2620 return NULL;
2621 else
2622 return xs_fmt("%s/history/%s", snac->basedir, id);
2623}
2624
2625
2626double history_mtime(snac *snac, const char *id)
2627{
2628 double t = 0.0;
2629 xs *fn = _history_fn(snac, id);
2630
2631 if (fn != NULL)
2632 t = mtime(fn);
2633
2634 return t;
2635}
2636
2637
2638void history_add(snac *snac, const char *id, const char *content, int size,
2639 xs_str **etag)
2640/* adds something to the history */
2641{
2642 xs *fn = _history_fn(snac, id);
2643 FILE *f;
2644
2645 if (fn && (f = fopen(fn, "w")) != NULL) {
2646 fwrite(content, size, 1, f);
2647 fclose(f);
2648
2649 if (etag) {
2650 double tm = mtime(fn);
2651 *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2652 }
2653 }
2654}
2655
2656
2657int history_get(snac *snac, const char *id, xs_str **content, int *size,
2658 const char *inm, xs_str **etag)
2659{
2660 xs *fn = _history_fn(snac, id);
2661
2662 return _load_raw_file(fn, content, size, inm, etag);
2663}
2664
2665
2666int history_del(snac *snac, const char *id)
2667{
2668 xs *fn = _history_fn(snac, id);
2669
2670 if (fn)
2671 return unlink(fn);
2672 else
2673 return -1;
2674}
2675
2676
2677xs_list *history_list(snac *snac)
2678{
2679 xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2680
2681 return xs_glob(spec, 1, 1);
2682}
2683
2684
2685void lastlog_write(snac *snac, const char *source)
2686/* writes the last time the user logged in */
2687{
2688 xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2689 FILE *f;
2690
2691 if ((f = fopen(fn, "w")) != NULL) {
2692 fprintf(f, "%lf %s\n", ftime(), source);
2693 fclose(f);
2694 }
2695}
2696
2697
2698/** inbox collection **/
2699
2700void inbox_add(const char *inbox)
2701/* collects a shared inbox */
2702{
2703 /* don't collect ourselves */
2704 if (xs_startswith(inbox, srv_baseurl))
2705 return;
2706
2707 xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2708 xs *fn = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2709 FILE *f;
2710
2711 if ((f = fopen(fn, "w")) != NULL) {
2712 fprintf(f, "%s\n", inbox);
2713 fclose(f);
2714 }
2715}
2716
2717
2718void inbox_add_by_actor(const xs_dict *actor)
2719/* collects an actor's shared inbox, if it has one */
2720{
2721 const char *v;
2722
2723 if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2724 !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2725 /* only collect this inbox if its instance is not blocked */
2726 if (!is_instance_blocked(v))
2727 inbox_add(v);
2728 }
2729}
2730
2731
2732xs_list *inbox_list(void)
2733/* returns the collected inboxes as a list */
2734{
2735 xs_list *ibl = xs_list_new();
2736 xs *spec = xs_fmt("%s/inbox/" "*", srv_basedir);
2737 xs *files = xs_glob(spec, 0, 0);
2738 const xs_val *v;
2739
2740 xs_list_foreach(files, v) {
2741 FILE *f;
2742
2743 if ((f = fopen(v, "r")) != NULL) {
2744 xs *line = xs_readline(f);
2745
2746 if (line && *line) {
2747 line = xs_strip_i(line);
2748
2749 if (!is_instance_blocked(line))
2750 ibl = xs_list_append(ibl, line);
2751 }
2752
2753 fclose(f);
2754 }
2755 }
2756
2757 return ibl;
2758}
2759
2760
2761/** instance-wide operations **/
2762
2763xs_str *_instance_block_fn(const char *instance)
2764{
2765 xs *s = xs_replace(instance, "http:/" "/", "");
2766 xs *s1 = xs_replace(s, "https:/" "/", "");
2767 xs *l = xs_split(s1, "/");
2768 const char *p = xs_list_get(l, 0);
2769 xs *md5 = xs_md5_hex(p, strlen(p));
2770
2771 return xs_fmt("%s/block/%s", srv_basedir, md5);
2772}
2773
2774
2775int is_instance_blocked(const char *instance)
2776{
2777 xs *fn = _instance_block_fn(instance);
2778
2779 return !!(mtime(fn) != 0.0);
2780}
2781
2782
2783int instance_block(const char *instance)
2784/* blocks a full instance */
2785{
2786 int ret;
2787
2788 /* create the subdir */
2789 xs *dir = xs_fmt("%s/block/", srv_basedir);
2790 mkdirx(dir);
2791
2792 if (!is_instance_blocked(instance)) {
2793 xs *fn = _instance_block_fn(instance);
2794 FILE *f;
2795
2796 if ((f = fopen(fn, "w")) != NULL) {
2797 fprintf(f, "%s\n", instance);
2798 fclose(f);
2799
2800 ret = 0;
2801 }
2802 else
2803 ret = -1;
2804 }
2805 else
2806 ret = -2;
2807
2808 return ret;
2809}
2810
2811
2812int instance_unblock(const char *instance)
2813/* unblocks a full instance */
2814{
2815 int ret;
2816
2817 if (is_instance_blocked(instance)) {
2818 xs *fn = _instance_block_fn(instance);
2819 ret = unlink(fn);
2820 }
2821 else
2822 ret = -2;
2823
2824 return ret;
2825}
2826
2827
2828/** operations by content **/
2829
2830int content_match(const char *file, const xs_dict *msg)
2831/* checks if a message's content matches any of the regexes in file */
2832/* file format: one regex per line */
2833{
2834 xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2835 FILE *f;
2836 int r = 0;
2837 const char *v = xs_dict_get(msg, "content");
2838
2839 if (xs_type(v) == XSTYPE_STRING && *v) {
2840 if ((f = fopen(fn, "r")) != NULL) {
2841 srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2842
2843 /* massage content (strip HTML tags, etc.) */
2844 xs *c1 = xs_regex_replace(v, "<[^>]+>", " ");
2845 c1 = xs_regex_replace_i(c1, " {2,}", " ");
2846 xs *c = xs_utf8_to_lower(c1);
2847
2848 while (!r && !feof(f)) {
2849 xs *rx = xs_strip_i(xs_readline(f));
2850
2851 if (*rx && xs_regex_match(c, rx)) {
2852 srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2853 r = 1;
2854 }
2855 }
2856
2857 fclose(f);
2858 }
2859 }
2860
2861 return r;
2862}
2863
2864
2865xs_list *content_search(snac *user, const char *regex,
2866 int priv, int skip, int show, int max_secs, int *timeout)
2867/* returns a list of posts which content matches the regex */
2868{
2869 if (regex == NULL || *regex == '\0')
2870 return xs_list_new();
2871
2872 xs *i_regex = xs_utf8_to_lower(regex);
2873
2874 xs_set seen;
2875
2876 xs_set_init(&seen);
2877
2878 if (max_secs == 0)
2879 max_secs = 3;
2880
2881 time_t t = time(NULL) + max_secs;
2882 *timeout = 0;
2883
2884 /* iterate all timelines simultaneously */
2885 xs_list *tls[3] = {0};
2886 const char *md5s[3] = {0};
2887 int c[3] = {0};
2888
2889 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */
2890 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2891 tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
2892
2893 /* first positioning */
2894 for (int n = 0; n < 3; n++)
2895 xs_list_next(tls[n], &md5s[n], &c[n]);
2896
2897 show += skip;
2898
2899 while (show > 0) {
2900 /* timeout? */
2901 if (time(NULL) > t) {
2902 *timeout = 1;
2903 break;
2904 }
2905
2906 /* find the newest post */
2907 int newest = -1;
2908 double mtime = 0.0;
2909
2910 for (int n = 0; n < 3; n++) {
2911 if (md5s[n] != NULL) {
2912 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2913 double mt;
2914
2915 while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
2916 /* object is not here: move to the next one */
2917 if (xs_list_next(tls[n], &md5s[n], &c[n])) {
2918 xs_free(fn);
2919 fn = _object_fn_by_md5(md5s[n], "content_search_2");
2920 }
2921 else
2922 md5s[n] = NULL;
2923 }
2924
2925 if (mt > mtime) {
2926 newest = n;
2927 mtime = mt;
2928 }
2929 }
2930 }
2931
2932 if (newest == -1)
2933 break;
2934
2935 const char *md5 = md5s[newest];
2936
2937 /* advance the chosen timeline */
2938 if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2939 md5s[newest] = NULL;
2940
2941 xs *post = NULL;
2942
2943 if (!valid_status(object_get_by_md5(md5, &post)))
2944 continue;
2945
2946 if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2947 continue;
2948
2949 const char *id = xs_dict_get(post, "id");
2950
2951 if (id == NULL || is_hidden(user, id))
2952 continue;
2953
2954 /* recalculate the md5 id to be sure it's not repeated
2955 (it may have been searched by the "url" field instead of "id") */
2956 xs *new_md5 = xs_md5_hex(id, strlen(id));
2957 md5 = new_md5;
2958
2959 /* test for the post URL */
2960 if (strcmp(id, regex) == 0) {
2961 if (xs_set_add(&seen, md5) == 1)
2962 show--;
2963
2964 continue;
2965 }
2966
2967 /* test for the alternate post id */
2968 const char *url = xs_dict_get(post, "url");
2969 if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
2970 if (xs_set_add(&seen, md5) == 1)
2971 show--;
2972
2973 continue;
2974 }
2975
2976 xs *c = xs_str_new(NULL);
2977 const char *content = xs_dict_get(post, "content");
2978 const char *name = xs_dict_get(post, "name");
2979
2980 if (!xs_is_null(content))
2981 c = xs_str_cat(c, content);
2982 if (!xs_is_null(name))
2983 c = xs_str_cat(c, " ", name);
2984
2985 /* add alt-texts from attachments */
2986 const xs_list *atts = xs_dict_get(post, "attachment");
2987 int tc = 0;
2988 const xs_dict *att;
2989
2990 while (xs_list_next(atts, &att, &tc)) {
2991 const char *name = xs_dict_get(att, "name");
2992
2993 if (name != NULL)
2994 c = xs_str_cat(c, " ", name);
2995 }
2996
2997 /* strip HTML */
2998 c = xs_regex_replace_i(c, "<[^>]+>", " ");
2999 c = xs_regex_replace_i(c, " {2,}", " ");
3000
3001 /* convert to lowercase */
3002 xs *lc = xs_utf8_to_lower(c);
3003
3004 /* apply regex */
3005 if (xs_regex_match(lc, i_regex)) {
3006 if (xs_set_add(&seen, md5) == 1)
3007 show--;
3008 }
3009 }
3010
3011 xs_list *r = xs_set_result(&seen);
3012
3013 if (skip) {
3014 /* BAD */
3015 while (skip--) {
3016 r = xs_list_del(r, 0);
3017 }
3018 }
3019
3020 xs_free(tls[0]);
3021 xs_free(tls[1]);
3022 xs_free(tls[2]);
3023
3024 return r;
3025}
3026
3027
3028/** notifications **/
3029
3030xs_str *notify_check_time(snac *snac, int reset)
3031/* gets or resets the latest notification check time */
3032{
3033 xs_str *t = NULL;
3034 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
3035 FILE *f;
3036
3037 if (reset) {
3038 if ((f = fopen(fn, "w")) != NULL) {
3039 t = tid(0);
3040 fprintf(f, "%s\n", t);
3041 fclose(f);
3042 }
3043 }
3044 else {
3045 if ((f = fopen(fn, "r")) != NULL) {
3046 t = xs_readline(f);
3047 fclose(f);
3048 }
3049 else
3050 /* never set before */
3051 t = xs_fmt("%16.6f", 0.0);
3052 }
3053
3054 return t;
3055}
3056
3057xs_dict *markers_get(snac *snac, const xs_list *markers)
3058{
3059 xs *data = NULL;
3060 xs_dict *returns = xs_dict_new();
3061 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3062 const xs_str *v = NULL;
3063 FILE *f;
3064
3065 if ((f = fopen(fn, "r")) != NULL) {
3066 data = xs_json_load(f);
3067 fclose(f);
3068 }
3069
3070 if (xs_is_null(data))
3071 data = xs_dict_new();
3072
3073 xs_list_foreach(markers, v) {
3074 const xs_dict *mark = xs_dict_get(data, v);
3075 if (!xs_is_null(mark)) {
3076 returns = xs_dict_append(returns, v, mark);
3077 }
3078 }
3079 return returns;
3080}
3081
3082xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
3083/* gets or sets notification marker */
3084{
3085 xs *data = NULL;
3086 xs_dict *written = xs_dict_new();
3087 xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3088 FILE *f;
3089
3090 if ((f = fopen(fn, "r")) != NULL) {
3091 data = xs_json_load(f);
3092 fclose(f);
3093 }
3094
3095 if (xs_is_null(data))
3096 data = xs_dict_new();
3097
3098 if (!xs_is_null(home_marker)) {
3099 xs *home = xs_dict_new();
3100 xs *s_tid = tid(0);
3101 home = xs_dict_append(home, "last_read_id", home_marker);
3102 home = xs_dict_append(home, "version", xs_stock(0));
3103 home = xs_dict_append(home, "updated_at", s_tid);
3104 data = xs_dict_set(data, "home", home);
3105 written = xs_dict_append(written, "home", home);
3106 }
3107
3108 if (!xs_is_null(notify_marker)) {
3109 xs *notify = xs_dict_new();
3110 xs *s_tid = tid(0);
3111 notify = xs_dict_append(notify, "last_read_id", notify_marker);
3112 notify = xs_dict_append(notify, "version", xs_stock(0));
3113 notify = xs_dict_append(notify, "updated_at", s_tid);
3114 data = xs_dict_set(data, "notifications", notify);
3115 written = xs_dict_append(written, "notifications", notify);
3116 }
3117
3118 if ((f = fopen(fn, "w")) != NULL) {
3119 xs_json_dump(data, 4, f);
3120 fclose(f);
3121 }
3122
3123 return written;
3124}
3125
3126void notify_add(snac *snac, const char *type, const char *utype,
3127 const char *actor, const char *objid, const xs_dict *msg)
3128/* adds a new notification */
3129{
3130 xs *ntid = tid(0);
3131 xs *fn = xs_fmt("%s/notify/", snac->basedir);
3132 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3133 FILE *f;
3134
3135 /* create the directory */
3136 mkdirx(fn);
3137 fn = xs_str_cat(fn, ntid);
3138 fn = xs_str_cat(fn, ".json");
3139
3140 xs *noti = xs_dict_new();
3141
3142 noti = xs_dict_append(noti, "id", ntid);
3143 noti = xs_dict_append(noti, "type", type);
3144 noti = xs_dict_append(noti, "utype", utype);
3145 noti = xs_dict_append(noti, "actor", actor);
3146 noti = xs_dict_append(noti, "date", date);
3147 noti = xs_dict_append(noti, "msg", msg);
3148
3149 if (!xs_is_null(objid))
3150 noti = xs_dict_append(noti, "objid", objid);
3151
3152 if ((f = fopen(fn, "w")) != NULL) {
3153 xs_json_dump(noti, 4, f);
3154 fclose(f);
3155 }
3156
3157 /* add it to the index if it already exists */
3158 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3159
3160 if (mtime(idx) != 0.0) {
3161 pthread_mutex_lock(&data_mutex);
3162
3163 if ((f = fopen(idx, "a")) != NULL) {
3164 fprintf(f, "%-32s\n", ntid);
3165 fclose(f);
3166 }
3167
3168 pthread_mutex_unlock(&data_mutex);
3169 }
3170
3171 if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook")))
3172 enqueue_notify_webhook(snac, noti, 0);
3173}
3174
3175
3176xs_dict *notify_get(snac *snac, const char *id)
3177/* gets a notification */
3178{
3179 /* base file */
3180 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
3181
3182 /* strip spaces and add extension */
3183 fn = xs_strip_i(fn);
3184 fn = xs_str_cat(fn, ".json");
3185
3186 FILE *f;
3187 xs_dict *out = NULL;
3188
3189 if ((f = fopen(fn, "r")) != NULL) {
3190 out = xs_json_load(f);
3191 fclose(f);
3192 }
3193
3194 return out;
3195}
3196
3197
3198xs_list *notify_list(snac *snac, int skip, int show)
3199/* returns a list of notification ids */
3200{
3201 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3202
3203 if (mtime(idx) == 0.0) {
3204 /* create the index from scratch */
3205 FILE *f;
3206
3207 pthread_mutex_lock(&data_mutex);
3208
3209 if ((f = fopen(idx, "w")) != NULL) {
3210 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
3211 xs *lst = xs_glob(spec, 1, 0);
3212 xs_list *p = lst;
3213 const char *v;
3214
3215 while (xs_list_iter(&p, &v)) {
3216 char *p = strrchr(v, '.');
3217 if (p) {
3218 *p = '\0';
3219 fprintf(f, "%-32s\n", v);
3220 }
3221 }
3222
3223 fclose(f);
3224 }
3225
3226 pthread_mutex_unlock(&data_mutex);
3227 }
3228
3229 return index_list_desc(idx, skip, show);
3230}
3231
3232
3233int notify_new_num(snac *snac)
3234/* counts the number of new notifications */
3235{
3236 xs *t = notify_check_time(snac, 0);
3237 xs *lst = notify_list(snac, 0, XS_ALL);
3238 int cnt = 0;
3239
3240 xs_list *p = lst;
3241 const xs_str *v;
3242
3243 while (xs_list_iter(&p, &v)) {
3244 xs *id = xs_strip_i(xs_dup(v));
3245
3246 /* old? count no more */
3247 if (strcmp(id, t) < 0)
3248 break;
3249
3250 cnt++;
3251 }
3252
3253 return cnt;
3254}
3255
3256
3257void notify_clear(snac *snac)
3258/* clears all notifications */
3259{
3260 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
3261 xs *lst = xs_glob(spec, 0, 0);
3262 xs_list *p = lst;
3263 const xs_str *v;
3264
3265 while (xs_list_iter(&p, &v))
3266 unlink(v);
3267
3268 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3269
3270 if (mtime(idx) != 0.0) {
3271 pthread_mutex_lock(&data_mutex);
3272 truncate(idx, 0);
3273 pthread_mutex_unlock(&data_mutex);
3274 }
3275}
3276
3277
3278/** the queue **/
3279
3280static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
3281/* writes safely to the queue */
3282{
3283 xs *tfn = xs_fmt("%s.tmp", fn);
3284 FILE *f;
3285
3286 if ((f = fopen(tfn, "w")) != NULL) {
3287 xs_json_dump(msg, 4, f);
3288 fclose(f);
3289
3290 rename(tfn, fn);
3291 }
3292
3293 return msg;
3294}
3295
3296
3297static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
3298/* creates a queue message */
3299{
3300 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
3301 xs *ntid = tid(retries * 60 * qrt);
3302 xs *rn = xs_number_new(retries);
3303
3304 xs_dict *qmsg = xs_dict_new();
3305
3306 qmsg = xs_dict_append(qmsg, "type", type);
3307 qmsg = xs_dict_append(qmsg, "message", msg);
3308 qmsg = xs_dict_append(qmsg, "retries", rn);
3309 qmsg = xs_dict_append(qmsg, "ntid", ntid);
3310
3311 return qmsg;
3312}
3313
3314
3315void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3316/* enqueues an input message */
3317{
3318 xs *qmsg = _new_qmsg("input", msg, retries);
3319 const char *ntid = xs_dict_get(qmsg, "ntid");
3320 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3321
3322 qmsg = xs_dict_append(qmsg, "req", req);
3323
3324 qmsg = _enqueue_put(fn, qmsg);
3325
3326 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3327}
3328
3329
3330void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3331/* enqueues an input message from the shared input */
3332{
3333 xs *qmsg = _new_qmsg("input", msg, retries);
3334 const char *ntid = xs_dict_get(qmsg, "ntid");
3335 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3336
3337 qmsg = xs_dict_append(qmsg, "req", req);
3338
3339 qmsg = _enqueue_put(fn, qmsg);
3340
3341 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3342}
3343
3344
3345void enqueue_output_raw(const char *keyid, const char *seckey,
3346 const xs_dict *msg, const xs_str *inbox,
3347 int retries, int p_status)
3348/* enqueues an output message to an inbox */
3349{
3350 xs *qmsg = _new_qmsg("output", msg, retries);
3351 const char *ntid = xs_dict_get(qmsg, "ntid");
3352 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3353
3354 xs *ns = xs_number_new(p_status);
3355 qmsg = xs_dict_append(qmsg, "p_status", ns);
3356
3357 qmsg = xs_dict_append(qmsg, "inbox", inbox);
3358 qmsg = xs_dict_append(qmsg, "keyid", keyid);
3359 qmsg = xs_dict_append(qmsg, "seckey", seckey);
3360
3361 /* if it's to be sent right now, bypass the disk queue and post the job */
3362 if (retries == 0 && p_state != NULL)
3363 job_post(qmsg, 0);
3364 else {
3365 qmsg = _enqueue_put(fn, qmsg);
3366 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3367 }
3368}
3369
3370
3371void enqueue_output(snac *snac, const xs_dict *msg,
3372 const xs_str *inbox, int retries, int p_status)
3373/* enqueues an output message to an inbox */
3374{
3375 if (xs_startswith(inbox, snac->actor)) {
3376 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3377 return;
3378 }
3379
3380 const char *seckey = xs_dict_get(snac->key, "secret");
3381
3382 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3383}
3384
3385
3386void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3387 const xs_str *actor, int retries)
3388/* enqueues an output message for an actor */
3389{
3390 xs *inbox = get_actor_inbox(actor, 1);
3391
3392 if (!xs_is_null(inbox))
3393 enqueue_output(snac, msg, inbox, retries, 0);
3394 else
3395 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3396}
3397
3398
3399void enqueue_email(const xs_dict *msg, int retries)
3400/* enqueues an email message to be sent */
3401{
3402 xs *qmsg = _new_qmsg("email", msg, retries);
3403 const char *ntid = xs_dict_get(qmsg, "ntid");
3404 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3405
3406 qmsg = _enqueue_put(fn, qmsg);
3407
3408 srv_debug(1, xs_fmt("enqueue_email %d", retries));
3409}
3410
3411
3412void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3413/* enqueues a message to be sent via Telegram */
3414{
3415 xs *qmsg = _new_qmsg("telegram", msg, 0);
3416 const char *ntid = xs_dict_get(qmsg, "ntid");
3417 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3418
3419 qmsg = xs_dict_append(qmsg, "bot", bot);
3420 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
3421
3422 qmsg = _enqueue_put(fn, qmsg);
3423
3424 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3425}
3426
3427void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3428/* enqueues a message to be sent via ntfy */
3429{
3430 xs *qmsg = _new_qmsg("ntfy", msg, 0);
3431 const char *ntid = xs_dict_get(qmsg, "ntid");
3432 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3433
3434 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3435 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
3436
3437
3438 qmsg = _enqueue_put(fn, qmsg);
3439
3440 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3441}
3442
3443void enqueue_message(snac *snac, const xs_dict *msg)
3444/* enqueues an output message */
3445{
3446 xs *qmsg = _new_qmsg("message", msg, 0);
3447 const char *ntid = xs_dict_get(qmsg, "ntid");
3448 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3449
3450 qmsg = _enqueue_put(fn, qmsg);
3451
3452 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3453}
3454
3455
3456void enqueue_close_question(snac *user, const char *id, int end_secs)
3457/* enqueues the closing of a question */
3458{
3459 xs *qmsg = _new_qmsg("close_question", id, 0);
3460 xs *ntid = tid(end_secs);
3461 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3462
3463 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3464
3465 qmsg = _enqueue_put(fn, qmsg);
3466
3467 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3468}
3469
3470
3471void enqueue_object_request(snac *user, const char *id, int forward_secs)
3472/* enqueues the request of an object in the future */
3473{
3474 xs *qmsg = _new_qmsg("object_request", id, 0);
3475 xs *ntid = tid(forward_secs);
3476 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3477
3478 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3479
3480 qmsg = _enqueue_put(fn, qmsg);
3481
3482 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3483}
3484
3485
3486void enqueue_verify_links(snac *user)
3487/* enqueues a link verification */
3488{
3489 xs *qmsg = _new_qmsg("verify_links", "", 0);
3490 const char *ntid = xs_dict_get(qmsg, "ntid");
3491 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3492
3493 qmsg = _enqueue_put(fn, qmsg);
3494
3495 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3496}
3497
3498
3499void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3500/* enqueues an actor refresh */
3501{
3502 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3503 xs *ntid = tid(forward_secs);
3504 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3505
3506 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3507 qmsg = xs_dict_append(qmsg, "actor", actor);
3508
3509 qmsg = _enqueue_put(fn, qmsg);
3510
3511 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3512}
3513
3514
3515void enqueue_webmention(const xs_dict *msg)
3516/* enqueues a webmention for the post */
3517{
3518 xs *qmsg = _new_qmsg("webmention", msg, 0);
3519 const char *ntid = xs_dict_get(qmsg, "ntid");
3520 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3521
3522 qmsg = _enqueue_put(fn, qmsg);
3523
3524 srv_debug(1, xs_fmt("enqueue_webmention"));
3525}
3526
3527
3528void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries)
3529/* enqueues a notification webhook */
3530{
3531 const char *webhook = xs_dict_get(user->config, "notify_webhook");
3532
3533 if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3534 xs *msg = xs_dup(noti);
3535
3536 /* add more data */
3537 msg = xs_dict_set(msg, "target", user->actor);
3538 msg = xs_dict_set(msg, "uid", user->uid);
3539 msg = xs_dict_set(msg, "basedir", srv_basedir);
3540 msg = xs_dict_set(msg, "baseurl", srv_baseurl);
3541
3542 xs *actor_obj = NULL;
3543
3544 if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj)
3545 msg = xs_dict_set(msg, "account", actor_obj);
3546
3547 /* if this post is a reply, also add the inReplyTo object */
3548 const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo");
3549
3550 if (xs_is_string(in_reply_to)) {
3551 xs *irt_obj = NULL;
3552
3553 if (valid_status(object_get(in_reply_to, &irt_obj)))
3554 msg = xs_dict_set(msg, "reply", irt_obj);
3555 }
3556
3557 xs *qmsg = _new_qmsg("notify_webhook", msg, retries);
3558 const char *ntid = xs_dict_get(qmsg, "ntid");
3559 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3560
3561 qmsg = _enqueue_put(fn, qmsg);
3562
3563 snac_debug(user, 1, xs_fmt("notify_webhook"));
3564 }
3565}
3566
3567
3568int was_question_voted(snac *user, const char *id)
3569/* returns true if the user voted in this poll */
3570{
3571 xs *children = object_children(id);
3572 int voted = 0;
3573 xs_list *p;
3574 const xs_str *md5;
3575
3576 p = children;
3577 while (xs_list_iter(&p, &md5)) {
3578 xs *obj = NULL;
3579
3580 if (valid_status(object_get_by_md5(md5, &obj))) {
3581 const char *atto = get_atto(obj);
3582 if (atto && strcmp(atto, user->actor) == 0 &&
3583 !xs_is_null(xs_dict_get(obj, "name"))) {
3584 voted = 1;
3585 break;
3586 }
3587 }
3588 }
3589
3590 return voted;
3591}
3592
3593
3594xs_list *user_queue(snac *snac)
3595/* returns a list with filenames that can be dequeued */
3596{
3597 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
3598 xs_list *list = xs_list_new();
3599 time_t t = time(NULL);
3600 xs_list *p;
3601 const xs_val *v;
3602
3603 xs *fns = xs_glob(spec, 0, 0);
3604
3605 p = fns;
3606 while (xs_list_iter(&p, &v)) {
3607 /* get the retry time from the basename */
3608 char *bn = strrchr(v, '/');
3609 time_t t2 = atol(bn + 1);
3610
3611 if (t2 > t)
3612 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3613 else {
3614 list = xs_list_append(list, v);
3615 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3616 }
3617 }
3618
3619 return list;
3620}
3621
3622
3623xs_list *queue(void)
3624/* returns a list with filenames that can be dequeued */
3625{
3626 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
3627 xs_list *list = xs_list_new();
3628 time_t t = time(NULL);
3629 xs_list *p;
3630 const xs_val *v;
3631
3632 xs *fns = xs_glob(spec, 0, 0);
3633
3634 p = fns;
3635 while (xs_list_iter(&p, &v)) {
3636 /* get the retry time from the basename */
3637 char *bn = strrchr(v, '/');
3638 time_t t2 = atol(bn + 1);
3639
3640 if (t2 > t)
3641 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3642 else {
3643 list = xs_list_append(list, v);
3644 srv_debug(2, xs_fmt("queue ready for %s", v));
3645 }
3646 }
3647
3648 return list;
3649}
3650
3651
3652xs_dict *queue_get(const char *fn)
3653/* gets a file from a queue */
3654{
3655 FILE *f;
3656 xs_dict *obj = NULL;
3657
3658 if ((f = fopen(fn, "r")) != NULL) {
3659 obj = xs_json_load(f);
3660 fclose(f);
3661 }
3662
3663 return obj;
3664}
3665
3666
3667xs_dict *dequeue(const char *fn)
3668/* dequeues a message */
3669{
3670 xs_dict *obj = queue_get(fn);
3671
3672 unlink(fn);
3673
3674 return obj;
3675}
3676
3677
3678/** the purge **/
3679
3680static int _purge_file(const char *fn, time_t mt)
3681/* purge fn if it's older than days */
3682{
3683 int ret = 0;
3684
3685 if (mtime(fn) < mt) {
3686 /* older than the minimum time: delete it */
3687 unlink(fn);
3688 srv_debug(2, xs_fmt("purged %s", fn));
3689 ret = 1;
3690 }
3691
3692 return ret;
3693}
3694
3695
3696static void _purge_dir(const char *dir, int days)
3697/* purges all files in a directory older than days */
3698{
3699 int cnt = 0;
3700
3701 if (days) {
3702 time_t mt = time(NULL) - days * 24 * 3600;
3703 xs *spec = xs_fmt("%s/" "*", dir);
3704 xs *list = xs_glob(spec, 0, 0);
3705 xs_list *p;
3706 const xs_str *v;
3707
3708 p = list;
3709 while (xs_list_iter(&p, &v))
3710 cnt += _purge_file(v, mt);
3711
3712 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3713 }
3714}
3715
3716
3717static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3718/* purges all files in a user subdir older than days */
3719{
3720 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3721
3722 _purge_dir(u_subdir, days);
3723}
3724
3725
3726void purge_server(void)
3727/* purge global server data */
3728{
3729 xs *spec = xs_fmt("%s/object/??", srv_basedir);
3730 xs *dirs = xs_glob(spec, 0, 0);
3731 xs_list *p;
3732 const xs_str *v;
3733 int cnt = 0;
3734 int icnt = 0;
3735
3736 time_t mt = time(NULL) - 7 * 24 * 3600;
3737
3738 p = dirs;
3739 while (xs_list_iter(&p, &v)) {
3740 xs_list *p2;
3741 const xs_str *v2;
3742
3743 {
3744 xs *spec2 = xs_fmt("%s/" "*.json", v);
3745 xs *files = xs_glob(spec2, 0, 0);
3746
3747 p2 = files;
3748 while (xs_list_iter(&p2, &v2)) {
3749 int n_link;
3750
3751 /* old and with no hard links? */
3752 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3753 xs *s1 = xs_replace(v2, ".json", "");
3754 xs *l = xs_split(s1, "/");
3755 const char *md5 = xs_list_get(l, -1);
3756
3757 object_del_by_md5(md5);
3758 cnt++;
3759 }
3760 }
3761 }
3762
3763 {
3764 /* look for stray indexes */
3765 xs *speci = xs_fmt("%s/" "*_?.idx", v);
3766 xs *idxfs = xs_glob(speci, 0, 0);
3767
3768 p2 = idxfs;
3769 while (xs_list_iter(&p2, &v2)) {
3770 /* old enough to consider? */
3771 if (mtime(v2) < mt) {
3772 /* check if the indexed object is here */
3773 xs *o = xs_dup(v2);
3774 char *ext = strchr(o, '_');
3775
3776 if (ext) {
3777 *ext = '\0';
3778 o = xs_str_cat(o, ".json");
3779
3780 if (mtime(o) == 0.0) {
3781 /* delete */
3782 unlink(v2);
3783 srv_debug(1, xs_fmt("purged %s", v2));
3784 icnt++;
3785 }
3786 }
3787 }
3788 }
3789
3790 /* delete index backups */
3791 xs *specb = xs_fmt("%s/" "*.bak", v);
3792 xs *bakfs = xs_glob(specb, 0, 0);
3793
3794 p2 = bakfs;
3795 while (xs_list_iter(&p2, &v2)) {
3796 unlink(v2);
3797 srv_debug(1, xs_fmt("purged %s", v2));
3798 }
3799 }
3800 }
3801
3802 /* purge collected inboxes */
3803 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3804 _purge_dir(ib_dir, 7);
3805
3806 /* purge the instance timeline */
3807 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3808 int itl_gc = index_gc(itl_fn);
3809
3810 /* purge tag indexes */
3811 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3812 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3813 p = tag_dirs;
3814
3815 int tag_gc = 0;
3816 while (xs_list_iter(&p, &v)) {
3817 xs *spec2 = xs_fmt("%s/" "*.idx", v);
3818 xs *files = xs_glob(spec2, 0, 0);
3819 xs_list *p2;
3820 const xs_str *v2;
3821
3822 p2 = files;
3823 while (xs_list_iter(&p2, &v2)) {
3824 tag_gc += index_gc(v2);
3825 xs *bak = xs_fmt("%s.bak", v2);
3826 unlink(bak);
3827
3828 if (index_len(v2) == 0) {
3829 /* there are no longer any entry with this tag;
3830 purge it completely */
3831 unlink(v2);
3832 xs *dottag = xs_replace(v2, ".idx", ".tag");
3833 unlink(dottag);
3834 }
3835 }
3836 }
3837
3838 srv_debug(1, xs_fmt("purge: global "
3839 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3840}
3841
3842
3843void purge_user(snac *snac)
3844/* do the purge for this user */
3845{
3846 int priv_days, pub_days, user_days = 0;
3847 const char *v;
3848 int n;
3849
3850 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3851 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3852
3853 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3854 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3855 user_days = xs_number_get(v);
3856
3857 if (user_days) {
3858 /* override admin settings only if they are lesser */
3859 if (priv_days == 0 || user_days < priv_days)
3860 priv_days = user_days;
3861
3862 if (pub_days == 0 || user_days < pub_days)
3863 pub_days = user_days;
3864 }
3865
3866 _purge_user_subdir(snac, "hidden", priv_days);
3867 _purge_user_subdir(snac, "private", priv_days);
3868
3869 _purge_user_subdir(snac, "public", pub_days);
3870
3871 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
3872 "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
3873
3874 for (n = 0; idxs[n]; n++) {
3875 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3876 int gc = index_gc(idx);
3877 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3878 }
3879
3880 /* purge lists */
3881 {
3882 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3883 xs *lol = xs_glob(spec, 0, 0);
3884 int c = 0;
3885 const char *v;
3886
3887 while (xs_list_next(lol, &v, &c)) {
3888 int gc = index_gc(v);
3889 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3890 }
3891 }
3892
3893 /* unrelated to purging, but it's a janitorial process, so what the hell */
3894 verify_links(snac);
3895}
3896
3897
3898void purge_all(void)
3899/* purge all users */
3900{
3901 snac snac;
3902 xs *list = user_list();
3903 char *p;
3904 const char *uid;
3905
3906 p = list;
3907 while (xs_list_iter(&p, &uid)) {
3908 if (user_open(&snac, uid)) {
3909 purge_user(&snac);
3910 user_free(&snac);
3911 }
3912 }
3913
3914 purge_server();
3915
3916#ifndef NO_MASTODON_API
3917 mastoapi_purge();
3918#endif
3919}
3920
3921
3922/** archive **/
3923
3924void srv_archive(const char *direction, const char *url, xs_dict *req,
3925 const char *payload, int p_size,
3926 int status, xs_dict *headers,
3927 const char *body, int b_size)
3928/* archives a connection */
3929{
3930 /* obsessive archiving */
3931 xs *date = tid(0);
3932 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3933 FILE *f;
3934
3935 if (mkdirx(dir) != -1) {
3936 xs *meta_fn = xs_fmt("%s/_META", dir);
3937
3938 if ((f = fopen(meta_fn, "w")) != NULL) {
3939 xs *j1 = xs_json_dumps(req, 4);
3940 xs *j2 = xs_json_dumps(headers, 4);
3941
3942 fprintf(f, "dir: %s\n", direction);
3943
3944 if (url)
3945 fprintf(f, "url: %s\n", url);
3946
3947 fprintf(f, "req: %s\n", j1);
3948 fprintf(f, "p_size: %d\n", p_size);
3949 fprintf(f, "status: %d\n", status);
3950 fprintf(f, "response: %s\n", j2);
3951 fprintf(f, "b_size: %d\n", b_size);
3952 fclose(f);
3953 }
3954
3955 if (p_size && payload) {
3956 xs *payload_fn = NULL;
3957 xs *payload_fn_raw = NULL;
3958 const char *v = xs_dict_get(req, "content-type");
3959
3960 if (v && xs_str_in(v, "json") != -1) {
3961 payload_fn = xs_fmt("%s/payload.json", dir);
3962
3963 if ((f = fopen(payload_fn, "w")) != NULL) {
3964 xs *v1 = xs_json_loads(payload);
3965 xs *j1 = NULL;
3966
3967 if (v1 != NULL)
3968 j1 = xs_json_dumps(v1, 4);
3969
3970 if (j1 != NULL)
3971 fwrite(j1, strlen(j1), 1, f);
3972 else
3973 fwrite(payload, p_size, 1, f);
3974
3975 fclose(f);
3976 }
3977 }
3978
3979 payload_fn_raw = xs_fmt("%s/payload", dir);
3980
3981 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3982 fwrite(payload, p_size, 1, f);
3983 fclose(f);
3984 }
3985 }
3986
3987 if (b_size && body) {
3988 xs *body_fn = NULL;
3989 const char *v = xs_dict_get(headers, "content-type");
3990
3991 if (v && xs_str_in(v, "json") != -1) {
3992 body_fn = xs_fmt("%s/body.json", dir);
3993
3994 if ((f = fopen(body_fn, "w")) != NULL) {
3995 xs *v1 = xs_json_loads(body);
3996 xs *j1 = NULL;
3997
3998 if (v1 != NULL)
3999 j1 = xs_json_dumps(v1, 4);
4000
4001 if (j1 != NULL)
4002 fwrite(j1, strlen(j1), 1, f);
4003 else
4004 fwrite(body, b_size, 1, f);
4005
4006 fclose(f);
4007 }
4008 }
4009 else {
4010 body_fn = xs_fmt("%s/body", dir);
4011
4012 if ((f = fopen(body_fn, "w")) != NULL) {
4013 fwrite(body, b_size, 1, f);
4014 fclose(f);
4015 }
4016 }
4017 }
4018 }
4019}
4020
4021
4022void srv_archive_error(const char *prefix, const xs_str *err,
4023 const xs_dict *req, const xs_val *data)
4024/* archives an error */
4025{
4026 xs *ntid = tid(0);
4027 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
4028 FILE *f;
4029
4030 if ((f = fopen(fn, "w")) != NULL) {
4031 fprintf(f, "Error: %s\n", err);
4032
4033 if (req) {
4034 fprintf(f, "Request headers:\n");
4035
4036 xs_json_dump(req, 4, f);
4037
4038 fprintf(f, "\n");
4039 }
4040
4041 if (data) {
4042 fprintf(f, "Data:\n");
4043
4044 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
4045 xs_json_dump(data, 4, f);
4046 }
4047 else
4048 fprintf(f, "%s", data);
4049
4050 fprintf(f, "\n");
4051 }
4052
4053 fclose(f);
4054 }
4055}
4056
4057
4058void srv_archive_qitem(const char *prefix, xs_dict *q_item)
4059/* archives a q_item in the error folder */
4060{
4061 xs *ntid = tid(0);
4062 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
4063 FILE *f;
4064
4065 if ((f = fopen(fn, "w")) != NULL) {
4066 xs_json_dump(q_item, 4, f);
4067 fclose(f);
4068 }
4069}
4070
4071
4072t_announcement *announcement(const double after)
4073/* returns announcement text or NULL if none exists or it is olde than "after" */
4074{
4075 static const long int MAX_SIZE = 2048;
4076 static t_announcement a = {
4077 .text = NULL,
4078 .timestamp = 0.0,
4079 };
4080 static xs_str *fn = NULL;
4081 if (fn == NULL)
4082 fn = xs_fmt("%s/announcement.txt", srv_basedir);
4083
4084 const double ts = mtime(fn);
4085
4086 /* file does not exist or other than what was requested */
4087 if (ts == 0.0 || ts <= after)
4088 return NULL;
4089
4090 /* nothing changed, just return the current announcement */
4091 if (a.text != NULL && ts <= a.timestamp)
4092 return &a;
4093
4094 /* read and store new announcement */
4095 FILE *f;
4096
4097 if ((f = fopen(fn, "r")) != NULL) {
4098 fseek (f, 0, SEEK_END);
4099 const long int length = ftell(f);
4100
4101 if (length > MAX_SIZE) {
4102 /* this is probably unintentional */
4103 srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
4104 }
4105 else
4106 if (length > 0) {
4107 fseek (f, 0, SEEK_SET);
4108 char *buffer = malloc(length + 1);
4109 if (buffer) {
4110 fread(buffer, 1, length, f);
4111 buffer[length] = '\0';
4112
4113 free(a.text);
4114 a.text = buffer;
4115 a.timestamp = ts;
4116 }
4117 else {
4118 srv_log("Error allocating memory for announcement");
4119 }
4120 }
4121 else {
4122 /* an empty file means no announcement */
4123 free(a.text);
4124 a.text = NULL;
4125 a.timestamp = 0.0;
4126 }
4127
4128 fclose (f);
4129 }
4130
4131 if (a.text != NULL)
4132 return &a;
4133
4134 return NULL;
4135}
4136
4137
4138xs_str *make_url(const char *href, const char *proxy, int by_token)
4139/* makes an URL, possibly including proxying */
4140{
4141 xs_str *url = NULL;
4142
4143 if (proxy && !xs_startswith(href, srv_baseurl)) {
4144 xs *p = NULL;
4145
4146 if (by_token) {
4147 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
4148 xs *tk = xs_md5_hex(tks, strlen(tks));
4149
4150 p = xs_fmt("%s/y/%s/", proxy, tk);
4151 }
4152 else
4153 p = xs_fmt("%s/x/", proxy);
4154
4155 url = xs_replace(href, "https:/" "/", p);
4156 }
4157 else
4158 url = xs_dup(href);
4159
4160 return url;
4161}
4162
4163
4164/** bad login throttle **/
4165
4166xs_str *_badlogin_fn(const char *addr)
4167{
4168 xs *md5 = xs_md5_hex(addr, strlen(addr));
4169 xs *dir = xs_fmt("%s/badlogin", srv_basedir);
4170
4171 mkdirx(dir);
4172
4173 return xs_fmt("%s/%s", dir, md5);
4174}
4175
4176
4177int _badlogin_read(const char *fn, int *failures)
4178/* reads a badlogin file */
4179{
4180 int ok = 0;
4181 FILE *f;
4182
4183 pthread_mutex_lock(&data_mutex);
4184
4185 if ((f = fopen(fn, "r")) != NULL) {
4186 xs *l = xs_readline(f);
4187 fclose(f);
4188
4189 if (sscanf(l, "%d", failures) == 1)
4190 ok = 1;
4191 }
4192
4193 pthread_mutex_unlock(&data_mutex);
4194
4195 return ok;
4196}
4197
4198
4199int badlogin_check(const char *user, const char *addr)
4200/* checks if this address is authorized to try a login */
4201{
4202 int valid = 1;
4203
4204 if (xs_type(addr) == XSTYPE_STRING) {
4205 xs *fn = _badlogin_fn(addr);
4206 double mt = mtime(fn);
4207
4208 if (mt > 0) {
4209 int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
4210 "badlogin_expire", "300"));
4211
4212 mt += badlogin_expire;
4213
4214 /* if file is expired, delete and give pass */
4215 if (mt < time(NULL)) {
4216 srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
4217 unlink(fn);
4218 }
4219 else {
4220 int failures;
4221
4222 if (_badlogin_read(fn, &failures)) {
4223 int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
4224 "badlogin_retries", "5"));
4225
4226 if (failures >= badlogin_max) {
4227 valid = 0;
4228
4229 xs *d = xs_str_iso_date((time_t) mt);
4230
4231 srv_debug(1,
4232 xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
4233 }
4234 }
4235 }
4236 }
4237 }
4238
4239 return valid;
4240}
4241
4242
4243void badlogin_inc(const char *user, const char *addr)
4244/* increments a bad login from this address */
4245{
4246 if (xs_type(addr) == XSTYPE_STRING) {
4247 int failures = 0;
4248 xs *fn = _badlogin_fn(addr);
4249 FILE *f;
4250
4251 _badlogin_read(fn, &failures);
4252
4253 pthread_mutex_lock(&data_mutex);
4254
4255 if ((f = fopen(fn, "w")) != NULL) {
4256 failures++;
4257
4258 fprintf(f, "%d %s %s\n", failures, addr, user);
4259 fclose(f);
4260
4261 srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
4262 }
4263
4264 pthread_mutex_unlock(&data_mutex);
4265 }
4266}
4267
4268
4269/** language strings **/
4270
4271const char *lang_str(const char *str, const snac *user)
4272/* returns a translated string */
4273{
4274 const char *n_str = str;
4275
4276 if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4277 n_str = xs_dict_get(user->lang, str);
4278
4279 if (xs_is_null(n_str) || *n_str == '\0')
4280 n_str = str;
4281 }
4282
4283 return n_str;
4284}