2.66
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 xs *c = xs_str_new(NULL);
2746 const char *content = xs_dict_get(post, "content");
2747 const char *name = xs_dict_get(post, "name");
2748
2749 if (!xs_is_null(content))
2750 c = xs_str_cat(c, content);
2751 if (!xs_is_null(name))
2752 c = xs_str_cat(c, " ", name);
2753
2754 /* add alt-texts from attachments */
2755 const xs_list *atts = xs_dict_get(post, "attachment");
2756 int tc = 0;
2757 const xs_dict *att;
2758
2759 while (xs_list_next(atts, &att, &tc)) {
2760 const char *name = xs_dict_get(att, "name");
2761
2762 if (name != NULL)
2763 c = xs_str_cat(c, " ", name);
2764 }
2765
2766 /* strip HTML */
2767 c = xs_regex_replace_i(c, "<[^>]+>", " ");
2768 c = xs_regex_replace_i(c, " {2,}", " ");
2769
2770 /* convert to lowercase */
2771 xs *lc = xs_utf8_to_lower(c);
2772
2773 /* apply regex */
2774 if (xs_regex_match(lc, i_regex)) {
2775 if (xs_set_add(&seen, md5) == 1)
2776 show--;
2777 }
2778 }
2779
2780 xs_list *r = xs_set_result(&seen);
2781
2782 if (skip) {
2783 /* BAD */
2784 while (skip--) {
2785 r = xs_list_del(r, 0);
2786 }
2787 }
2788
2789 xs_free(tls[0]);
2790 xs_free(tls[1]);
2791 xs_free(tls[2]);
2792
2793 return r;
2794}
2795
2796
2797/** notifications **/
2798
2799xs_str *notify_check_time(snac *snac, int reset)
2800/* gets or resets the latest notification check time */
2801{
2802 xs_str *t = NULL;
2803 xs *fn = xs_fmt("%s/notifydate.txt", snac->basedir);
2804 FILE *f;
2805
2806 if (reset) {
2807 if ((f = fopen(fn, "w")) != NULL) {
2808 t = tid(0);
2809 fprintf(f, "%s\n", t);
2810 fclose(f);
2811 }
2812 }
2813 else {
2814 if ((f = fopen(fn, "r")) != NULL) {
2815 t = xs_readline(f);
2816 fclose(f);
2817 }
2818 else
2819 /* never set before */
2820 t = xs_fmt("%16.6f", 0.0);
2821 }
2822
2823 return t;
2824}
2825
2826
2827void notify_add(snac *snac, const char *type, const char *utype,
2828 const char *actor, const char *objid, const xs_dict *msg)
2829/* adds a new notification */
2830{
2831 xs *ntid = tid(0);
2832 xs *fn = xs_fmt("%s/notify/", snac->basedir);
2833 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
2834 FILE *f;
2835
2836 /* create the directory */
2837 mkdirx(fn);
2838 fn = xs_str_cat(fn, ntid);
2839 fn = xs_str_cat(fn, ".json");
2840
2841 xs *noti = xs_dict_new();
2842
2843 noti = xs_dict_append(noti, "id", ntid);
2844 noti = xs_dict_append(noti, "type", type);
2845 noti = xs_dict_append(noti, "utype", utype);
2846 noti = xs_dict_append(noti, "actor", actor);
2847 noti = xs_dict_append(noti, "date", date);
2848 noti = xs_dict_append(noti, "msg", msg);
2849
2850 if (!xs_is_null(objid))
2851 noti = xs_dict_append(noti, "objid", objid);
2852
2853 if ((f = fopen(fn, "w")) != NULL) {
2854 xs_json_dump(noti, 4, f);
2855 fclose(f);
2856 }
2857
2858 /* add it to the index if it already exists */
2859 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2860
2861 if (mtime(idx) != 0.0) {
2862 pthread_mutex_lock(&data_mutex);
2863
2864 if ((f = fopen(idx, "a")) != NULL) {
2865 fprintf(f, "%-32s\n", ntid);
2866 fclose(f);
2867 }
2868
2869 pthread_mutex_unlock(&data_mutex);
2870 }
2871}
2872
2873
2874xs_dict *notify_get(snac *snac, const char *id)
2875/* gets a notification */
2876{
2877 /* base file */
2878 xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
2879
2880 /* strip spaces and add extension */
2881 fn = xs_strip_i(fn);
2882 fn = xs_str_cat(fn, ".json");
2883
2884 FILE *f;
2885 xs_dict *out = NULL;
2886
2887 if ((f = fopen(fn, "r")) != NULL) {
2888 out = xs_json_load(f);
2889 fclose(f);
2890 }
2891
2892 return out;
2893}
2894
2895
2896xs_list *notify_list(snac *snac, int skip, int show)
2897/* returns a list of notification ids */
2898{
2899 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2900
2901 if (mtime(idx) == 0.0) {
2902 /* create the index from scratch */
2903 FILE *f;
2904
2905 pthread_mutex_lock(&data_mutex);
2906
2907 if ((f = fopen(idx, "w")) != NULL) {
2908 xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
2909 xs *lst = xs_glob(spec, 1, 0);
2910 xs_list *p = lst;
2911 const char *v;
2912
2913 while (xs_list_iter(&p, &v)) {
2914 char *p = strrchr(v, '.');
2915 if (p) {
2916 *p = '\0';
2917 fprintf(f, "%-32s\n", v);
2918 }
2919 }
2920
2921 fclose(f);
2922 }
2923
2924 pthread_mutex_unlock(&data_mutex);
2925 }
2926
2927 return index_list_desc(idx, skip, show);
2928}
2929
2930
2931int notify_new_num(snac *snac)
2932/* counts the number of new notifications */
2933{
2934 xs *t = notify_check_time(snac, 0);
2935 xs *lst = notify_list(snac, 0, XS_ALL);
2936 int cnt = 0;
2937
2938 xs_list *p = lst;
2939 const xs_str *v;
2940
2941 while (xs_list_iter(&p, &v)) {
2942 xs *id = xs_strip_i(xs_dup(v));
2943
2944 /* old? count no more */
2945 if (strcmp(id, t) < 0)
2946 break;
2947
2948 cnt++;
2949 }
2950
2951 return cnt;
2952}
2953
2954
2955void notify_clear(snac *snac)
2956/* clears all notifications */
2957{
2958 xs *spec = xs_fmt("%s/notify/" "*", snac->basedir);
2959 xs *lst = xs_glob(spec, 0, 0);
2960 xs_list *p = lst;
2961 const xs_str *v;
2962
2963 while (xs_list_iter(&p, &v))
2964 unlink(v);
2965
2966 xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2967
2968 if (mtime(idx) != 0.0) {
2969 pthread_mutex_lock(&data_mutex);
2970 truncate(idx, 0);
2971 pthread_mutex_unlock(&data_mutex);
2972 }
2973}
2974
2975
2976/** the queue **/
2977
2978static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
2979/* writes safely to the queue */
2980{
2981 xs *tfn = xs_fmt("%s.tmp", fn);
2982 FILE *f;
2983
2984 if ((f = fopen(tfn, "w")) != NULL) {
2985 xs_json_dump(msg, 4, f);
2986 fclose(f);
2987
2988 rename(tfn, fn);
2989 }
2990
2991 return msg;
2992}
2993
2994
2995static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
2996/* creates a queue message */
2997{
2998 int qrt = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
2999 xs *ntid = tid(retries * 60 * qrt);
3000 xs *rn = xs_number_new(retries);
3001
3002 xs_dict *qmsg = xs_dict_new();
3003
3004 qmsg = xs_dict_append(qmsg, "type", type);
3005 qmsg = xs_dict_append(qmsg, "message", msg);
3006 qmsg = xs_dict_append(qmsg, "retries", rn);
3007 qmsg = xs_dict_append(qmsg, "ntid", ntid);
3008
3009 return qmsg;
3010}
3011
3012
3013void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3014/* enqueues an input message */
3015{
3016 xs *qmsg = _new_qmsg("input", msg, retries);
3017 const char *ntid = xs_dict_get(qmsg, "ntid");
3018 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3019
3020 qmsg = xs_dict_append(qmsg, "req", req);
3021
3022 qmsg = _enqueue_put(fn, qmsg);
3023
3024 snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3025}
3026
3027
3028void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3029/* enqueues an input message from the shared input */
3030{
3031 xs *qmsg = _new_qmsg("input", msg, retries);
3032 const char *ntid = xs_dict_get(qmsg, "ntid");
3033 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3034
3035 qmsg = xs_dict_append(qmsg, "req", req);
3036
3037 qmsg = _enqueue_put(fn, qmsg);
3038
3039 srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3040}
3041
3042
3043void enqueue_output_raw(const char *keyid, const char *seckey,
3044 const xs_dict *msg, const xs_str *inbox,
3045 int retries, int p_status)
3046/* enqueues an output message to an inbox */
3047{
3048 xs *qmsg = _new_qmsg("output", msg, retries);
3049 const char *ntid = xs_dict_get(qmsg, "ntid");
3050 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3051
3052 xs *ns = xs_number_new(p_status);
3053 qmsg = xs_dict_append(qmsg, "p_status", ns);
3054
3055 qmsg = xs_dict_append(qmsg, "inbox", inbox);
3056 qmsg = xs_dict_append(qmsg, "keyid", keyid);
3057 qmsg = xs_dict_append(qmsg, "seckey", seckey);
3058
3059 /* if it's to be sent right now, bypass the disk queue and post the job */
3060 if (retries == 0 && p_state != NULL)
3061 job_post(qmsg, 0);
3062 else {
3063 qmsg = _enqueue_put(fn, qmsg);
3064 srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3065 }
3066}
3067
3068
3069void enqueue_output(snac *snac, const xs_dict *msg,
3070 const xs_str *inbox, int retries, int p_status)
3071/* enqueues an output message to an inbox */
3072{
3073 if (xs_startswith(inbox, snac->actor)) {
3074 snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3075 return;
3076 }
3077
3078 const char *seckey = xs_dict_get(snac->key, "secret");
3079
3080 enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3081}
3082
3083
3084void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3085 const xs_str *actor, int retries)
3086/* enqueues an output message for an actor */
3087{
3088 xs *inbox = get_actor_inbox(actor, 1);
3089
3090 if (!xs_is_null(inbox))
3091 enqueue_output(snac, msg, inbox, retries, 0);
3092 else
3093 snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3094}
3095
3096
3097void enqueue_email(const xs_str *msg, int retries)
3098/* enqueues an email message to be sent */
3099{
3100 xs *qmsg = _new_qmsg("email", msg, retries);
3101 const char *ntid = xs_dict_get(qmsg, "ntid");
3102 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3103
3104 qmsg = _enqueue_put(fn, qmsg);
3105
3106 srv_debug(1, xs_fmt("enqueue_email %d", retries));
3107}
3108
3109
3110void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3111/* enqueues a message to be sent via Telegram */
3112{
3113 xs *qmsg = _new_qmsg("telegram", msg, 0);
3114 const char *ntid = xs_dict_get(qmsg, "ntid");
3115 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3116
3117 qmsg = xs_dict_append(qmsg, "bot", bot);
3118 qmsg = xs_dict_append(qmsg, "chat_id", chat_id);
3119
3120 qmsg = _enqueue_put(fn, qmsg);
3121
3122 srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3123}
3124
3125void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3126/* enqueues a message to be sent via ntfy */
3127{
3128 xs *qmsg = _new_qmsg("ntfy", msg, 0);
3129 const char *ntid = xs_dict_get(qmsg, "ntid");
3130 xs *fn = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3131
3132 qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3133 qmsg = xs_dict_append(qmsg, "ntfy_token", ntfy_token);
3134
3135
3136 qmsg = _enqueue_put(fn, qmsg);
3137
3138 srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3139}
3140
3141void enqueue_message(snac *snac, const xs_dict *msg)
3142/* enqueues an output message */
3143{
3144 xs *qmsg = _new_qmsg("message", msg, 0);
3145 const char *ntid = xs_dict_get(qmsg, "ntid");
3146 xs *fn = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3147
3148 qmsg = _enqueue_put(fn, qmsg);
3149
3150 snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3151}
3152
3153
3154void enqueue_close_question(snac *user, const char *id, int end_secs)
3155/* enqueues the closing of a question */
3156{
3157 xs *qmsg = _new_qmsg("close_question", id, 0);
3158 xs *ntid = tid(end_secs);
3159 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3160
3161 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3162
3163 qmsg = _enqueue_put(fn, qmsg);
3164
3165 snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3166}
3167
3168
3169void enqueue_object_request(snac *user, const char *id, int forward_secs)
3170/* enqueues the request of an object in the future */
3171{
3172 xs *qmsg = _new_qmsg("object_request", id, 0);
3173 xs *ntid = tid(forward_secs);
3174 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3175
3176 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3177
3178 qmsg = _enqueue_put(fn, qmsg);
3179
3180 snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3181}
3182
3183
3184void enqueue_verify_links(snac *user)
3185/* enqueues a link verification */
3186{
3187 xs *qmsg = _new_qmsg("verify_links", "", 0);
3188 const char *ntid = xs_dict_get(qmsg, "ntid");
3189 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3190
3191 qmsg = _enqueue_put(fn, qmsg);
3192
3193 snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3194}
3195
3196
3197void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3198/* enqueues an actor refresh */
3199{
3200 xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3201 xs *ntid = tid(forward_secs);
3202 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3203
3204 qmsg = xs_dict_set(qmsg, "ntid", ntid);
3205 qmsg = xs_dict_append(qmsg, "actor", actor);
3206
3207 qmsg = _enqueue_put(fn, qmsg);
3208
3209 snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3210}
3211
3212
3213int was_question_voted(snac *user, const char *id)
3214/* returns true if the user voted in this poll */
3215{
3216 xs *children = object_children(id);
3217 int voted = 0;
3218 xs_list *p;
3219 const xs_str *md5;
3220
3221 p = children;
3222 while (xs_list_iter(&p, &md5)) {
3223 xs *obj = NULL;
3224
3225 if (valid_status(object_get_by_md5(md5, &obj))) {
3226 const char *atto = get_atto(obj);
3227 if (atto && strcmp(atto, user->actor) == 0 &&
3228 !xs_is_null(xs_dict_get(obj, "name"))) {
3229 voted = 1;
3230 break;
3231 }
3232 }
3233 }
3234
3235 return voted;
3236}
3237
3238
3239xs_list *user_queue(snac *snac)
3240/* returns a list with filenames that can be dequeued */
3241{
3242 xs *spec = xs_fmt("%s/queue/" "*.json", snac->basedir);
3243 xs_list *list = xs_list_new();
3244 time_t t = time(NULL);
3245 xs_list *p;
3246 const xs_val *v;
3247
3248 xs *fns = xs_glob(spec, 0, 0);
3249
3250 p = fns;
3251 while (xs_list_iter(&p, &v)) {
3252 /* get the retry time from the basename */
3253 char *bn = strrchr(v, '/');
3254 time_t t2 = atol(bn + 1);
3255
3256 if (t2 > t)
3257 snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3258 else {
3259 list = xs_list_append(list, v);
3260 snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3261 }
3262 }
3263
3264 return list;
3265}
3266
3267
3268xs_list *queue(void)
3269/* returns a list with filenames that can be dequeued */
3270{
3271 xs *spec = xs_fmt("%s/queue/" "*.json", srv_basedir);
3272 xs_list *list = xs_list_new();
3273 time_t t = time(NULL);
3274 xs_list *p;
3275 const xs_val *v;
3276
3277 xs *fns = xs_glob(spec, 0, 0);
3278
3279 p = fns;
3280 while (xs_list_iter(&p, &v)) {
3281 /* get the retry time from the basename */
3282 char *bn = strrchr(v, '/');
3283 time_t t2 = atol(bn + 1);
3284
3285 if (t2 > t)
3286 srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3287 else {
3288 list = xs_list_append(list, v);
3289 srv_debug(2, xs_fmt("queue ready for %s", v));
3290 }
3291 }
3292
3293 return list;
3294}
3295
3296
3297xs_dict *queue_get(const char *fn)
3298/* gets a file from a queue */
3299{
3300 FILE *f;
3301 xs_dict *obj = NULL;
3302
3303 if ((f = fopen(fn, "r")) != NULL) {
3304 obj = xs_json_load(f);
3305 fclose(f);
3306 }
3307
3308 return obj;
3309}
3310
3311
3312xs_dict *dequeue(const char *fn)
3313/* dequeues a message */
3314{
3315 xs_dict *obj = queue_get(fn);
3316
3317 unlink(fn);
3318
3319 return obj;
3320}
3321
3322
3323/** the purge **/
3324
3325static int _purge_file(const char *fn, time_t mt)
3326/* purge fn if it's older than days */
3327{
3328 int ret = 0;
3329
3330 if (mtime(fn) < mt) {
3331 /* older than the minimum time: delete it */
3332 unlink(fn);
3333 srv_debug(2, xs_fmt("purged %s", fn));
3334 ret = 1;
3335 }
3336
3337 return ret;
3338}
3339
3340
3341static void _purge_dir(const char *dir, int days)
3342/* purges all files in a directory older than days */
3343{
3344 int cnt = 0;
3345
3346 if (days) {
3347 time_t mt = time(NULL) - days * 24 * 3600;
3348 xs *spec = xs_fmt("%s/" "*", dir);
3349 xs *list = xs_glob(spec, 0, 0);
3350 xs_list *p;
3351 const xs_str *v;
3352
3353 p = list;
3354 while (xs_list_iter(&p, &v))
3355 cnt += _purge_file(v, mt);
3356
3357 srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3358 }
3359}
3360
3361
3362static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3363/* purges all files in a user subdir older than days */
3364{
3365 xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3366
3367 _purge_dir(u_subdir, days);
3368}
3369
3370
3371void purge_server(void)
3372/* purge global server data */
3373{
3374 xs *spec = xs_fmt("%s/object/??", srv_basedir);
3375 xs *dirs = xs_glob(spec, 0, 0);
3376 xs_list *p;
3377 const xs_str *v;
3378 int cnt = 0;
3379 int icnt = 0;
3380
3381 time_t mt = time(NULL) - 7 * 24 * 3600;
3382
3383 p = dirs;
3384 while (xs_list_iter(&p, &v)) {
3385 xs_list *p2;
3386 const xs_str *v2;
3387
3388 {
3389 xs *spec2 = xs_fmt("%s/" "*.json", v);
3390 xs *files = xs_glob(spec2, 0, 0);
3391
3392 p2 = files;
3393 while (xs_list_iter(&p2, &v2)) {
3394 int n_link;
3395
3396 /* old and with no hard links? */
3397 if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3398 xs *s1 = xs_replace(v2, ".json", "");
3399 xs *l = xs_split(s1, "/");
3400 const char *md5 = xs_list_get(l, -1);
3401
3402 object_del_by_md5(md5);
3403 cnt++;
3404 }
3405 }
3406 }
3407
3408 {
3409 /* look for stray indexes */
3410 xs *speci = xs_fmt("%s/" "*_?.idx", v);
3411 xs *idxfs = xs_glob(speci, 0, 0);
3412
3413 p2 = idxfs;
3414 while (xs_list_iter(&p2, &v2)) {
3415 /* old enough to consider? */
3416 if (mtime(v2) < mt) {
3417 /* check if the indexed object is here */
3418 xs *o = xs_dup(v2);
3419 char *ext = strchr(o, '_');
3420
3421 if (ext) {
3422 *ext = '\0';
3423 o = xs_str_cat(o, ".json");
3424
3425 if (mtime(o) == 0.0) {
3426 /* delete */
3427 unlink(v2);
3428 srv_debug(1, xs_fmt("purged %s", v2));
3429 icnt++;
3430 }
3431 }
3432 }
3433 }
3434
3435 /* delete index backups */
3436 xs *specb = xs_fmt("%s/" "*.bak", v);
3437 xs *bakfs = xs_glob(specb, 0, 0);
3438
3439 p2 = bakfs;
3440 while (xs_list_iter(&p2, &v2)) {
3441 unlink(v2);
3442 srv_debug(1, xs_fmt("purged %s", v2));
3443 }
3444 }
3445 }
3446
3447 /* purge collected inboxes */
3448 xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3449 _purge_dir(ib_dir, 7);
3450
3451 /* purge the instance timeline */
3452 xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3453 int itl_gc = index_gc(itl_fn);
3454
3455 /* purge tag indexes */
3456 xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3457 xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3458 p = tag_dirs;
3459
3460 int tag_gc = 0;
3461 while (xs_list_iter(&p, &v)) {
3462 xs *spec2 = xs_fmt("%s/" "*.idx", v);
3463 xs *files = xs_glob(spec2, 0, 0);
3464 xs_list *p2;
3465 const xs_str *v2;
3466
3467 p2 = files;
3468 while (xs_list_iter(&p2, &v2)) {
3469 tag_gc += index_gc(v2);
3470 xs *bak = xs_fmt("%s.bak", v2);
3471 unlink(bak);
3472
3473 if (index_len(v2) == 0) {
3474 /* there are no longer any entry with this tag;
3475 purge it completely */
3476 unlink(v2);
3477 xs *dottag = xs_replace(v2, ".idx", ".tag");
3478 unlink(dottag);
3479 }
3480 }
3481 }
3482
3483 srv_debug(1, xs_fmt("purge: global "
3484 "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3485}
3486
3487
3488void purge_user(snac *snac)
3489/* do the purge for this user */
3490{
3491 int priv_days, pub_days, user_days = 0;
3492 const char *v;
3493 int n;
3494
3495 priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3496 pub_days = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3497
3498 if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3499 (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3500 user_days = xs_number_get(v);
3501
3502 if (user_days) {
3503 /* override admin settings only if they are lesser */
3504 if (priv_days == 0 || user_days < priv_days)
3505 priv_days = user_days;
3506
3507 if (pub_days == 0 || user_days < pub_days)
3508 pub_days = user_days;
3509 }
3510
3511 _purge_user_subdir(snac, "hidden", priv_days);
3512 _purge_user_subdir(snac, "private", priv_days);
3513
3514 _purge_user_subdir(snac, "public", pub_days);
3515
3516 const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
3517 "pinned.idx", "bookmark.idx", "draft.idx", NULL };
3518
3519 for (n = 0; idxs[n]; n++) {
3520 xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3521 int gc = index_gc(idx);
3522 srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3523 }
3524
3525 /* purge lists */
3526 {
3527 xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3528 xs *lol = xs_glob(spec, 0, 0);
3529 int c = 0;
3530 const char *v;
3531
3532 while (xs_list_next(lol, &v, &c)) {
3533 int gc = index_gc(v);
3534 srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3535 }
3536 }
3537
3538 /* unrelated to purging, but it's a janitorial process, so what the hell */
3539 verify_links(snac);
3540}
3541
3542
3543void purge_all(void)
3544/* purge all users */
3545{
3546 snac snac;
3547 xs *list = user_list();
3548 char *p;
3549 const char *uid;
3550
3551 p = list;
3552 while (xs_list_iter(&p, &uid)) {
3553 if (user_open(&snac, uid)) {
3554 purge_user(&snac);
3555 user_free(&snac);
3556 }
3557 }
3558
3559 purge_server();
3560
3561#ifndef NO_MASTODON_API
3562 mastoapi_purge();
3563#endif
3564}
3565
3566
3567/** archive **/
3568
3569void srv_archive(const char *direction, const char *url, xs_dict *req,
3570 const char *payload, int p_size,
3571 int status, xs_dict *headers,
3572 const char *body, int b_size)
3573/* archives a connection */
3574{
3575 /* obsessive archiving */
3576 xs *date = tid(0);
3577 xs *dir = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3578 FILE *f;
3579
3580 if (mkdirx(dir) != -1) {
3581 xs *meta_fn = xs_fmt("%s/_META", dir);
3582
3583 if ((f = fopen(meta_fn, "w")) != NULL) {
3584 xs *j1 = xs_json_dumps(req, 4);
3585 xs *j2 = xs_json_dumps(headers, 4);
3586
3587 fprintf(f, "dir: %s\n", direction);
3588
3589 if (url)
3590 fprintf(f, "url: %s\n", url);
3591
3592 fprintf(f, "req: %s\n", j1);
3593 fprintf(f, "p_size: %d\n", p_size);
3594 fprintf(f, "status: %d\n", status);
3595 fprintf(f, "response: %s\n", j2);
3596 fprintf(f, "b_size: %d\n", b_size);
3597 fclose(f);
3598 }
3599
3600 if (p_size && payload) {
3601 xs *payload_fn = NULL;
3602 xs *payload_fn_raw = NULL;
3603 const char *v = xs_dict_get(req, "content-type");
3604
3605 if (v && xs_str_in(v, "json") != -1) {
3606 payload_fn = xs_fmt("%s/payload.json", dir);
3607
3608 if ((f = fopen(payload_fn, "w")) != NULL) {
3609 xs *v1 = xs_json_loads(payload);
3610 xs *j1 = NULL;
3611
3612 if (v1 != NULL)
3613 j1 = xs_json_dumps(v1, 4);
3614
3615 if (j1 != NULL)
3616 fwrite(j1, strlen(j1), 1, f);
3617 else
3618 fwrite(payload, p_size, 1, f);
3619
3620 fclose(f);
3621 }
3622 }
3623
3624 payload_fn_raw = xs_fmt("%s/payload", dir);
3625
3626 if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3627 fwrite(payload, p_size, 1, f);
3628 fclose(f);
3629 }
3630 }
3631
3632 if (b_size && body) {
3633 xs *body_fn = NULL;
3634 const char *v = xs_dict_get(headers, "content-type");
3635
3636 if (v && xs_str_in(v, "json") != -1) {
3637 body_fn = xs_fmt("%s/body.json", dir);
3638
3639 if ((f = fopen(body_fn, "w")) != NULL) {
3640 xs *v1 = xs_json_loads(body);
3641 xs *j1 = NULL;
3642
3643 if (v1 != NULL)
3644 j1 = xs_json_dumps(v1, 4);
3645
3646 if (j1 != NULL)
3647 fwrite(j1, strlen(j1), 1, f);
3648 else
3649 fwrite(body, b_size, 1, f);
3650
3651 fclose(f);
3652 }
3653 }
3654 else {
3655 body_fn = xs_fmt("%s/body", dir);
3656
3657 if ((f = fopen(body_fn, "w")) != NULL) {
3658 fwrite(body, b_size, 1, f);
3659 fclose(f);
3660 }
3661 }
3662 }
3663 }
3664}
3665
3666
3667void srv_archive_error(const char *prefix, const xs_str *err,
3668 const xs_dict *req, const xs_val *data)
3669/* archives an error */
3670{
3671 xs *ntid = tid(0);
3672 xs *fn = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
3673 FILE *f;
3674
3675 if ((f = fopen(fn, "w")) != NULL) {
3676 fprintf(f, "Error: %s\n", err);
3677
3678 if (req) {
3679 fprintf(f, "Request headers:\n");
3680
3681 xs_json_dump(req, 4, f);
3682
3683 fprintf(f, "\n");
3684 }
3685
3686 if (data) {
3687 fprintf(f, "Data:\n");
3688
3689 if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
3690 xs_json_dump(data, 4, f);
3691 }
3692 else
3693 fprintf(f, "%s", data);
3694
3695 fprintf(f, "\n");
3696 }
3697
3698 fclose(f);
3699 }
3700}
3701
3702
3703void srv_archive_qitem(const char *prefix, xs_dict *q_item)
3704/* archives a q_item in the error folder */
3705{
3706 xs *ntid = tid(0);
3707 xs *fn = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
3708 FILE *f;
3709
3710 if ((f = fopen(fn, "w")) != NULL) {
3711 xs_json_dump(q_item, 4, f);
3712 fclose(f);
3713 }
3714}
3715
3716
3717t_announcement *announcement(const double after)
3718/* returns announcement text or NULL if none exists or it is olde than "after" */
3719{
3720 static const long int MAX_SIZE = 2048;
3721 static t_announcement a = {
3722 .text = NULL,
3723 .timestamp = 0.0,
3724 };
3725 static xs_str *fn = NULL;
3726 if (fn == NULL)
3727 fn = xs_fmt("%s/announcement.txt", srv_basedir);
3728
3729 const double ts = mtime(fn);
3730
3731 /* file does not exist or other than what was requested */
3732 if (ts == 0.0 || ts <= after)
3733 return NULL;
3734
3735 /* nothing changed, just return the current announcement */
3736 if (a.text != NULL && ts <= a.timestamp)
3737 return &a;
3738
3739 /* read and store new announcement */
3740 FILE *f;
3741
3742 if ((f = fopen(fn, "r")) != NULL) {
3743 fseek (f, 0, SEEK_END);
3744 const long int length = ftell(f);
3745
3746 if (length > MAX_SIZE) {
3747 /* this is probably unintentional */
3748 srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
3749 }
3750 else
3751 if (length > 0) {
3752 fseek (f, 0, SEEK_SET);
3753 char *buffer = malloc(length + 1);
3754 if (buffer) {
3755 fread(buffer, 1, length, f);
3756 buffer[length] = '\0';
3757
3758 free(a.text);
3759 a.text = buffer;
3760 a.timestamp = ts;
3761 }
3762 else {
3763 srv_log("Error allocating memory for announcement");
3764 }
3765 }
3766 else {
3767 /* an empty file means no announcement */
3768 free(a.text);
3769 a.text = NULL;
3770 a.timestamp = 0.0;
3771 }
3772
3773 fclose (f);
3774 }
3775
3776 if (a.text != NULL)
3777 return &a;
3778
3779 return NULL;
3780}
3781
3782
3783xs_str *make_url(const char *href, const char *proxy, int by_token)
3784/* makes an URL, possibly including proxying */
3785{
3786 xs_str *url = NULL;
3787
3788 if (proxy && !xs_startswith(href, srv_baseurl)) {
3789 xs *p = NULL;
3790
3791 if (by_token) {
3792 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
3793 xs *tk = xs_md5_hex(tks, strlen(tks));
3794
3795 p = xs_fmt("%s/y/%s/", proxy, tk);
3796 }
3797 else
3798 p = xs_fmt("%s/x/", proxy);
3799
3800 url = xs_replace(href, "https:/" "/", p);
3801 }
3802 else
3803 url = xs_dup(href);
3804
3805 return url;
3806}