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