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