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