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