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