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