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