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