snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

data.c (109290B)


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