2.59 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/** drafts **/
1662
1663int is_draft(snac *user, const char *id)
1664/* returns true if this note is a draft */
1665{
1666    return object_user_cache_in(user, id, "draft");
1667}
1668
1669
1670void draft_del(snac *user, const char *id)
1671/* delete a message from the draft cache */
1672{
1673    object_user_cache_del(user, id, "draft");
1674}
1675
1676
1677void draft_add(snac *user, const char *id, const xs_dict *msg)
1678/* store the message as a draft */
1679{
1680    /* delete from the index, in case it was already there */
1681    draft_del(user, id);
1682
1683    /* overwrite object */
1684    object_add_ow(id, msg);
1685
1686    /* [re]add to the index */
1687    object_user_cache_add(user, id, "draft");
1688}
1689
1690
1691xs_list *draft_list(snac *user)
1692/* return the lists of drafts */
1693{
1694    return object_user_cache_list(user, "draft", XS_ALL, 1);
1695}
1696
1697
1698/** hiding **/
1699
1700xs_str *_hidden_fn(snac *snac, const char *id)
1701{
1702    xs *md5 = xs_md5_hex(id, strlen(id));
1703    return xs_fmt("%s/hidden/%s", snac->basedir, md5);
1704}
1705
1706
1707void hide(snac *snac, const char *id)
1708/* hides a message tree */
1709{
1710    xs *fn = _hidden_fn(snac, id);
1711    FILE *f;
1712
1713    if ((f = fopen(fn, "w")) != NULL) {
1714        fprintf(f, "%s\n", id);
1715        fclose(f);
1716
1717        snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
1718
1719        /* hide all the children */
1720        xs *chld = object_children(id);
1721        char *p;
1722        const char *v;
1723
1724        p = chld;
1725        while (xs_list_iter(&p, &v)) {
1726            xs *co = NULL;
1727
1728            /* resolve to get the id */
1729            if (valid_status(object_get_by_md5(v, &co))) {
1730                const char *id = xs_dict_get(co, "id");
1731                if (id != NULL)
1732                    hide(snac, id);
1733            }
1734        }
1735    }
1736}
1737
1738
1739int is_hidden(snac *snac, const char *id)
1740/* check is id is hidden */
1741{
1742    xs *fn = _hidden_fn(snac, id);
1743
1744    return !!(mtime(fn) != 0.0);
1745}
1746
1747
1748int actor_add(const char *actor, const xs_dict *msg)
1749/* adds an actor */
1750{
1751    return object_add_ow(actor, msg);
1752}
1753
1754
1755int actor_get(const char *actor, xs_dict **data)
1756/* returns an already downloaded actor */
1757{
1758    int status = HTTP_STATUS_OK;
1759    xs_dict *d = NULL;
1760
1761    if (xs_startswith(actor, srv_baseurl)) {
1762        /* it's a (possible) local user */
1763        xs *l = xs_split(actor, "/");
1764        const char *uid = xs_list_get(l, -1);
1765        snac user;
1766
1767        if (!xs_is_null(uid) && user_open(&user, uid)) {
1768            if (data)
1769                *data = msg_actor(&user);
1770
1771            user_free(&user);
1772            return HTTP_STATUS_OK;
1773        }
1774        else
1775            return HTTP_STATUS_NOT_FOUND;
1776    }
1777
1778    /* read the object */
1779    if (!valid_status(status = object_get(actor, &d))) {
1780        d = xs_free(d);
1781        return status;
1782    }
1783
1784    /* if the object is corrupted, discard it */
1785    if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
1786        srv_debug(1, xs_fmt("corrupted actor object %s", actor));
1787        d = xs_free(d);
1788        return HTTP_STATUS_NOT_FOUND;
1789    }
1790
1791    if (data)
1792        *data = d;
1793    else
1794        d = xs_free(d);
1795
1796    xs *fn = _object_fn(actor);
1797    double max_time;
1798
1799    /* maximum time for the actor data to be considered stale */
1800    max_time = 3600.0 * 36.0;
1801
1802    if (mtime(fn) + max_time < (double) time(NULL)) {
1803        /* actor data exists but also stinks */
1804        status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
1805    }
1806
1807    return status;
1808}
1809
1810
1811int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
1812/* gets an actor and requests a refresh if it's stale */
1813{
1814    int status = actor_get(actor, data);
1815
1816    if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
1817        enqueue_actor_refresh(user, actor, 0);
1818
1819    return status;
1820}
1821
1822
1823/** user limiting (announce blocks) **/
1824
1825int limited(snac *user, const char *id, int cmd)
1826/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
1827{
1828    int ret = 0;
1829    xs *dir = xs_fmt("%s/limited", user->basedir);
1830    xs *md5 = xs_md5_hex(id, strlen(id));
1831    xs *fn  = xs_fmt("%s/%s", dir, md5);
1832
1833    switch (cmd) {
1834    case 0: /** check **/
1835        ret = !!(mtime(fn) > 0.0);
1836        break;
1837
1838    case 1: /** limit **/
1839        mkdirx(dir);
1840
1841        if (mtime(fn) > 0.0)
1842            ret = -1;
1843        else {
1844            FILE *f;
1845
1846            if ((f = fopen(fn, "w")) != NULL) {
1847                fprintf(f, "%s\n", id);
1848                fclose(f);
1849            }
1850            else
1851                ret = -2;
1852        }
1853        break;
1854
1855    case 2: /** unlimit **/
1856        if (mtime(fn) > 0.0)
1857            ret = unlink(fn);
1858        else
1859            ret = -1;
1860        break;
1861    }
1862
1863    return ret;
1864}
1865
1866
1867/** tag indexing **/
1868
1869void tag_index(const char *id, const xs_dict *obj)
1870/* update the tag indexes for this object */
1871{
1872    const xs_list *tags = xs_dict_get(obj, "tag");
1873
1874    if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
1875        xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
1876
1877        mkdirx(g_tag_dir);
1878
1879        const xs_dict *v;
1880        int ct = 0;
1881        while (xs_list_next(tags, &v, &ct)) {
1882            const char *type = xs_dict_get(v, "type");
1883            const char *name = xs_dict_get(v, "name");
1884
1885            if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
1886                while (*name == '#' || *name == '@')
1887                    name++;
1888
1889                if (*name == '\0')
1890                    continue;
1891
1892                name = xs_tolower_i((xs_str *)name);
1893
1894                xs *md5_tag   = xs_md5_hex(name, strlen(name));
1895                xs *tag_dir   = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
1896                mkdirx(tag_dir);
1897
1898                xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
1899                index_add(g_tag_idx, id);
1900
1901                FILE *f;
1902                xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
1903                if ((f = fopen(g_tag_name, "w")) != NULL) {
1904                    fprintf(f, "%s\n", name);
1905                    fclose(f);
1906                }
1907
1908                srv_debug(0, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
1909            }
1910        }
1911    }
1912}
1913
1914
1915xs_str *tag_fn(const char *tag)
1916{
1917    if (*tag == '#')
1918        tag++;
1919
1920    xs *lw_tag = xs_tolower_i(xs_dup(tag));
1921    xs *md5    = xs_md5_hex(lw_tag, strlen(lw_tag));
1922
1923    return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
1924}
1925
1926
1927xs_list *tag_search(const char *tag, int skip, int show)
1928/* returns the list of posts tagged with tag */
1929{
1930    xs *idx = tag_fn(tag);
1931
1932    return index_list_desc(idx, skip, show);
1933}
1934
1935
1936/** lists **/
1937
1938xs_val *list_maint(snac *user, const char *list, int op)
1939/* list maintenance */
1940{
1941    xs_val *l = NULL;
1942
1943    switch (op) {
1944    case 0: /** list of lists **/
1945        {
1946            FILE *f;
1947            xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
1948            xs *ls   = xs_glob(spec, 0, 0);
1949            int c = 0;
1950            const char *v;
1951
1952            l = xs_list_new();
1953
1954            while (xs_list_next(ls, &v, &c)) {
1955                if ((f = fopen(v, "r")) != NULL) {
1956                    xs *title = xs_readline(f);
1957                    fclose(f);
1958
1959                    title = xs_strip_i(title);
1960
1961                    xs *v2 = xs_replace(v, ".id", "");
1962                    xs *l2 = xs_split(v2, "/");
1963
1964                    /* return [ list_id, list_title ] */
1965                    l = xs_list_append(l, xs_list_append(xs_list_new(), xs_list_get(l2, -1), title));
1966                }
1967            }
1968        }
1969
1970        break;
1971
1972    case 1: /** create new list (list is the name) **/
1973        {
1974            xs *lol = list_maint(user, NULL, 0);
1975            int c = 0;
1976            const xs_list *v;
1977            int add = 1;
1978
1979            /* check if this list name already exists */
1980            while (xs_list_next(lol, &v, &c)) {
1981                if (strcmp(xs_list_get(v, 1), list) == 0) {
1982                    add = 0;
1983                    break;
1984                }
1985            }
1986
1987            if (add) {
1988                FILE *f;
1989                xs *dir = xs_fmt("%s/list/", user->basedir);
1990
1991                l = xs_fmt("%010x", time(NULL));
1992
1993                mkdirx(dir);
1994
1995                xs *fn = xs_fmt("%s%s.id", dir, l);
1996
1997                if ((f = fopen(fn, "w")) != NULL) {
1998                    fprintf(f, "%s\n", list);
1999                    fclose(f);
2000                }
2001            }
2002        }
2003
2004        break;
2005
2006    case 2: /** delete list (list is the id) **/
2007        {
2008            if (xs_is_hex(list)) {
2009                xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2010                unlink(fn);
2011
2012                fn = xs_replace_i(fn, ".id", ".lst");
2013                unlink(fn);
2014
2015                fn = xs_replace_i(fn, ".lst", ".idx");
2016                unlink(fn);
2017
2018                fn = xs_str_cat(fn, ".bak");
2019                unlink(fn);
2020            }
2021        }
2022
2023        break;
2024
2025    case 3: /** get list name **/
2026        if (xs_is_hex(list)) {
2027            FILE *f;
2028            xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2029
2030            if ((f = fopen(fn, "r")) != NULL) {
2031                l = xs_strip_i(xs_readline(f));
2032                fclose(f);
2033            }
2034        }
2035
2036        break;
2037    }
2038
2039    return l;
2040}
2041
2042
2043xs_str *list_timeline_fn(snac *user, const char *list)
2044{
2045    return xs_fmt("%s/list/%s.idx", user->basedir, list);
2046}
2047
2048
2049xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2050/* returns the timeline of a list */
2051{
2052    xs_list *l = NULL;
2053
2054    if (!xs_is_hex(list))
2055        return NULL;
2056
2057    xs *fn = list_timeline_fn(user, list);
2058
2059    if (mtime(fn) > 0.0)
2060        l = index_list_desc(fn, skip, show);
2061
2062    return l;
2063}
2064
2065
2066xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
2067/* list content management */
2068{
2069    xs_val *l = NULL;
2070
2071    if (!xs_is_hex(list))
2072        return NULL;
2073
2074    if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2075        return NULL;
2076
2077    xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2078
2079    switch (op) {
2080    case 0: /** list content **/
2081        l = index_list(fn, XS_ALL);
2082
2083        break;
2084
2085    case 1: /** append actor to list **/
2086        if (actor_md5 != NULL) {
2087            if (!index_in_md5(fn, actor_md5))
2088                index_add_md5(fn, actor_md5);
2089        }
2090
2091        break;
2092
2093    case 2: /** delete actor from list **/
2094        if (actor_md5 != NULL)
2095            index_del_md5(fn, actor_md5);
2096
2097        break;
2098
2099    default:
2100        srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2101        break;
2102    }
2103
2104    return l;
2105}
2106
2107
2108void list_distribute(snac *user, const char *who, const xs_dict *post)
2109/* distributes the post to all appropriate lists */
2110{
2111    const char *id = xs_dict_get(post, "id");
2112
2113    /* if who is not set, use the attributedTo in the message */
2114    if (xs_is_null(who))
2115        who = get_atto(post);
2116
2117    if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2118        xs *a_md5 = xs_md5_hex(who, strlen(who));
2119        xs *i_md5 = xs_md5_hex(id, strlen(id));
2120        xs *spec  = xs_fmt("%s/list/" "*.lst", user->basedir);
2121        xs *ls    = xs_glob(spec, 0, 0);
2122        int c = 0;
2123        const char *v;
2124
2125        while (xs_list_next(ls, &v, &c)) {
2126            /* is the actor in this list? */
2127            if (index_in_md5(v, a_md5)) {
2128                /* it is; add post md5 to its timeline */
2129                xs *idx = xs_replace(v, ".lst", ".idx");
2130                index_add_md5(idx, i_md5);
2131
2132                snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2133            }
2134        }
2135    }
2136}
2137
2138
2139/** static data **/
2140
2141static int _load_raw_file(const char *fn, xs_val **data, int *size,
2142                        const char *inm, xs_str **etag)
2143/* loads a cached file */
2144{
2145    int status = HTTP_STATUS_NOT_FOUND;
2146
2147    if (fn) {
2148        double tm = mtime(fn);
2149
2150        if (tm > 0.0) {
2151            /* file exists; build the etag */
2152            xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2153
2154            /* if if-none-match is set, check if it's the same */
2155            if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2156                /* client has the newest version */
2157                status = HTTP_STATUS_NOT_MODIFIED;
2158            }
2159            else {
2160                /* newer or never downloaded; read the full file */
2161                FILE *f;
2162
2163                if ((f = fopen(fn, "rb")) != NULL) {
2164                    *size = XS_ALL;
2165                    *data = xs_read(f, size);
2166                    fclose(f);
2167
2168                    status = HTTP_STATUS_OK;
2169                }
2170            }
2171
2172            /* if caller wants the etag, return it */
2173            if (etag != NULL)
2174                *etag = xs_dup(e);
2175
2176            srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2177        }
2178    }
2179
2180    return status;
2181}
2182
2183
2184xs_str *_static_fn(snac *snac, const char *id)
2185/* gets the filename for a static file */
2186{
2187    if (strchr(id, '/'))
2188        return NULL;
2189    else
2190        return xs_fmt("%s/static/%s", snac->basedir, id);
2191}
2192
2193
2194int static_get(snac *snac, const char *id, xs_val **data, int *size,
2195                const char *inm, xs_str **etag)
2196/* returns static content */
2197{
2198    xs *fn = _static_fn(snac, id);
2199
2200    return _load_raw_file(fn, data, size, inm, etag);
2201}
2202
2203
2204void static_put(snac *snac, const char *id, const char *data, int size)
2205/* writes status content */
2206{
2207    xs *fn = _static_fn(snac, id);
2208    FILE *f;
2209
2210    if (fn && (f = fopen(fn, "wb")) != NULL) {
2211        fwrite(data, size, 1, f);
2212        fclose(f);
2213    }
2214}
2215
2216
2217void static_put_meta(snac *snac, const char *id, const char *str)
2218/* puts metadata (i.e. a media description string) to id */
2219{
2220    xs *fn = _static_fn(snac, id);
2221
2222    if (fn) {
2223        fn = xs_str_cat(fn, ".txt");
2224        FILE *f;
2225
2226        if ((f = fopen(fn, "w")) != NULL) {
2227            fprintf(f, "%s\n", str);
2228            fclose(f);
2229        }
2230    }
2231}
2232
2233
2234xs_str *static_get_meta(snac *snac, const char *id)
2235/* gets metadata from a media */
2236{
2237    xs *fn    = _static_fn(snac, id);
2238    xs_str *r = NULL;
2239
2240    if (fn) {
2241        fn = xs_str_cat(fn, ".txt");
2242        FILE *f;
2243
2244        if ((f = fopen(fn, "r")) != NULL) {
2245            r = xs_strip_i(xs_readline(f));
2246            fclose(f);
2247        }
2248    }
2249    else
2250        r = xs_str_new("");
2251
2252    return r;
2253}
2254
2255
2256/** history **/
2257
2258xs_str *_history_fn(snac *snac, const char *id)
2259/* gets the filename for the history */
2260{
2261    if (strchr(id, '/'))
2262        return NULL;
2263    else
2264        return xs_fmt("%s/history/%s", snac->basedir, id);
2265}
2266
2267
2268double history_mtime(snac *snac, const char *id)
2269{
2270    double t = 0.0;
2271    xs *fn = _history_fn(snac, id);
2272
2273    if (fn != NULL)
2274        t = mtime(fn);
2275
2276    return t;
2277}
2278
2279
2280void history_add(snac *snac, const char *id, const char *content, int size,
2281                    xs_str **etag)
2282/* adds something to the history */
2283{
2284    xs *fn = _history_fn(snac, id);
2285    FILE *f;
2286
2287    if (fn && (f = fopen(fn, "w")) != NULL) {
2288        fwrite(content, size, 1, f);
2289        fclose(f);
2290
2291        if (etag) {
2292            double tm = mtime(fn);
2293            *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2294        }
2295    }
2296}
2297
2298
2299int history_get(snac *snac, const char *id, xs_str **content, int *size,
2300                const char *inm, xs_str **etag)
2301{
2302    xs *fn = _history_fn(snac, id);
2303
2304    return _load_raw_file(fn, content, size, inm, etag);
2305}
2306
2307
2308int history_del(snac *snac, const char *id)
2309{
2310    xs *fn = _history_fn(snac, id);
2311
2312    if (fn)
2313        return unlink(fn);
2314    else
2315        return -1;
2316}
2317
2318
2319xs_list *history_list(snac *snac)
2320{
2321    xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2322
2323    return xs_glob(spec, 1, 1);
2324}
2325
2326
2327void lastlog_write(snac *snac, const char *source)
2328/* writes the last time the user logged in */
2329{
2330    xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2331    FILE *f;
2332
2333    if ((f = fopen(fn, "w")) != NULL) {
2334        fprintf(f, "%lf %s\n", ftime(), source);
2335        fclose(f);
2336    }
2337}
2338
2339
2340/** inbox collection **/
2341
2342void inbox_add(const char *inbox)
2343/* collects a shared inbox */
2344{
2345    /* don't collect ourselves */
2346    if (xs_startswith(inbox, srv_baseurl))
2347        return;
2348
2349    xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2350    xs *fn  = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2351    FILE *f;
2352
2353    if ((f = fopen(fn, "w")) != NULL) {
2354        fprintf(f, "%s\n", inbox);
2355        fclose(f);
2356    }
2357}
2358
2359
2360void inbox_add_by_actor(const xs_dict *actor)
2361/* collects an actor's shared inbox, if it has one */
2362{
2363    const char *v;
2364
2365    if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2366        !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2367        /* only collect this inbox if its instance is not blocked */
2368        if (!is_instance_blocked(v))
2369            inbox_add(v);
2370    }
2371}
2372
2373
2374xs_list *inbox_list(void)
2375/* returns the collected inboxes as a list */
2376{
2377    xs_list *ibl = xs_list_new();
2378    xs *spec     = xs_fmt("%s/inbox/" "*", srv_basedir);
2379    xs *files    = xs_glob(spec, 0, 0);
2380    xs_list *p   = files;
2381    const xs_val *v;
2382
2383    while (xs_list_iter(&p, &v)) {
2384        FILE *f;
2385
2386        if ((f = fopen(v, "r")) != NULL) {
2387            xs *line = xs_readline(f);
2388
2389            if (line) {
2390                line = xs_strip_i(line);
2391                ibl  = xs_list_append(ibl, line);
2392            }
2393
2394            fclose(f);
2395        }
2396    }
2397
2398    return ibl;
2399}
2400
2401
2402/** instance-wide operations **/
2403
2404xs_str *_instance_block_fn(const char *instance)
2405{
2406    xs *s   = xs_replace(instance, "http:/" "/", "");
2407    xs *s1  = xs_replace(s, "https:/" "/", "");
2408    xs *l   = xs_split(s1, "/");
2409    const char *p = xs_list_get(l, 0);
2410    xs *md5 = xs_md5_hex(p, strlen(p));
2411
2412    return xs_fmt("%s/block/%s", srv_basedir, md5);
2413}
2414
2415
2416int is_instance_blocked(const char *instance)
2417{
2418    xs *fn = _instance_block_fn(instance);
2419
2420    return !!(mtime(fn) != 0.0);
2421}
2422
2423
2424int instance_block(const char *instance)
2425/* blocks a full instance */
2426{
2427    int ret;
2428
2429    /* create the subdir */
2430    xs *dir = xs_fmt("%s/block/", srv_basedir);
2431    mkdirx(dir);
2432
2433    if (!is_instance_blocked(instance)) {
2434        xs *fn = _instance_block_fn(instance);
2435        FILE *f;
2436
2437        if ((f = fopen(fn, "w")) != NULL) {
2438            fprintf(f, "%s\n", instance);
2439            fclose(f);
2440
2441            ret = 0;
2442        }
2443        else
2444            ret = -1;
2445    }
2446    else
2447        ret = -2;
2448
2449    return ret;
2450}
2451
2452
2453int instance_unblock(const char *instance)
2454/* unblocks a full instance */
2455{
2456    int ret;
2457
2458    if (is_instance_blocked(instance)) {
2459        xs *fn = _instance_block_fn(instance);
2460        ret = unlink(fn);
2461    }
2462    else
2463        ret = -2;
2464
2465    return ret;
2466}
2467
2468
2469/** operations by content **/
2470
2471int content_match(const char *file, const xs_dict *msg)
2472/* checks if a message's content matches any of the regexes in file */
2473/* file format: one regex per line */
2474{
2475    xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2476    FILE *f;
2477    int r = 0;
2478    const char *v = xs_dict_get(msg, "content");
2479
2480    if (xs_type(v) == XSTYPE_STRING && *v) {
2481        if ((f = fopen(fn, "r")) != NULL) {
2482            srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2483
2484            /* massage content (strip HTML tags, etc.) */
2485            xs *c = xs_regex_replace(v, "<[^>]+>", " ");
2486            c = xs_regex_replace_i(c, " {2,}", " ");
2487            c = xs_tolower_i(c);
2488
2489            while (!r && !feof(f)) {
2490                xs *rx = xs_strip_i(xs_readline(f));
2491
2492                if (*rx && xs_regex_match(c, rx)) {
2493                    srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2494                    r = 1;
2495                }
2496            }
2497
2498            fclose(f);
2499        }
2500    }
2501
2502    return r;
2503}
2504
2505
2506xs_list *content_search(snac *user, const char *regex,
2507                int priv, int skip, int show, int max_secs, int *timeout)
2508/* returns a list of posts which content matches the regex */
2509{
2510    if (regex == NULL || *regex == '\0')
2511        return xs_list_new();
2512
2513    xs *i_regex = xs_utf8_to_lower(regex);
2514
2515    xs_set seen;
2516
2517    xs_set_init(&seen);
2518
2519    if (max_secs == 0)
2520        max_secs = 3;
2521
2522    time_t t = time(NULL) + max_secs;
2523    *timeout = 0;
2524
2525    /* iterate all timelines simultaneously */
2526    xs_list *tls[3] = {0};
2527    const char *md5s[3] = {0};
2528    int c[3] = {0};
2529
2530    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL);   /* public */
2531    tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2532    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
2533
2534    /* first positioning */
2535    for (int n = 0; n < 3; n++)
2536        xs_list_next(tls[n], &md5s[n], &c[n]);
2537
2538    show += skip;
2539
2540    while (show > 0) {
2541        /* timeout? */
2542        if (time(NULL) > t) {
2543            *timeout = 1;
2544            break;
2545        }
2546
2547        /* find the newest post */
2548        int newest = -1;
2549        double mtime = 0.0;
2550
2551        for (int n = 0; n < 3; n++) {
2552            if (md5s[n] != NULL) {
2553                xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2554                double mt = mtime(fn);
2555
2556                if (mt > mtime) {
2557                    newest = n;
2558                    mtime = mt;
2559                }
2560            }
2561        }
2562
2563        if (newest == -1)
2564            break;
2565
2566        const char *md5 = md5s[newest];
2567
2568        /* advance the chosen timeline */
2569        if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2570            md5s[newest] = NULL;
2571
2572        xs *post = NULL;
2573
2574        if (!valid_status(object_get_by_md5(md5, &post)))
2575            continue;
2576
2577        if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2578            continue;
2579
2580        const char *id = xs_dict_get(post, "id");
2581
2582        if (id == NULL || is_hidden(user, id))
2583            continue;
2584
2585        xs *c = xs_str_new(NULL);
2586        const char *content = xs_dict_get(post, "content");
2587        const char *name    = xs_dict_get(post, "name");
2588
2589        if (!xs_is_null(content))
2590            c = xs_str_cat(c, content);
2591        if (!xs_is_null(name))
2592            c = xs_str_cat(c, " ", name);
2593
2594        /* add alt-texts from attachments */
2595        const xs_list *atts = xs_dict_get(post, "attachment");
2596        int tc = 0;
2597        const xs_dict *att;
2598
2599        while (xs_list_next(atts, &att, &tc)) {
2600            const char *name = xs_dict_get(att, "name");
2601
2602            if (name != NULL)
2603                c = xs_str_cat(c, " ", name);
2604        }
2605
2606        /* strip HTML */
2607        c = xs_regex_replace_i(c, "<[^>]+>", " ");
2608        c = xs_regex_replace_i(c, " {2,}", " ");
2609
2610        /* convert to lowercase */
2611        xs *lc = xs_utf8_to_lower(c);
2612
2613        /* apply regex */
2614        if (xs_regex_match(lc, i_regex)) {
2615            if (xs_set_add(&seen, md5) == 1)
2616            show--;
2617        }
2618    }
2619
2620    xs_list *r = xs_set_result(&seen);
2621
2622    if (skip) {
2623        /* BAD */
2624        while (skip--) {
2625            r = xs_list_del(r, 0);
2626        }
2627    }
2628
2629    xs_free(tls[0]);
2630    xs_free(tls[1]);
2631    xs_free(tls[2]);
2632
2633    return r;
2634}
2635
2636
2637/** notifications **/
2638
2639xs_str *notify_check_time(snac *snac, int reset)
2640/* gets or resets the latest notification check time */
2641{
2642    xs_str *t = NULL;
2643    xs *fn    = xs_fmt("%s/notifydate.txt", snac->basedir);
2644    FILE *f;
2645
2646    if (reset) {
2647        if ((f = fopen(fn, "w")) != NULL) {
2648            t = tid(0);
2649            fprintf(f, "%s\n", t);
2650            fclose(f);
2651        }
2652    }
2653    else {
2654        if ((f = fopen(fn, "r")) != NULL) {
2655            t = xs_readline(f);
2656            fclose(f);
2657        }
2658        else
2659            /* never set before */
2660            t = xs_fmt("%16.6f", 0.0);
2661    }
2662
2663    return t;
2664}
2665
2666
2667void notify_add(snac *snac, const char *type, const char *utype,
2668                const char *actor, const char *objid, const xs_dict *msg)
2669/* adds a new notification */
2670{
2671    xs *ntid = tid(0);
2672    xs *fn   = xs_fmt("%s/notify/", snac->basedir);
2673    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
2674    FILE *f;
2675
2676    /* create the directory */
2677    mkdirx(fn);
2678    fn = xs_str_cat(fn, ntid);
2679    fn = xs_str_cat(fn, ".json");
2680
2681    xs *noti = xs_dict_new();
2682
2683    noti = xs_dict_append(noti, "id",    ntid);
2684    noti = xs_dict_append(noti, "type",  type);
2685    noti = xs_dict_append(noti, "utype", utype);
2686    noti = xs_dict_append(noti, "actor", actor);
2687    noti = xs_dict_append(noti, "date",  date);
2688    noti = xs_dict_append(noti, "msg",   msg);
2689
2690    if (!xs_is_null(objid))
2691        noti = xs_dict_append(noti, "objid", objid);
2692
2693    if ((f = fopen(fn, "w")) != NULL) {
2694        xs_json_dump(noti, 4, f);
2695        fclose(f);
2696    }
2697
2698    /* add it to the index if it already exists */
2699    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2700
2701    if (mtime(idx) != 0.0) {
2702        pthread_mutex_lock(&data_mutex);
2703
2704        if ((f = fopen(idx, "a")) != NULL) {
2705            fprintf(f, "%-32s\n", ntid);
2706            fclose(f);
2707        }
2708
2709        pthread_mutex_unlock(&data_mutex);
2710    }
2711}
2712
2713
2714xs_dict *notify_get(snac *snac, const char *id)
2715/* gets a notification */
2716{
2717    /* base file */
2718    xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
2719
2720    /* strip spaces and add extension */
2721    fn = xs_strip_i(fn);
2722    fn = xs_str_cat(fn, ".json");
2723
2724    FILE *f;
2725    xs_dict *out = NULL;
2726
2727    if ((f = fopen(fn, "r")) != NULL) {
2728        out = xs_json_load(f);
2729        fclose(f);
2730    }
2731
2732    return out;
2733}
2734
2735
2736xs_list *notify_list(snac *snac, int skip, int show)
2737/* returns a list of notification ids */
2738{
2739    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2740
2741    if (mtime(idx) == 0.0) {
2742        /* create the index from scratch */
2743        FILE *f;
2744
2745        pthread_mutex_lock(&data_mutex);
2746
2747        if ((f = fopen(idx, "w")) != NULL) {
2748            xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
2749            xs *lst  = xs_glob(spec, 1, 0);
2750            xs_list *p = lst;
2751            const char *v;
2752
2753            while (xs_list_iter(&p, &v)) {
2754                char *p = strrchr(v, '.');
2755                if (p) {
2756                    *p = '\0';
2757                    fprintf(f, "%-32s\n", v);
2758                }
2759            }
2760
2761            fclose(f);
2762        }
2763
2764        pthread_mutex_unlock(&data_mutex);
2765    }
2766
2767    return index_list_desc(idx, skip, show);
2768}
2769
2770
2771int notify_new_num(snac *snac)
2772/* counts the number of new notifications */
2773{
2774    xs *t = notify_check_time(snac, 0);
2775    xs *lst = notify_list(snac, 0, XS_ALL);
2776    int cnt = 0;
2777
2778    xs_list *p = lst;
2779    const xs_str *v;
2780
2781    while (xs_list_iter(&p, &v)) {
2782        xs *id = xs_strip_i(xs_dup(v));
2783
2784        /* old? count no more */
2785        if (strcmp(id, t) < 0)
2786            break;
2787
2788        cnt++;
2789    }
2790
2791    return cnt;
2792}
2793
2794
2795void notify_clear(snac *snac)
2796/* clears all notifications */
2797{
2798    xs *spec   = xs_fmt("%s/notify/" "*", snac->basedir);
2799    xs *lst    = xs_glob(spec, 0, 0);
2800    xs_list *p = lst;
2801    const xs_str *v;
2802
2803    while (xs_list_iter(&p, &v))
2804        unlink(v);
2805
2806    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
2807
2808    if (mtime(idx) != 0.0) {
2809        pthread_mutex_lock(&data_mutex);
2810        truncate(idx, 0);
2811        pthread_mutex_unlock(&data_mutex);
2812    }
2813}
2814
2815
2816/** the queue **/
2817
2818static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
2819/* writes safely to the queue */
2820{
2821    xs *tfn = xs_fmt("%s.tmp", fn);
2822    FILE *f;
2823
2824    if ((f = fopen(tfn, "w")) != NULL) {
2825        xs_json_dump(msg, 4, f);
2826        fclose(f);
2827
2828        rename(tfn, fn);
2829    }
2830
2831    return msg;
2832}
2833
2834
2835static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
2836/* creates a queue message */
2837{
2838    int qrt  = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
2839    xs *ntid = tid(retries * 60 * qrt);
2840    xs *rn   = xs_number_new(retries);
2841
2842    xs_dict *qmsg = xs_dict_new();
2843
2844    qmsg = xs_dict_append(qmsg, "type",    type);
2845    qmsg = xs_dict_append(qmsg, "message", msg);
2846    qmsg = xs_dict_append(qmsg, "retries", rn);
2847    qmsg = xs_dict_append(qmsg, "ntid",    ntid);
2848
2849    return qmsg;
2850}
2851
2852
2853void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
2854/* enqueues an input message */
2855{
2856    xs *qmsg   = _new_qmsg("input", msg, retries);
2857    const char *ntid = xs_dict_get(qmsg, "ntid");
2858    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2859
2860    qmsg = xs_dict_append(qmsg, "req", req);
2861
2862    qmsg = _enqueue_put(fn, qmsg);
2863
2864    snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
2865}
2866
2867
2868void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
2869/* enqueues an input message from the shared input */
2870{
2871    xs *qmsg   = _new_qmsg("input", msg, retries);
2872    const char *ntid = xs_dict_get(qmsg, "ntid");
2873    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2874
2875    qmsg = xs_dict_append(qmsg, "req", req);
2876
2877    qmsg = _enqueue_put(fn, qmsg);
2878
2879    srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
2880}
2881
2882
2883void enqueue_output_raw(const char *keyid, const char *seckey,
2884                        const xs_dict *msg, const xs_str *inbox,
2885                        int retries, int p_status)
2886/* enqueues an output message to an inbox */
2887{
2888    xs *qmsg   = _new_qmsg("output", msg, retries);
2889    const char *ntid = xs_dict_get(qmsg, "ntid");
2890    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2891
2892    xs *ns = xs_number_new(p_status);
2893    qmsg = xs_dict_append(qmsg, "p_status", ns);
2894
2895    qmsg = xs_dict_append(qmsg, "inbox",  inbox);
2896    qmsg = xs_dict_append(qmsg, "keyid",  keyid);
2897    qmsg = xs_dict_append(qmsg, "seckey", seckey);
2898
2899    /* if it's to be sent right now, bypass the disk queue and post the job */
2900    if (retries == 0 && p_state != NULL)
2901        job_post(qmsg, 0);
2902    else {
2903        qmsg = _enqueue_put(fn, qmsg);
2904        srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
2905    }
2906}
2907
2908
2909void enqueue_output(snac *snac, const xs_dict *msg,
2910                    const xs_str *inbox, int retries, int p_status)
2911/* enqueues an output message to an inbox */
2912{
2913    if (xs_startswith(inbox, snac->actor)) {
2914        snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
2915        return;
2916    }
2917
2918    const char *seckey = xs_dict_get(snac->key, "secret");
2919
2920    enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
2921}
2922
2923
2924void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
2925                            const xs_str *actor, int retries)
2926/* enqueues an output message for an actor */
2927{
2928    xs *inbox = get_actor_inbox(actor);
2929
2930    if (!xs_is_null(inbox))
2931        enqueue_output(snac, msg, inbox, retries, 0);
2932    else
2933        snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
2934}
2935
2936
2937void enqueue_email(const xs_str *msg, int retries)
2938/* enqueues an email message to be sent */
2939{
2940    xs *qmsg   = _new_qmsg("email", msg, retries);
2941    const char *ntid = xs_dict_get(qmsg, "ntid");
2942    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2943
2944    qmsg = _enqueue_put(fn, qmsg);
2945
2946    srv_debug(1, xs_fmt("enqueue_email %d", retries));
2947}
2948
2949
2950void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
2951/* enqueues a message to be sent via Telegram */
2952{
2953    xs *qmsg   = _new_qmsg("telegram", msg, 0);
2954    const char *ntid = xs_dict_get(qmsg, "ntid");
2955    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2956
2957    qmsg = xs_dict_append(qmsg, "bot",      bot);
2958    qmsg = xs_dict_append(qmsg, "chat_id",  chat_id);
2959
2960    qmsg = _enqueue_put(fn, qmsg);
2961
2962    srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
2963}
2964
2965void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
2966/* enqueues a message to be sent via ntfy */
2967{
2968    xs *qmsg   = _new_qmsg("ntfy", msg, 0);
2969    const char *ntid = xs_dict_get(qmsg, "ntid");
2970    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
2971
2972    qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
2973    qmsg = xs_dict_append(qmsg, "ntfy_token",  ntfy_token);
2974
2975
2976    qmsg = _enqueue_put(fn, qmsg);
2977
2978    srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
2979}
2980
2981void enqueue_message(snac *snac, const xs_dict *msg)
2982/* enqueues an output message */
2983{
2984    xs *qmsg   = _new_qmsg("message", msg, 0);
2985    const char *ntid = xs_dict_get(qmsg, "ntid");
2986    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
2987
2988    qmsg = _enqueue_put(fn, qmsg);
2989
2990    snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
2991}
2992
2993
2994void enqueue_close_question(snac *user, const char *id, int end_secs)
2995/* enqueues the closing of a question */
2996{
2997    xs *qmsg = _new_qmsg("close_question", id, 0);
2998    xs *ntid = tid(end_secs);
2999    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3000
3001    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3002
3003    qmsg = _enqueue_put(fn, qmsg);
3004
3005    snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3006}
3007
3008
3009void enqueue_object_request(snac *user, const char *id, int forward_secs)
3010/* enqueues the request of an object in the future */
3011{
3012    xs *qmsg = _new_qmsg("object_request", id, 0);
3013    xs *ntid = tid(forward_secs);
3014    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3015
3016    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3017
3018    qmsg = _enqueue_put(fn, qmsg);
3019
3020    snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3021}
3022
3023
3024void enqueue_verify_links(snac *user)
3025/* enqueues a link verification */
3026{
3027    xs *qmsg   = _new_qmsg("verify_links", "", 0);
3028    const char *ntid = xs_dict_get(qmsg, "ntid");
3029    xs *fn     = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3030
3031    qmsg = _enqueue_put(fn, qmsg);
3032
3033    snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3034}
3035
3036
3037void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3038/* enqueues an actor refresh */
3039{
3040    xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3041    xs *ntid = tid(forward_secs);
3042    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3043
3044    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3045    qmsg = xs_dict_append(qmsg, "actor", actor);
3046
3047    qmsg = _enqueue_put(fn, qmsg);
3048
3049    snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3050}
3051
3052
3053int was_question_voted(snac *user, const char *id)
3054/* returns true if the user voted in this poll */
3055{
3056    xs *children = object_children(id);
3057    int voted = 0;
3058    xs_list *p;
3059    const xs_str *md5;
3060
3061    p = children;
3062    while (xs_list_iter(&p, &md5)) {
3063        xs *obj = NULL;
3064
3065        if (valid_status(object_get_by_md5(md5, &obj))) {
3066            const char *atto = get_atto(obj);
3067            if (atto && strcmp(atto, user->actor) == 0 &&
3068                !xs_is_null(xs_dict_get(obj, "name"))) {
3069                voted = 1;
3070                break;
3071            }
3072        }
3073    }
3074
3075    return voted;
3076}
3077
3078
3079xs_list *user_queue(snac *snac)
3080/* returns a list with filenames that can be dequeued */
3081{
3082    xs *spec      = xs_fmt("%s/queue/" "*.json", snac->basedir);
3083    xs_list *list = xs_list_new();
3084    time_t t      = time(NULL);
3085    xs_list *p;
3086    const xs_val *v;
3087
3088    xs *fns = xs_glob(spec, 0, 0);
3089
3090    p = fns;
3091    while (xs_list_iter(&p, &v)) {
3092        /* get the retry time from the basename */
3093        char *bn  = strrchr(v, '/');
3094        time_t t2 = atol(bn + 1);
3095
3096        if (t2 > t)
3097            snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3098        else {
3099            list = xs_list_append(list, v);
3100            snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3101        }
3102    }
3103
3104    return list;
3105}
3106
3107
3108xs_list *queue(void)
3109/* returns a list with filenames that can be dequeued */
3110{
3111    xs *spec      = xs_fmt("%s/queue/" "*.json", srv_basedir);
3112    xs_list *list = xs_list_new();
3113    time_t t      = time(NULL);
3114    xs_list *p;
3115    const xs_val *v;
3116
3117    xs *fns = xs_glob(spec, 0, 0);
3118
3119    p = fns;
3120    while (xs_list_iter(&p, &v)) {
3121        /* get the retry time from the basename */
3122        char *bn  = strrchr(v, '/');
3123        time_t t2 = atol(bn + 1);
3124
3125        if (t2 > t)
3126            srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3127        else {
3128            list = xs_list_append(list, v);
3129            srv_debug(2, xs_fmt("queue ready for %s", v));
3130        }
3131    }
3132
3133    return list;
3134}
3135
3136
3137xs_dict *queue_get(const char *fn)
3138/* gets a file from a queue */
3139{
3140    FILE *f;
3141    xs_dict *obj = NULL;
3142
3143    if ((f = fopen(fn, "r")) != NULL) {
3144        obj = xs_json_load(f);
3145        fclose(f);
3146    }
3147
3148    return obj;
3149}
3150
3151
3152xs_dict *dequeue(const char *fn)
3153/* dequeues a message */
3154{
3155    xs_dict *obj = queue_get(fn);
3156
3157    unlink(fn);
3158
3159    return obj;
3160}
3161
3162
3163/** the purge **/
3164
3165static int _purge_file(const char *fn, time_t mt)
3166/* purge fn if it's older than days */
3167{
3168    int ret = 0;
3169
3170    if (mtime(fn) < mt) {
3171        /* older than the minimum time: delete it */
3172        unlink(fn);
3173        srv_debug(2, xs_fmt("purged %s", fn));
3174        ret = 1;
3175    }
3176
3177    return ret;
3178}
3179
3180
3181static void _purge_dir(const char *dir, int days)
3182/* purges all files in a directory older than days */
3183{
3184    int cnt = 0;
3185
3186    if (days) {
3187        time_t mt = time(NULL) - days * 24 * 3600;
3188        xs *spec  = xs_fmt("%s/" "*", dir);
3189        xs *list  = xs_glob(spec, 0, 0);
3190        xs_list *p;
3191        const xs_str *v;
3192
3193        p = list;
3194        while (xs_list_iter(&p, &v))
3195            cnt += _purge_file(v, mt);
3196
3197        srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3198    }
3199}
3200
3201
3202static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3203/* purges all files in a user subdir older than days */
3204{
3205    xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3206
3207    _purge_dir(u_subdir, days);
3208}
3209
3210
3211void purge_server(void)
3212/* purge global server data */
3213{
3214    xs *spec = xs_fmt("%s/object/??", srv_basedir);
3215    xs *dirs = xs_glob(spec, 0, 0);
3216    xs_list *p;
3217    const xs_str *v;
3218    int cnt = 0;
3219    int icnt = 0;
3220
3221    time_t mt = time(NULL) - 7 * 24 * 3600;
3222
3223    p = dirs;
3224    while (xs_list_iter(&p, &v)) {
3225        xs_list *p2;
3226        const xs_str *v2;
3227
3228        {
3229            xs *spec2 = xs_fmt("%s/" "*.json", v);
3230            xs *files = xs_glob(spec2, 0, 0);
3231
3232            p2 = files;
3233            while (xs_list_iter(&p2, &v2)) {
3234                int n_link;
3235
3236                /* old and with no hard links? */
3237                if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3238                    xs *s1    = xs_replace(v2, ".json", "");
3239                    xs *l     = xs_split(s1, "/");
3240                    const char *md5 = xs_list_get(l, -1);
3241
3242                    object_del_by_md5(md5);
3243                    cnt++;
3244                }
3245            }
3246        }
3247
3248        {
3249            /* look for stray indexes */
3250            xs *speci = xs_fmt("%s/" "*_?.idx", v);
3251            xs *idxfs = xs_glob(speci, 0, 0);
3252
3253            p2 = idxfs;
3254            while (xs_list_iter(&p2, &v2)) {
3255                /* old enough to consider? */
3256                if (mtime(v2) < mt) {
3257                    /* check if the indexed object is here */
3258                    xs *o = xs_dup(v2);
3259                    char *ext = strchr(o, '_');
3260
3261                    if (ext) {
3262                        *ext = '\0';
3263                        o = xs_str_cat(o, ".json");
3264
3265                        if (mtime(o) == 0.0) {
3266                            /* delete */
3267                            unlink(v2);
3268                            srv_debug(1, xs_fmt("purged %s", v2));
3269                            icnt++;
3270                        }
3271                    }
3272                }
3273            }
3274
3275            /* delete index backups */
3276            xs *specb = xs_fmt("%s/" "*.bak", v);
3277            xs *bakfs = xs_glob(specb, 0, 0);
3278
3279            p2 = bakfs;
3280            while (xs_list_iter(&p2, &v2)) {
3281                unlink(v2);
3282                srv_debug(1, xs_fmt("purged %s", v2));
3283            }
3284        }
3285    }
3286
3287    /* purge collected inboxes */
3288    xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3289    _purge_dir(ib_dir, 7);
3290
3291    /* purge the instance timeline */
3292    xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3293    int itl_gc = index_gc(itl_fn);
3294
3295    /* purge tag indexes */
3296    xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3297    xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3298    p = tag_dirs;
3299
3300    int tag_gc = 0;
3301    while (xs_list_iter(&p, &v)) {
3302        xs *spec2 = xs_fmt("%s/" "*.idx", v);
3303        xs *files = xs_glob(spec2, 0, 0);
3304        xs_list *p2;
3305        const xs_str *v2;
3306
3307        p2 = files;
3308        while (xs_list_iter(&p2, &v2)) {
3309            tag_gc += index_gc(v2);
3310            xs *bak = xs_fmt("%s.bak", v2);
3311            unlink(bak);
3312
3313            if (index_len(v2) == 0) {
3314                /* there are no longer any entry with this tag;
3315                   purge it completely */
3316                unlink(v2);
3317                xs *dottag = xs_replace(v2, ".idx", ".tag");
3318                unlink(dottag);
3319            }
3320        }
3321    }
3322
3323    srv_debug(1, xs_fmt("purge: global "
3324            "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3325}
3326
3327
3328void purge_user(snac *snac)
3329/* do the purge for this user */
3330{
3331    int priv_days, pub_days, user_days = 0;
3332    const char *v;
3333    int n;
3334
3335    priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3336    pub_days  = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3337
3338    if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3339        (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3340        user_days = xs_number_get(v);
3341
3342    if (user_days) {
3343        /* override admin settings only if they are lesser */
3344        if (priv_days == 0 || user_days < priv_days)
3345            priv_days = user_days;
3346
3347        if (pub_days == 0 || user_days < pub_days)
3348            pub_days = user_days;
3349    }
3350
3351    _purge_user_subdir(snac, "hidden",  priv_days);
3352    _purge_user_subdir(snac, "private", priv_days);
3353
3354    _purge_user_subdir(snac, "public",  pub_days);
3355
3356    const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
3357                           "pinned.idx", "bookmark.idx", "draft.idx", NULL };
3358
3359    for (n = 0; idxs[n]; n++) {
3360        xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3361        int gc = index_gc(idx);
3362        srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3363    }
3364
3365    /* purge lists */
3366    {
3367        xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3368        xs *lol  = xs_glob(spec, 0, 0);
3369        int c = 0;
3370        const char *v;
3371
3372        while (xs_list_next(lol, &v, &c)) {
3373            int gc = index_gc(v);
3374            srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3375        }
3376    }
3377
3378    /* unrelated to purging, but it's a janitorial process, so what the hell */
3379    verify_links(snac);
3380}
3381
3382
3383void purge_all(void)
3384/* purge all users */
3385{
3386    snac snac;
3387    xs *list = user_list();
3388    char *p;
3389    const char *uid;
3390
3391    p = list;
3392    while (xs_list_iter(&p, &uid)) {
3393        if (user_open(&snac, uid)) {
3394            purge_user(&snac);
3395            user_free(&snac);
3396        }
3397    }
3398
3399    purge_server();
3400
3401#ifndef NO_MASTODON_API
3402    mastoapi_purge();
3403#endif
3404}
3405
3406
3407/** archive **/
3408
3409void srv_archive(const char *direction, const char *url, xs_dict *req,
3410                 const char *payload, int p_size,
3411                 int status, xs_dict *headers,
3412                 const char *body, int b_size)
3413/* archives a connection */
3414{
3415    /* obsessive archiving */
3416    xs *date = tid(0);
3417    xs *dir  = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3418    FILE *f;
3419
3420    if (mkdirx(dir) != -1) {
3421        xs *meta_fn = xs_fmt("%s/_META", dir);
3422
3423        if ((f = fopen(meta_fn, "w")) != NULL) {
3424            xs *j1 = xs_json_dumps(req, 4);
3425            xs *j2 = xs_json_dumps(headers, 4);
3426
3427            fprintf(f, "dir: %s\n", direction);
3428
3429            if (url)
3430                fprintf(f, "url: %s\n", url);
3431
3432            fprintf(f, "req: %s\n", j1);
3433            fprintf(f, "p_size: %d\n", p_size);
3434            fprintf(f, "status: %d\n", status);
3435            fprintf(f, "response: %s\n", j2);
3436            fprintf(f, "b_size: %d\n", b_size);
3437            fclose(f);
3438        }
3439
3440        if (p_size && payload) {
3441            xs *payload_fn = NULL;
3442            xs *payload_fn_raw = NULL;
3443            const char *v = xs_dict_get(req, "content-type");
3444
3445            if (v && xs_str_in(v, "json") != -1) {
3446                payload_fn = xs_fmt("%s/payload.json", dir);
3447
3448                if ((f = fopen(payload_fn, "w")) != NULL) {
3449                    xs *v1 = xs_json_loads(payload);
3450                    xs *j1 = NULL;
3451
3452                    if (v1 != NULL)
3453                        j1 = xs_json_dumps(v1, 4);
3454
3455                    if (j1 != NULL)
3456                        fwrite(j1, strlen(j1), 1, f);
3457                    else
3458                        fwrite(payload, p_size, 1, f);
3459
3460                    fclose(f);
3461                }
3462            }
3463
3464            payload_fn_raw = xs_fmt("%s/payload", dir);
3465
3466            if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3467                fwrite(payload, p_size, 1, f);
3468                fclose(f);
3469            }
3470        }
3471
3472        if (b_size && body) {
3473            xs *body_fn = NULL;
3474            const char *v = xs_dict_get(headers, "content-type");
3475
3476            if (v && xs_str_in(v, "json") != -1) {
3477                body_fn = xs_fmt("%s/body.json", dir);
3478
3479                if ((f = fopen(body_fn, "w")) != NULL) {
3480                    xs *v1 = xs_json_loads(body);
3481                    xs *j1 = NULL;
3482
3483                    if (v1 != NULL)
3484                        j1 = xs_json_dumps(v1, 4);
3485
3486                    if (j1 != NULL)
3487                        fwrite(j1, strlen(j1), 1, f);
3488                    else
3489                        fwrite(body, b_size, 1, f);
3490
3491                    fclose(f);
3492                }
3493            }
3494            else {
3495                body_fn = xs_fmt("%s/body", dir);
3496
3497                if ((f = fopen(body_fn, "w")) != NULL) {
3498                    fwrite(body, b_size, 1, f);
3499                    fclose(f);
3500                }
3501            }
3502        }
3503    }
3504}
3505
3506
3507void srv_archive_error(const char *prefix, const xs_str *err,
3508                       const xs_dict *req, const xs_val *data)
3509/* archives an error */
3510{
3511    xs *ntid = tid(0);
3512    xs *fn   = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
3513    FILE *f;
3514
3515    if ((f = fopen(fn, "w")) != NULL) {
3516        fprintf(f, "Error: %s\n", err);
3517
3518        if (req) {
3519            fprintf(f, "Request headers:\n");
3520
3521            xs_json_dump(req, 4, f);
3522
3523            fprintf(f, "\n");
3524        }
3525
3526        if (data) {
3527            fprintf(f, "Data:\n");
3528
3529            if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
3530                xs_json_dump(data, 4, f);
3531            }
3532            else
3533                fprintf(f, "%s", data);
3534
3535            fprintf(f, "\n");
3536        }
3537
3538        fclose(f);
3539    }
3540}
3541
3542
3543void srv_archive_qitem(const char *prefix, xs_dict *q_item)
3544/* archives a q_item in the error folder */
3545{
3546    xs *ntid = tid(0);
3547    xs *fn   = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
3548    FILE *f;
3549
3550    if ((f = fopen(fn, "w")) != NULL) {
3551        xs_json_dump(q_item, 4, f);
3552        fclose(f);
3553    }
3554}
3555
3556
3557t_announcement *announcement(const double after)
3558/* returns announcement text or NULL if none exists or it is olde than "after" */
3559{
3560    static const long int MAX_SIZE = 2048;
3561    static t_announcement a = {
3562        .text = NULL,
3563        .timestamp = 0.0,
3564    };
3565    static xs_str *fn = NULL;
3566    if (fn == NULL)
3567        fn = xs_fmt("%s/announcement.txt", srv_basedir);
3568
3569    const double ts = mtime(fn);
3570
3571    /* file does not exist or other than what was requested */
3572    if (ts == 0.0 || ts <= after)
3573        return NULL;
3574
3575    /* nothing changed, just return the current announcement */
3576    if (a.text != NULL && ts <= a.timestamp)
3577        return &a;
3578
3579    /* read and store new announcement */
3580    FILE *f;
3581
3582    if ((f = fopen(fn, "r")) != NULL) {
3583        fseek (f, 0, SEEK_END);
3584        const long int length = ftell(f);
3585
3586        if (length > MAX_SIZE) {
3587            /* this is probably unintentional */
3588            srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
3589        }
3590        else
3591        if (length > 0) {
3592            fseek (f, 0, SEEK_SET);
3593            char *buffer = malloc(length + 1);
3594            if (buffer) {
3595                fread(buffer, 1, length, f);
3596                buffer[length] = '\0';
3597
3598                free(a.text);
3599                a.text = buffer;
3600                a.timestamp = ts;
3601            }
3602            else {
3603                srv_log("Error allocating memory for announcement");
3604            }
3605        }
3606        else {
3607            /* an empty file means no announcement */
3608            free(a.text);
3609            a.text = NULL;
3610            a.timestamp = 0.0;
3611        }
3612
3613        fclose (f);
3614    }
3615
3616    if (a.text != NULL)
3617        return &a;
3618
3619    return NULL;
3620}