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