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