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