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