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