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