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