2.89 data.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2026 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#include "xs_random.h"
  16#include "xs_po.h"
  17#include "xs_http.h"
  18
  19#include "snac.h"
  20
  21#include <time.h>
  22#include <sys/stat.h>
  23#include <sys/file.h>
  24#include <sys/time.h>
  25#include <fcntl.h>
  26#include <pthread.h>
  27
  28double disk_layout = 2.7;
  29
  30/* storage serializer */
  31pthread_mutex_t data_mutex = {0};
  32
  33int snac_upgrade(xs_str **error);
  34
  35
  36int srv_open(const char *basedir, int auto_upgrade)
  37/* opens a server */
  38{
  39    int ret = 0;
  40    xs *cfg_file = NULL;
  41    FILE *f;
  42    xs_str *error = NULL;
  43
  44    pthread_mutex_init(&data_mutex, NULL);
  45
  46    srv_basedir = xs_str_new(basedir);
  47
  48    if (xs_endswith(srv_basedir, "/"))
  49        srv_basedir = xs_crop_i(srv_basedir, 0, -1);
  50
  51    cfg_file = xs_fmt("%s/server.json", basedir);
  52
  53    if ((f = fopen(cfg_file, "r")) == NULL)
  54        error = xs_fmt("ERROR: cannot open '%s'", cfg_file);
  55    else {
  56        /* read full config file */
  57        srv_config = xs_json_load(f);
  58        fclose(f);
  59
  60        /* parse */
  61
  62        if (srv_config == NULL)
  63            error = xs_fmt("ERROR: cannot parse '%s'", cfg_file);
  64        else {
  65            const char *host;
  66            const char *prefix;
  67            const char *dbglvl;
  68            const char *proto;
  69
  70            host   = xs_dict_get(srv_config, "host");
  71            prefix = xs_dict_get(srv_config, "prefix");
  72            dbglvl = xs_dict_get(srv_config, "dbglevel");
  73            proto  = xs_dict_get_def(srv_config, "protocol", "https");
  74
  75            if (host == NULL || prefix == NULL)
  76                error = xs_str_new("ERROR: cannot get server data");
  77            else {
  78                srv_baseurl = xs_fmt("%s:/" "/%s%s", proto, host, prefix);
  79
  80                dbglevel = (int) xs_number_get(dbglvl);
  81
  82                if ((dbglvl = getenv("DEBUG")) != NULL) {
  83                    dbglevel = atoi(dbglvl);
  84                    error = xs_fmt("DEBUG level set to %d from environment", dbglevel);
  85                }
  86
  87                if (auto_upgrade)
  88                    ret = snac_upgrade(&error);
  89                else {
  90                    if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
  91                        error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
  92                    else {
  93                        if (!check_strip_tool()) {
  94                            const char *mp = xs_dict_get(srv_config, "mogrify_path");
  95                            if (mp == NULL) mp = "mogrify";
  96                            error = xs_fmt("ERROR: strip_exif enabled but '%s' not found or not working (set 'mogrify_path' in server.json)", mp);
  97                        }
  98                        else
  99                            ret = 1;
 100                    }
 101                }
 102            }
 103
 104        }
 105    }
 106
 107    if (error != NULL)
 108        srv_log(error);
 109
 110    if (!ret)
 111        return ret;
 112
 113    /* create the queue/ subdir, just in case */
 114    xs *qdir = xs_fmt("%s/queue", srv_basedir);
 115    mkdirx(qdir);
 116
 117    xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
 118    mkdirx(ibdir);
 119
 120    xs *tmpdir = xs_fmt("%s/tmp", srv_basedir);
 121    mkdirx(tmpdir);
 122
 123    xs *faildir = xs_fmt("%s/failure", srv_basedir);
 124    mkdirx(faildir);
 125
 126#ifdef __APPLE__
 127/* Apple uses st_atimespec instead of st_atim etc */
 128#define st_atim st_atimespec
 129#define st_ctim st_ctimespec
 130#define st_mtim st_mtimespec
 131#endif
 132
 133    sbox_enter(srv_basedir);
 134
 135    /* read (and drop) emojis.json, possibly creating it */
 136    xs_free(emojis());
 137
 138    /* if style.css does not exist, create it */
 139    xs *css_fn = xs_fmt("%s/style.css", srv_basedir);
 140
 141    if (mtime(css_fn) == 0) {
 142        srv_log(xs_fmt("Writing style.css"));
 143        write_default_css();
 144    }
 145
 146    /* create the proxy token seed */
 147    {
 148        char rnd[16];
 149        xs_rnd_buf(rnd, sizeof(rnd));
 150
 151        srv_proxy_token_seed = xs_hex_enc(rnd, sizeof(rnd));
 152    }
 153
 154    /* ensure user directories include important subdirectories */
 155    xs *users = user_list();
 156    const char *uid;
 157
 158    xs_list_foreach(users, uid) {
 159        xs *impdir = xs_fmt("%s/user/%s/import", srv_basedir, uid);
 160        xs *expdir = xs_fmt("%s/user/%s/export", srv_basedir, uid);
 161
 162        mkdirx(impdir);
 163        mkdirx(expdir);
 164    }
 165
 166    /* languages */
 167    srv_langs = xs_dict_new();
 168    srv_langs = xs_dict_set(srv_langs, "en", xs_stock(XSTYPE_NULL));
 169
 170    xs *l_dir = xs_fmt("%s/lang/", srv_basedir);
 171    mkdirx(l_dir);
 172
 173    l_dir = xs_str_cat(l_dir, "*.po");
 174    xs *pos = xs_glob(l_dir, 0, 0);
 175    const char *po;
 176
 177    xs_list_foreach(pos, po) {
 178        xs *d = xs_po_to_dict(po);
 179
 180        if (xs_is_dict(d)) {
 181            xs *l = xs_split(po, "/");
 182            xs *id = xs_dup(xs_list_get(l, -1));
 183            id = xs_replace_i(id, ".po", "");
 184
 185            srv_langs = xs_dict_set(srv_langs, id, d);
 186        }
 187    }
 188
 189    return ret;
 190}
 191
 192
 193void srv_free(void)
 194{
 195    xs_free(srv_basedir);
 196    xs_free(srv_config);
 197    xs_free(srv_baseurl);
 198
 199    pthread_mutex_destroy(&data_mutex);
 200}
 201
 202
 203void user_free(snac *snac)
 204/* frees a user snac */
 205{
 206    xs_free(snac->uid);
 207    xs_free(snac->basedir);
 208    xs_free(snac->config);
 209    xs_free(snac->config_o);
 210    xs_free(snac->key);
 211    xs_free(snac->links);
 212    xs_free(snac->actor);
 213    xs_free(snac->md5);
 214}
 215
 216
 217int user_open(snac *user, const char *uid)
 218/* opens a user */
 219{
 220    int ret = 0;
 221
 222    *user = (snac){0};
 223
 224    if (validate_uid(uid)) {
 225        xs *cfg_file = NULL;
 226        FILE *f;
 227
 228        xs *t = xs_fmt("%s/user/%s", srv_basedir, uid);
 229
 230        if (mtime(t) == 0.0) {
 231            /* user folder does not exist; try with a different case */
 232            xs *lcuid = xs_tolower_i(xs_dup(uid));
 233            xs *ulist = user_list();
 234            xs_list *p = ulist;
 235            const xs_str *v;
 236
 237            while (xs_list_iter(&p, &v)) {
 238                xs *v2 = xs_tolower_i(xs_dup(v));
 239
 240                if (strcmp(lcuid, v2) == 0) {
 241                    user->uid = xs_dup(v);
 242                    break;
 243                }
 244            }
 245        }
 246        else
 247            user->uid = xs_str_new(uid);
 248
 249        if (user->uid == NULL)
 250            return ret;
 251
 252        user->basedir = xs_fmt("%s/user/%s", srv_basedir, user->uid);
 253
 254        cfg_file = xs_fmt("%s/user.json", user->basedir);
 255
 256        if ((f = fopen(cfg_file, "r")) != NULL) {
 257            /* read full config file */
 258            user->config = xs_json_load(f);
 259            fclose(f);
 260
 261            if (user->config != NULL) {
 262                xs *key_file = xs_fmt("%s/key.json", user->basedir);
 263
 264                if ((f = fopen(key_file, "r")) != NULL) {
 265                    user->key = xs_json_load(f);
 266                    fclose(f);
 267
 268                    if (user->key != NULL) {
 269                        user->actor = xs_fmt("%s/%s", srv_baseurl, user->uid);
 270                        user->md5   = xs_md5_hex(user->actor, strlen(user->actor));
 271
 272                        /* everything is ok right now */
 273                        ret = 1;
 274
 275                        /* does it have a configuration override? */
 276                        xs *cfg_file_o = xs_fmt("%s/user_o.json", user->basedir);
 277                        if ((f = fopen(cfg_file_o, "r")) != NULL) {
 278                            user->config_o = xs_json_load(f);
 279                            fclose(f);
 280
 281                            if (user->config_o == NULL)
 282                                srv_log(xs_fmt("error parsing '%s'", cfg_file_o));
 283                        }
 284
 285                        if (user->config_o == NULL)
 286                            user->config_o = xs_dict_new();
 287                    }
 288                    else
 289                        srv_log(xs_fmt("error parsing '%s'", key_file));
 290                }
 291                else
 292                    srv_log(xs_fmt("error opening '%s' %d", key_file, errno));
 293            }
 294            else
 295                srv_log(xs_fmt("error parsing '%s'", cfg_file));
 296
 297            user->tz = xs_dict_get_def(user->config, "tz", "UTC");
 298        }
 299        else
 300            srv_debug(2, xs_fmt("error opening '%s' %d", cfg_file, errno));
 301
 302        /* verified links */
 303        xs *links_file = xs_fmt("%s/links.json", user->basedir);
 304
 305        if ((f = fopen(links_file, "r")) != NULL) {
 306            user->links = xs_json_load(f);
 307            fclose(f);
 308        }
 309    }
 310    else
 311        srv_debug(1, xs_fmt("invalid user '%s'", uid));
 312
 313    if (!ret)
 314        user_free(user);
 315
 316    return ret;
 317}
 318
 319
 320xs_list *user_list(void)
 321/* returns the list of user ids */
 322{
 323    xs *spec = xs_fmt("%s/user/" "*", srv_basedir);
 324    return xs_glob(spec, 1, 0);
 325}
 326
 327
 328int user_open_by_md5(snac *snac, const char *md5)
 329/* iterates all users searching by md5 */
 330{
 331    xs *ulist  = user_list();
 332    xs_list *p = ulist;
 333    const xs_str *v;
 334
 335    while (xs_list_iter(&p, &v)) {
 336        user_open(snac, v);
 337
 338        if (strcmp(snac->md5, md5) == 0)
 339            return 1;
 340
 341        user_free(snac);
 342    }
 343
 344    return 0;
 345}
 346
 347
 348int user_persist(snac *snac, int publish)
 349/* store user */
 350{
 351    xs *fn  = xs_fmt("%s/user.json", snac->basedir);
 352    xs *bfn = xs_fmt("%s.bak", fn);
 353    FILE *f;
 354
 355    if (publish) {
 356        /* check if any of the relevant fields have really changed */
 357        if ((f = fopen(fn, "r")) != NULL) {
 358            xs *old = xs_json_load(f);
 359            fclose(f);
 360
 361            if (old != NULL) {
 362                int nw = 0;
 363                const char *fields[] = { "header", "avatar", "name", "bio", "alias", "alias_raw",
 364                                         "metadata", "latitude", "longitude", NULL };
 365
 366                for (int n = 0; fields[n]; n++) {
 367                    const char *of = xs_dict_get(old, fields[n]);
 368                    const char *nf = xs_dict_get(snac->config, fields[n]);
 369
 370                    if (of == NULL && nf == NULL)
 371                        continue;
 372
 373                    if (xs_type(of) != XSTYPE_STRING || xs_type(nf) != XSTYPE_STRING || strcmp(of, nf)) {
 374                        nw = 1;
 375                        break;
 376                    }
 377                }
 378
 379                if (!nw)
 380                    publish = 0;
 381                else {
 382                    /* uncache the actor object */
 383                    object_del(snac->actor);
 384                }
 385            }
 386        }
 387    }
 388
 389    rename(fn, bfn);
 390
 391    if ((f = fopen(fn, "w")) != NULL) {
 392        xs_json_dump(snac->config, 4, f);
 393        fclose(f);
 394    }
 395    else
 396        rename(bfn, fn);
 397
 398    history_del(snac, "timeline.html_");
 399    timeline_touch(snac);
 400
 401    if (publish) {
 402        xs *a_msg = msg_actor(snac);
 403        xs *u_msg = msg_update(snac, a_msg);
 404
 405        enqueue_message(snac, u_msg);
 406
 407        enqueue_verify_links(snac);
 408    }
 409
 410    return 0;
 411}
 412
 413
 414double mtime_nl(const char *fn, int *n_link)
 415/* returns the mtime and number of links of a file or directory, or 0.0 */
 416{
 417    struct stat st;
 418    double r = 0.0;
 419    int n = 0;
 420
 421    if (fn && stat(fn, &st) != -1) {
 422        r = (double) st.st_mtim.tv_sec;
 423        n = st.st_nlink;
 424    }
 425
 426    if (n_link)
 427        *n_link = n;
 428
 429    return r;
 430}
 431
 432
 433#define MIN(v1, v2) ((v1) < (v2) ? (v1) : (v2))
 434
 435double f_ctime(const char *fn)
 436/* returns the ctime of a file or directory, or 0.0 */
 437{
 438    struct stat st;
 439    double r = 0.0;
 440
 441    if (fn && stat(fn, &st) != -1) {
 442        /* return the lowest of ctime and mtime;
 443           there are operations that change the ctime, like link() */
 444        r = (double) MIN(st.st_ctim.tv_sec, st.st_mtim.tv_sec);
 445    }
 446
 447    return r;
 448}
 449
 450
 451int is_md5_hex(const char *md5)
 452{
 453    return xs_is_hex(md5) && strlen(md5) == MD5_HEX_SIZE - 1;
 454}
 455
 456
 457/** database 2.1+ **/
 458
 459/** indexes **/
 460
 461
 462int index_add_md5(const char *fn, const char *md5)
 463/* adds an md5 to an index */
 464{
 465    int status = HTTP_STATUS_CREATED;
 466    FILE *f;
 467
 468    if (!is_md5_hex(md5)) {
 469        srv_log(xs_fmt("index_add_md5: bad md5 %s %s", fn, md5));
 470        return HTTP_STATUS_BAD_REQUEST;
 471    }
 472
 473    pthread_mutex_lock(&data_mutex);
 474
 475    if ((f = fopen(fn, "a")) != NULL) {
 476        flock(fileno(f), LOCK_EX);
 477
 478        /* ensure the position is at the end after getting the lock */
 479        fseek(f, 0, SEEK_END);
 480
 481        fprintf(f, "%s\n", md5);
 482        fclose(f);
 483    }
 484    else
 485        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 486
 487    pthread_mutex_unlock(&data_mutex);
 488
 489    return status;
 490}
 491
 492
 493int index_add(const char *fn, const char *id)
 494/* adds an id to an index */
 495{
 496    xs *md5 = xs_md5_hex(id, strlen(id));
 497    return index_add_md5(fn, md5);
 498}
 499
 500
 501int index_del_md5(const char *fn, const char *md5)
 502/* deletes an md5 from an index */
 503{
 504    int status = HTTP_STATUS_NOT_FOUND;
 505    FILE *f;
 506
 507    pthread_mutex_lock(&data_mutex);
 508
 509    if ((f = fopen(fn, "r+")) != NULL) {
 510        char line[256];
 511
 512        while (fgets(line, sizeof(line), f) != NULL) {
 513            line[MD5_HEX_SIZE - 1] = '\0';
 514
 515            if (strcmp(line, md5) == 0) {
 516                /* found! just rewind, overwrite it with garbage
 517                   and an eventual call to index_gc() will clean it
 518                   [yes: this breaks index_len()] */
 519                fseek(f, -MD5_HEX_SIZE, SEEK_CUR);
 520                fwrite("-", 1, 1, f);
 521                status = HTTP_STATUS_OK;
 522
 523                break;
 524            }
 525        }
 526
 527        fclose(f);
 528    }
 529    else
 530        status = HTTP_STATUS_GONE;
 531
 532    pthread_mutex_unlock(&data_mutex);
 533
 534    return status;
 535}
 536
 537
 538int index_del(const char *fn, const char *id)
 539/* deletes an id from an index */
 540{
 541    xs *md5 = xs_md5_hex(id, strlen(id));
 542    return index_del_md5(fn, md5);
 543}
 544
 545
 546int index_gc(const char *fn)
 547/* garbage-collects an index, deleting objects that are not here */
 548{
 549    FILE *i, *o;
 550    int gc = -1;
 551
 552    pthread_mutex_lock(&data_mutex);
 553
 554    if ((i = fopen(fn, "r")) != NULL) {
 555        xs *nfn = xs_fmt("%s.new", fn);
 556        char line[256];
 557
 558        if ((o = fopen(nfn, "w")) != NULL) {
 559            gc = 0;
 560
 561            while (fgets(line, sizeof(line), i) != NULL) {
 562                line[MD5_HEX_SIZE - 1] = '\0';
 563
 564                if (line[0] != '-' && object_here_by_md5(line))
 565                    fprintf(o, "%s\n", line);
 566                else
 567                    gc++;
 568            }
 569
 570            fclose(o);
 571
 572            xs *ofn = xs_fmt("%s.bak", fn);
 573
 574            unlink(ofn);
 575            link(fn, ofn);
 576            rename(nfn, fn);
 577        }
 578
 579        fclose(i);
 580    }
 581
 582    pthread_mutex_unlock(&data_mutex);
 583
 584    return gc;
 585}
 586
 587
 588int index_in_md5(const char *fn, const char *md5)
 589/* checks if the md5 is already in the index */
 590{
 591    FILE *f;
 592    int ret = 0;
 593
 594    if ((f = fopen(fn, "r")) != NULL) {
 595        flock(fileno(f), LOCK_SH);
 596
 597        char line[256];
 598
 599        while (!ret && fgets(line, sizeof(line), f) != NULL) {
 600            line[MD5_HEX_SIZE - 1] = '\0';
 601
 602            if (strcmp(line, md5) == 0)
 603                ret = 1;
 604        }
 605
 606        fclose(f);
 607    }
 608
 609    return ret;
 610}
 611
 612
 613int index_in(const char *fn, const char *id)
 614/* checks if the object id is already in the index */
 615{
 616    xs *md5 = xs_md5_hex(id, strlen(id));
 617    return index_in_md5(fn, md5);
 618}
 619
 620
 621int index_first(const char *fn, char md5[MD5_HEX_SIZE])
 622/* reads the first entry of an index */
 623{
 624    FILE *f;
 625    int ret = 0;
 626
 627    if ((f = fopen(fn, "r")) != NULL) {
 628        if (fread(md5, MD5_HEX_SIZE, 1, f)) {
 629            md5[MD5_HEX_SIZE - 1] = '\0';
 630            ret = 1;
 631        }
 632
 633        fclose(f);
 634    }
 635
 636    return ret;
 637}
 638
 639
 640int index_len(const char *fn)
 641/* returns the number of elements in an index */
 642{
 643    struct stat st;
 644    int len = 0;
 645
 646    if (stat(fn, &st) != -1)
 647        len = st.st_size / MD5_HEX_SIZE;
 648
 649    return len;
 650}
 651
 652
 653xs_list *index_list(const char *fn, int max)
 654/* returns an index as a list */
 655{
 656    xs_list *list = xs_list_new();
 657    FILE *f;
 658    int n = 0;
 659
 660    if ((f = fopen(fn, "r")) != NULL) {
 661        flock(fileno(f), LOCK_SH);
 662
 663        char line[256];
 664
 665        while (n < max && fgets(line, sizeof(line), f) != NULL) {
 666            if (line[0] != '-') {
 667                line[MD5_HEX_SIZE - 1] = '\0';
 668                list = xs_list_append(list, line);
 669                n++;
 670            }
 671        }
 672
 673        fclose(f);
 674    }
 675
 676    return list;
 677}
 678
 679
 680int index_desc_next(FILE *f, char md5[MD5_HEX_SIZE])
 681/* reads the next entry of a desc index */
 682{
 683    for (;;) {
 684        /* move backwards 2 entries */
 685        if (fseek(f, MD5_HEX_SIZE * -2, SEEK_CUR) == -1)
 686            return 0;
 687
 688        /* read and md5 */
 689        if (!fread(md5, MD5_HEX_SIZE, 1, f))
 690            return 0;
 691
 692        if (md5[0] != '-')
 693            break;
 694    }
 695
 696    md5[MD5_HEX_SIZE - 1] = '\0';
 697
 698    return 1;
 699}
 700
 701
 702int index_desc_first(FILE *f, char md5[MD5_HEX_SIZE], int skip)
 703/* reads the first entry of a desc index */
 704{
 705    /* try to position at the end and then back to the first element */
 706    if (fseek(f, 0, SEEK_END) || fseek(f, (skip + 1) * -MD5_HEX_SIZE, SEEK_CUR))
 707        return 0;
 708
 709    /* try to read an md5 */
 710    if (!fread(md5, MD5_HEX_SIZE, 1, f))
 711        return 0;
 712
 713    /* null-terminate */
 714    md5[MD5_HEX_SIZE - 1] = '\0';
 715
 716    /* deleted? retry next */
 717    if (md5[0] == '-')
 718        return index_desc_next(f, md5);
 719
 720    return 1;
 721}
 722
 723int index_asc_first(FILE *f,char md5[MD5_HEX_SIZE], const char *seek_md5)
 724/* reads the first entry of an ascending index, starting from a given md5 */
 725{
 726    fseek(f, SEEK_SET, 0);
 727    while (fread(md5, MD5_HEX_SIZE, 1, f)) {
 728        md5[MD5_HEX_SIZE - 1] = '\0';
 729        if (strcmp(md5,seek_md5) == 0) {
 730            return index_asc_next(f, md5);
 731        }
 732    }
 733    return 0;
 734}
 735
 736int index_asc_next(FILE *f, char md5[MD5_HEX_SIZE])
 737/* reads the next entry of an ascending index */
 738{
 739    for (;;) {
 740        /* read an md5 */
 741        if (!fread(md5, MD5_HEX_SIZE, 1, f))
 742            return 0;
 743
 744        /* deleted, skip */
 745        if (md5[0] != '-')
 746            break;
 747    }
 748
 749    md5[MD5_HEX_SIZE - 1] = '\0';
 750
 751    return 1;
 752}
 753
 754
 755xs_list *index_list_desc(const char *fn, int skip, int show)
 756/* returns an index as a list, in reverse order */
 757{
 758    xs_list *list = xs_list_new();
 759    FILE *f;
 760
 761    if ((f = fopen(fn, "r")) != NULL) {
 762        char md5[MD5_HEX_SIZE];
 763
 764        if (index_desc_first(f, md5, skip)) {
 765            int n = 1;
 766
 767            do {
 768                list = xs_list_append(list, md5);
 769            } while (n++ < show && index_desc_next(f, md5));
 770        }
 771
 772        fclose(f);
 773    }
 774
 775    return list;
 776}
 777
 778
 779/** objects **/
 780
 781static xs_str *_object_fn_by_md5(const char *md5, const char *func)
 782{
 783    xs *bfn = xs_fmt("%s/object/%c%c", srv_basedir, md5[0], md5[1]);
 784    xs_str *ret;
 785    int ok = 1;
 786
 787    /* an object deleted from an index; fail but don't bark */
 788    if (md5[0] == '-')
 789        ok = 0;
 790    else
 791    if (!is_md5_hex(md5)) {
 792        srv_log(xs_fmt("_object_fn_by_md5() [from %s()]: bad md5 '%s'", func, md5));
 793        ok = 0;
 794    }
 795
 796    if (ok) {
 797        mkdirx(bfn);
 798        ret = xs_fmt("%s/%s.json", bfn, md5);
 799    }
 800    else
 801        ret = xs_fmt("%s/object/invalid/invalid.json", srv_basedir);
 802
 803    return ret;
 804}
 805
 806
 807static xs_str *_object_fn(const char *id)
 808{
 809    xs *md5 = xs_md5_hex(id, strlen(id));
 810    return _object_fn_by_md5(md5, "_object_fn");
 811}
 812
 813
 814int object_here_by_md5(const char *id)
 815/* checks if an object is already downloaded */
 816{
 817    xs *fn = _object_fn_by_md5(id, "object_here_by_md5");
 818    return mtime(fn) > 0.0;
 819}
 820
 821
 822int object_here(const char *id)
 823/* checks if an object is already downloaded */
 824{
 825    xs *fn = _object_fn(id);
 826    return mtime(fn) > 0.0;
 827}
 828
 829
 830int object_get_by_md5(const char *md5, xs_dict **obj)
 831/* returns a stored object, optionally of the requested type */
 832{
 833    int status = HTTP_STATUS_NOT_FOUND;
 834    xs *fn     = _object_fn_by_md5(md5, "object_get_by_md5");
 835    FILE *f;
 836
 837    if ((f = fopen(fn, "r")) != NULL) {
 838        *obj = xs_json_load(f);
 839        fclose(f);
 840
 841        if (*obj)
 842            status = HTTP_STATUS_OK;
 843    }
 844    else
 845        *obj = NULL;
 846
 847    return status;
 848}
 849
 850
 851int object_get(const char *id, xs_dict **obj)
 852/* returns a stored object, optionally of the requested type */
 853{
 854    xs *md5 = xs_md5_hex(id, strlen(id));
 855    return object_get_by_md5(md5, obj);
 856}
 857
 858
 859int _object_add(const char *id, const xs_dict *obj, int ow)
 860/* stores an object */
 861{
 862    int status = HTTP_STATUS_CREATED; /* Created */
 863    xs *fn     = _object_fn(id);
 864    FILE *f;
 865
 866    if (mtime(fn) > 0.0) {
 867        if (!ow) {
 868            /* object already here */
 869            srv_debug(1, xs_fmt("object_add object already here %s", id));
 870            return HTTP_STATUS_NO_CONTENT;
 871        }
 872        else
 873            status = HTTP_STATUS_OK;
 874    }
 875
 876    if ((f = fopen(fn, "w")) != NULL) {
 877        flock(fileno(f), LOCK_EX);
 878
 879        xs_json_dump(obj, 4, f);
 880        fclose(f);
 881
 882        /* does this object has a parent? */
 883        const char *in_reply_to = get_in_reply_to(obj);
 884
 885        if (!xs_is_null(in_reply_to) && *in_reply_to) {
 886            /* update the children index of the parent */
 887            xs *c_idx = _object_fn(in_reply_to);
 888
 889            c_idx = xs_replace_i(c_idx, ".json", "_c.idx");
 890
 891            if (!index_in(c_idx, id)) {
 892                index_add(c_idx, id);
 893                srv_debug(1, xs_fmt("object_add added child %s to %s", id, c_idx));
 894            }
 895            else
 896                srv_debug(1, xs_fmt("object_add %s child already in %s", id, c_idx));
 897
 898            /* create a one-element index with the parent */
 899            xs *p_idx = xs_replace(fn, ".json", "_p.idx");
 900
 901            if (mtime(p_idx) == 0.0) {
 902                index_add(p_idx, in_reply_to);
 903                srv_debug(1, xs_fmt("object_add added parent %s to %s", in_reply_to, p_idx));
 904            }
 905        }
 906    }
 907    else {
 908        srv_log(xs_fmt("object_add error writing %s (errno: %d)", fn, errno));
 909        status = HTTP_STATUS_INTERNAL_SERVER_ERROR;
 910    }
 911
 912    srv_debug(1, xs_fmt("object_add %s %s %d", id, fn, status));
 913
 914    return status;
 915}
 916
 917
 918int object_add(const char *id, const xs_dict *obj)
 919/* stores an object */
 920{
 921    return _object_add(id, obj, 0);
 922}
 923
 924
 925int object_add_ow(const char *id, const xs_dict *obj)
 926/* stores an object (overwriting allowed) */
 927{
 928    return _object_add(id, obj, 1);
 929}
 930
 931
 932int object_del_by_md5(const char *md5)
 933/* deletes an object by its md5 */
 934{
 935    int status = HTTP_STATUS_NOT_FOUND;
 936    xs *fn     = _object_fn_by_md5(md5, "object_del_by_md5");
 937
 938    if (unlink(fn) != -1) {
 939        status = HTTP_STATUS_OK;
 940
 941        /* also delete associated indexes */
 942        xs *spec  = xs_dup(fn);
 943        spec      = xs_replace_i(spec, ".json", "*.idx");
 944        xs *files = xs_glob(spec, 0, 0);
 945        char *p;
 946        const char *v;
 947
 948        p = files;
 949        while (xs_list_iter(&p, &v)) {
 950            srv_debug(1, xs_fmt("object_del index %s", v));
 951            unlink(v);
 952        }
 953    }
 954
 955    srv_debug(1, xs_fmt("object_del %s %d", fn, status));
 956
 957    return status;
 958}
 959
 960
 961int object_del(const char *id)
 962/* deletes an object */
 963{
 964    xs *md5 = xs_md5_hex(id, strlen(id));
 965    return object_del_by_md5(md5);
 966}
 967
 968
 969int object_del_if_unref(const char *id)
 970/* deletes an object if its n_links < 2 */
 971{
 972    xs *fn = _object_fn(id);
 973    int n_links;
 974    int ret = 0;
 975
 976    if (mtime_nl(fn, &n_links) > 0.0 && n_links < 2)
 977        ret = object_del(id);
 978
 979    return ret;
 980}
 981
 982
 983double object_ctime_by_md5(const char *md5)
 984{
 985    xs *fn = _object_fn_by_md5(md5, "object_ctime_by_md5");
 986    return f_ctime(fn);
 987}
 988
 989
 990double object_ctime(const char *id)
 991{
 992    xs *md5 = xs_md5_hex(id, strlen(id));
 993    return object_ctime_by_md5(md5);
 994}
 995
 996
 997double object_mtime_by_md5(const char *md5)
 998{
 999    xs *fn = _object_fn_by_md5(md5, "object_mtime_by_md5");
1000    return mtime(fn);
1001}
1002
1003
1004double object_mtime(const char *id)
1005{
1006    xs *md5 = xs_md5_hex(id, strlen(id));
1007    return object_mtime_by_md5(md5);
1008}
1009
1010
1011void object_touch(const char *id)
1012{
1013    xs *md5 = xs_md5_hex(id, strlen(id));
1014    xs *fn = _object_fn_by_md5(md5, "object_touch");
1015
1016    if (mtime(fn))
1017        utimes(fn, NULL);
1018}
1019
1020
1021xs_str *_object_index_fn(const char *id, const char *idxsfx)
1022/* returns the filename of an object's index */
1023{
1024    xs_str *fn = _object_fn(id);
1025    return xs_replace_i(fn, ".json", idxsfx);
1026}
1027
1028
1029int object_likes_len(const char *id)
1030/* returns the number of likes (without reading the index) */
1031{
1032    xs *fn = _object_index_fn(id, "_l.idx");
1033    return index_len(fn);
1034}
1035
1036
1037int object_announces_len(const char *id)
1038/* returns the number of announces (without reading the index) */
1039{
1040    xs *fn = _object_index_fn(id, "_a.idx");
1041    return index_len(fn);
1042}
1043
1044
1045xs_list *object_children(const char *id)
1046/* returns the list of an object's children */
1047{
1048    xs *fn = _object_index_fn(id, "_c.idx");
1049    return index_list(fn, XS_ALL);
1050}
1051
1052
1053xs_list *object_get_emoji_reacts(const char *id)
1054/* returns the list of an object's emoji reactions */
1055{
1056    xs *fn = _object_index_fn(id, "_e.idx");
1057    return index_list(fn, XS_ALL);
1058}
1059
1060
1061xs_list *object_likes(const char *id)
1062{
1063    xs *fn = _object_index_fn(id, "_l.idx");
1064    return index_list(fn, XS_ALL);
1065}
1066
1067
1068xs_list *object_announces(const char *id)
1069{
1070    xs *fn = _object_index_fn(id, "_a.idx");
1071    return index_list(fn, XS_ALL);
1072}
1073
1074
1075int object_parent(const char *md5, char parent[MD5_HEX_SIZE])
1076/* returns the object parent, if any */
1077{
1078    xs *fn = _object_fn_by_md5(md5, "object_parent");
1079
1080    fn = xs_replace_i(fn, ".json", "_p.idx");
1081    return index_first(fn, parent);
1082}
1083
1084
1085int object_admire(const char *id, const char *actor, int like)
1086/* actor likes or announces this object */
1087{
1088    int status = HTTP_STATUS_OK;
1089    xs *fn     = _object_fn(id);
1090
1091    fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
1092
1093    if (!index_in(fn, actor)) {
1094        status = index_add(fn, actor);
1095
1096        srv_debug(1, xs_fmt("object_admire (%s) %s %s", like ? "Like" : "Announce", actor, fn));
1097    }
1098
1099    return status;
1100}
1101
1102
1103int object_unadmire(const char *id, const char *actor, int like)
1104/* actor retrives their likes, announces or emojis this object */
1105{
1106    switch (like) {
1107    case 0:
1108        like = 'a';
1109        break;
1110    case 1:
1111        like = 'l';
1112        break;
1113    case 2:
1114        like = 'e';
1115        break;
1116    }
1117    int status;
1118    xs *fn = _object_fn(id);
1119
1120    char sfx[7] = "_x.idx";
1121    sfx[1] = like;
1122
1123    fn = xs_replace_i(fn, ".json", sfx);
1124
1125    status = index_del(fn, actor);
1126
1127    if (valid_status(status))
1128        index_gc(fn);
1129
1130    srv_debug(0,
1131        xs_fmt("object_unadmire (%s) %s %s %d", like >= 'e' ?
1132            (like == 'l' ? "Like" : "EmojiReact") : "Announce" , actor, fn, status));
1133
1134    return status;
1135}
1136
1137int object_emoji_react(const char *mid, const char *eid)
1138/* actor reacts w/ an emoji */
1139{
1140    int status = HTTP_STATUS_OK;
1141    xs *fn     = _object_fn(mid);
1142
1143    fn = xs_replace_i(fn, ".json", "_e.idx");
1144
1145    if (!index_in(fn, eid)) {
1146        status = index_add(fn, eid);
1147
1148        srv_debug(1, xs_fmt("object_emoji_react (%s) added %s to %s", "EmojiReact", eid, fn));
1149    }
1150
1151    return status;
1152}
1153
1154
1155int object_rm_emoji_react(const char *mid, const char *eid)
1156/* actor retrives their emoji reaction */
1157{
1158    int status;
1159    xs *fn = _object_fn(mid);
1160
1161    fn = xs_replace_i(fn, ".json", "_e.idx");
1162
1163    status = index_del(fn, eid);
1164    object_del(eid);
1165
1166    if (valid_status(status))
1167        index_gc(fn);
1168
1169    srv_debug(0,
1170        xs_fmt("object_unadmire (EmojiReact) %s %s %d", eid, fn, status));
1171
1172    return status;
1173}
1174
1175
1176xs_str *object_user_cache_fn_by_md5(snac *user, const char *md5, const char *cachedir)
1177{
1178    return xs_fmt("%s/%s/%s.json", user->basedir, cachedir, md5);
1179}
1180
1181
1182xs_str *object_user_cache_fn(snac *user, const char *id, const char *cachedir)
1183{
1184    xs *md5 = xs_md5_hex(id, strlen(id));
1185    return object_user_cache_fn_by_md5(user, md5, cachedir);
1186}
1187
1188
1189xs_str *object_user_cache_index_fn(snac *user, const char *cachedir)
1190{
1191    return xs_fmt("%s/%s.idx", user->basedir, cachedir);
1192}
1193
1194
1195int _object_user_cache(snac *user, const char *id, const char *cachedir, int del)
1196/* adds or deletes from a user cache */
1197{
1198    xs *ofn = _object_fn(id);
1199    xs *cfn = object_user_cache_fn(user, id, cachedir);
1200    xs *idx = object_user_cache_index_fn(user, cachedir);
1201    int ret;
1202
1203    if (del) {
1204        ret = unlink(cfn);
1205        index_del(idx, id);
1206    }
1207    else {
1208        /* create the subfolder, if it does not exist */
1209        xs *dir = xs_fmt("%s/%s/", user->basedir, cachedir);
1210        mkdirx(dir);
1211
1212        if ((ret = link(ofn, cfn)) != -1)
1213            index_add(idx, id);
1214    }
1215
1216    return ret;
1217}
1218
1219
1220int object_user_cache_add(snac *user, const char *id, const char *cachedir)
1221/* caches an object into a user cache */
1222{
1223    return _object_user_cache(user, id, cachedir, 0);
1224}
1225
1226
1227int object_user_cache_del(snac *user, const char *id, const char *cachedir)
1228/* deletes an object from a user cache */
1229{
1230    return _object_user_cache(user, id, cachedir, 1);
1231}
1232
1233
1234int object_user_cache_in(snac *user, const char *id, const char *cachedir)
1235/* checks if an object is stored in a cache */
1236{
1237    xs *cfn = object_user_cache_fn(user, id, cachedir);
1238    return !!(mtime(cfn) != 0.0);
1239}
1240
1241
1242int object_user_cache_in_by_md5(snac *user, const char *md5, const char *cachedir)
1243/* checks if an object is stored in a cache */
1244{
1245    xs *cfn = object_user_cache_fn_by_md5(user, md5, cachedir);
1246    return !!(mtime(cfn) != 0.0);
1247}
1248
1249
1250xs_list *object_user_cache_list(snac *user, const char *cachedir, int max, int inv)
1251/* returns the objects in a cache as a list */
1252{
1253    xs *idx = xs_fmt("%s/%s.idx", user->basedir, cachedir);
1254    return inv ? index_list_desc(idx, 0, max) : index_list(idx, max);
1255}
1256
1257
1258/** specialized functions **/
1259
1260/** followers **/
1261
1262int follower_add(snac *snac, const char *actor)
1263/* adds a follower */
1264{
1265    int ret = object_user_cache_add(snac, actor, "followers");
1266
1267    snac_debug(snac, 2, xs_fmt("follower_add %s", actor));
1268
1269    return ret == -1 ? HTTP_STATUS_INTERNAL_SERVER_ERROR : HTTP_STATUS_OK;
1270}
1271
1272
1273int follower_del(snac *snac, const char *actor)
1274/* deletes a follower */
1275{
1276    int ret = object_user_cache_del(snac, actor, "followers");
1277
1278    snac_debug(snac, 2, xs_fmt("follower_del %s", actor));
1279
1280    return ret == -1 ? HTTP_STATUS_NOT_FOUND : HTTP_STATUS_OK;
1281}
1282
1283
1284int follower_check(snac *snac, const char *actor)
1285/* checks if someone is a follower */
1286{
1287    return object_user_cache_in(snac, actor, "followers");
1288}
1289
1290
1291int follower_list_len(snac *snac)
1292/* returns the number of followers */
1293{
1294    xs *list = object_user_cache_list(snac, "followers", XS_ALL, 0);
1295    return xs_list_len(list);
1296}
1297
1298
1299xs_list *follower_list(snac *snac)
1300/* returns the list of followers */
1301{
1302    xs *list       = object_user_cache_list(snac, "followers", XS_ALL, 0);
1303    xs_list *fwers = xs_list_new();
1304    char *p;
1305    const char *v;
1306
1307    /* resolve the list of md5 to be a list of actors */
1308    p = list;
1309    while (xs_list_iter(&p, &v)) {
1310        xs *a_obj = NULL;
1311
1312        if (valid_status(object_get_by_md5(v, &a_obj))) {
1313            const char *actor = xs_dict_get(a_obj, "id");
1314
1315            if (!xs_is_null(actor)) {
1316                /* check if the actor is still cached */
1317                xs *fn = xs_fmt("%s/followers/%s.json", snac->basedir, v);
1318
1319                if (mtime(fn) > 0.0)
1320                    fwers = xs_list_append(fwers, actor);
1321            }
1322        }
1323    }
1324
1325    return fwers;
1326}
1327
1328
1329/** pending followers **/
1330
1331int pending_add(snac *user, const char *actor, const xs_dict *msg)
1332/* stores the follow message for later confirmation */
1333{
1334    xs *dir = xs_fmt("%s/pending", user->basedir);
1335    xs *md5 = xs_md5_hex(actor, strlen(actor));
1336    xs *fn  = xs_fmt("%s/%s.json", dir, md5);
1337    FILE *f;
1338
1339    mkdirx(dir);
1340
1341    if ((f = fopen(fn, "w")) == NULL)
1342        return -1;
1343
1344    xs_json_dump(msg, 4, f);
1345    fclose(f);
1346
1347    return 0;
1348}
1349
1350
1351int pending_check(snac *user, const char *actor)
1352/* checks if there is a pending follow confirmation for the actor */
1353{
1354    xs *md5 = xs_md5_hex(actor, strlen(actor));
1355    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1356
1357    return mtime(fn) != 0;
1358}
1359
1360
1361xs_dict *pending_get(snac *user, const char *actor)
1362/* returns the pending follow confirmation for the actor */
1363{
1364    xs *md5 = xs_md5_hex(actor, strlen(actor));
1365    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1366    xs_dict *msg = NULL;
1367    FILE *f;
1368
1369    if ((f = fopen(fn, "r")) != NULL) {
1370        msg = xs_json_load(f);
1371        fclose(f);
1372    }
1373
1374    return msg;
1375}
1376
1377
1378void pending_del(snac *user, const char *actor)
1379/* deletes a pending follow confirmation for the actor */
1380{
1381    xs *md5 = xs_md5_hex(actor, strlen(actor));
1382    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1383
1384    unlink(fn);
1385}
1386
1387
1388xs_list *pending_list(snac *user)
1389/* returns a list of pending follow confirmations */
1390{
1391    xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1392    xs *l = xs_glob(spec, 0, 0);
1393    xs_list *r = xs_list_new();
1394    const char *v;
1395
1396    xs_list_foreach(l, v) {
1397        FILE *f;
1398        xs *msg = NULL;
1399
1400        if ((f = fopen(v, "r")) == NULL)
1401            continue;
1402
1403        msg = xs_json_load(f);
1404        fclose(f);
1405
1406        if (msg == NULL)
1407            continue;
1408
1409        const char *actor = xs_dict_get(msg, "actor");
1410
1411        if (xs_type(actor) == XSTYPE_STRING)
1412            r = xs_list_append(r, actor);
1413    }
1414
1415    return r;
1416}
1417
1418
1419int pending_count(snac *user)
1420/* returns the number of pending follow confirmations */
1421{
1422    xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1423    xs *l = xs_glob(spec, 0, 0);
1424
1425    return xs_list_len(l);
1426}
1427
1428
1429int is_msg_mine(snac *user, const char *id)
1430/* returns true if a post id is by the given user */
1431{
1432    int ret = 0;
1433
1434    if (xs_is_string(id)) {
1435        xs *s1 = xs_fmt("%s/", user->actor);
1436        ret = xs_startswith(id, s1);
1437    }
1438
1439    return ret;
1440}
1441
1442
1443/** timeline **/
1444
1445double timeline_mtime(snac *snac)
1446{
1447    xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1448    return mtime(fn);
1449}
1450
1451
1452int timeline_touch(snac *snac)
1453/* changes the date of the timeline index */
1454{
1455    xs *fn = xs_fmt("%s/private.idx", snac->basedir);
1456    return utimes(fn, NULL);
1457}
1458
1459
1460xs_str *timeline_fn_by_md5(snac *snac, const char *md5)
1461/* get the filename of an entry by md5 from any timeline */
1462{
1463    xs_str *fn = NULL;
1464
1465    if (is_md5_hex(md5)) {
1466        fn = xs_fmt("%s/private/%s.json", snac->basedir, md5);
1467
1468        if (mtime(fn) == 0.0) {
1469            fn = xs_free(fn);
1470            fn = xs_fmt("%s/public/%s.json", snac->basedir, md5);
1471
1472            if (mtime(fn) == 0.0)
1473                fn = xs_free(fn);
1474        }
1475    }
1476
1477    return fn;
1478}
1479
1480
1481int timeline_here_by_md5(snac *snac, const char *md5)
1482/* checks if an object is in the user cache */
1483{
1484    xs *fn = timeline_fn_by_md5(snac, md5);
1485
1486    return !(fn == NULL);
1487}
1488
1489
1490int timeline_here(snac *user, const char *id)
1491{
1492    xs *md5 = xs_md5_hex(id, strlen(id));
1493
1494    return timeline_here_by_md5(user, md5);
1495}
1496
1497
1498int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg)
1499/* gets a message from the timeline */
1500{
1501    int status = HTTP_STATUS_NOT_FOUND;
1502    FILE *f    = NULL;
1503
1504    xs *fn = timeline_fn_by_md5(snac, md5);
1505
1506    if (fn != NULL && (f = fopen(fn, "r")) != NULL) {
1507        *msg = xs_json_load(f);
1508        fclose(f);
1509
1510        if (*msg != NULL)
1511            status = HTTP_STATUS_OK;
1512    }
1513
1514    return status;
1515}
1516
1517
1518int timeline_del(snac *snac, const char *id)
1519/* deletes a message from the timeline */
1520{
1521    /* delete from the user's caches */
1522    object_user_cache_del(snac, id, "public");
1523    object_user_cache_del(snac, id, "private");
1524
1525    unpin(snac, id);
1526    unbookmark(snac, id);
1527
1528    /* try to delete the object if it's not used elsewhere */
1529    return object_del_if_unref(id);
1530}
1531
1532
1533void timeline_update_indexes(snac *snac, const char *id)
1534/* updates the indexes */
1535{
1536    object_user_cache_add(snac, id, "private");
1537
1538    if (is_msg_mine(snac, id)) {
1539        xs *msg = NULL;
1540
1541        if (valid_status(object_get(id, &msg))) {
1542            const int scope = get_msg_visibility(msg);
1543            /* if its ours and is public, also store in public */
1544            if (scope == SCOPE_PUBLIC) {
1545                if (object_user_cache_add(snac, id, "public") >= 0) {
1546                    /* also add it to the instance public timeline */
1547                    xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
1548                    index_add(ipt, id);
1549                }
1550                else
1551                    srv_debug(1, xs_fmt("Not added to public instance index %s, visibility %d", id, scope));
1552            }
1553            else
1554                /* also add it to public, it will be discarded later */
1555                object_user_cache_add(snac, id, "public");
1556        }
1557    }
1558}
1559
1560
1561int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
1562/* adds a message to the timeline */
1563{
1564    int ret = object_add(id, o_msg);
1565    timeline_update_indexes(snac, id);
1566
1567    tag_index(id, o_msg);
1568
1569    list_distribute(snac, NULL, o_msg);
1570
1571    snac_debug(snac, 1, xs_fmt("timeline_add %s", id));
1572
1573    return ret;
1574}
1575
1576
1577int timeline_emoji_react(const char *act, const char *id, const xs_dict *msg_o)
1578/* adds an emoji reaction to a message */
1579{
1580    xs *msg = xs_dup(msg_o);
1581    msg = xs_dict_append(msg, "attributedTo", act);
1582    msg = xs_dict_set(msg, "type", "EmojiReact");
1583    const char *emote_id = xs_dict_get(msg, "id");
1584
1585    int ret = object_add(emote_id, msg);
1586    if (ret == HTTP_STATUS_OK || ret == HTTP_STATUS_CREATED)
1587        ret = object_emoji_react(id, emote_id);
1588
1589    return ret;
1590}
1591
1592
1593int timeline_admire(snac *snac, const char *id,
1594                    const char *admirer, int like, const xs_dict *msg)
1595/* updates a timeline entry with a new admiration or emoji reaction */
1596{
1597    int ret;
1598    const char *content = xs_dict_get_path(msg, "content");
1599    const char *type = xs_dict_get_path(msg, "type");
1600
1601    /* if we are admiring this, add to both timelines */
1602    if (!like && strcmp(admirer, snac->actor) == 0) {
1603        object_user_cache_add(snac, id, "public");
1604        object_user_cache_add(snac, id, "private");
1605    }
1606
1607    /* use utf <3 as a like, as it is ugly */
1608    if (type && xs_match(type, "Like|EmojiReact|Emoji") &&
1609            content && strcmp(content, "❤") != 0) {
1610        ret = timeline_emoji_react(snac->actor, id, msg);
1611        snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id));
1612    }
1613
1614    else {
1615        ret = object_admire(id, admirer, like);
1616        snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
1617                like ? "Like" : "Announce", id, admirer));
1618    }
1619
1620    return ret;
1621}
1622
1623
1624xs_list *timeline_top_level(snac *snac, const xs_list *list)
1625/* returns the top level md5 entries from this index */
1626{
1627    xs_set seen;
1628    const xs_str *v;
1629
1630    xs_set_init(&seen);
1631
1632    int c = 0;
1633    while (xs_list_next(list, &v, &c)) {
1634        char line[MD5_HEX_SIZE] = "";
1635
1636        strncpy(line, v, sizeof(line));
1637
1638        for (;;) {
1639            char line2[MD5_HEX_SIZE];
1640
1641            /* if it doesn't have a parent, use this */
1642            if (!object_parent(line, line2))
1643                break;
1644
1645            /* well, there is a parent... but is it here? */
1646            if (!timeline_here_by_md5(snac, line2))
1647                break;
1648
1649            /* it's here! try again with its own parent */
1650            strncpy(line, line2, sizeof(line));
1651        }
1652
1653        xs_set_add(&seen, line);
1654    }
1655
1656    return xs_set_result(&seen);
1657}
1658
1659
1660xs_str *user_index_fn(snac *user, const char *idx_name)
1661/* returns the filename of a user index */
1662{
1663    return xs_fmt("%s/%s.idx", user->basedir, idx_name);
1664}
1665
1666
1667xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more)
1668/* returns a timeline (with all entries) */
1669{
1670    xs *idx = user_index_fn(user, idx_name);
1671
1672    /* if a more flag is sent, request one more */
1673    xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0));
1674
1675    if (more != NULL) {
1676        if (xs_list_len(lst) > show) {
1677            *more = 1;
1678            lst = xs_list_del(lst, -1);
1679        }
1680        else
1681            *more = 0;
1682    }
1683
1684    return lst;
1685}
1686
1687
1688xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more)
1689/* returns a timeline (only top level entries) */
1690{
1691    int c_max;
1692
1693    /* maximum number of items in the timeline */
1694    c_max = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
1695
1696    /* never more timeline entries than the configured maximum */
1697    if (show > c_max)
1698        show = c_max;
1699
1700    xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
1701
1702    return timeline_top_level(snac, list);
1703}
1704
1705
1706void timeline_add_mark(snac *user)
1707/* adds an "already seen" mark to the private timeline */
1708{
1709    xs *fn = xs_fmt("%s/private.idx", user->basedir);
1710    char last_entry[MD5_HEX_SIZE] = "";
1711    FILE *f;
1712
1713    /* get the last entry in the index */
1714    if ((f = fopen(fn, "r")) != NULL) {
1715        index_desc_first(f, last_entry, 0);
1716        fclose(f);
1717    }
1718
1719    /* is the last entry *not* a mark? */
1720    if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) {
1721        /* add it */
1722        index_add_md5(fn, MD5_ALREADY_SEEN_MARK);
1723    }
1724}
1725
1726
1727xs_str *instance_index_fn(void)
1728{
1729    return xs_fmt("%s/public.idx", srv_basedir);
1730}
1731
1732
1733xs_list *timeline_instance_list(int skip, int show)
1734/* returns the timeline for the full instance */
1735{
1736    xs *idx = instance_index_fn();
1737    xs *lst = index_list_desc(idx, skip, show);
1738
1739    /* make the list unique */
1740    xs_set rep;
1741    xs_set_init(&rep);
1742    const char *md5;
1743
1744    xs_list_foreach(lst, md5)
1745        xs_set_add(&rep, md5);
1746
1747    return xs_set_result(&rep);
1748}
1749
1750
1751/** following **/
1752
1753/* this needs special treatment and cannot use the object db as is,
1754   with a link to a cached author, because we need the Follow object
1755   in case we need to unfollow (Undo + original Follow) */
1756
1757xs_str *_following_fn(snac *snac, const char *actor)
1758{
1759    xs *md5 = xs_md5_hex(actor, strlen(actor));
1760    return xs_fmt("%s/following/%s.json", snac->basedir, md5);
1761}
1762
1763
1764int following_add(snac *snac, const char *actor, const xs_dict *msg)
1765/* adds to the following list */
1766{
1767    int ret = HTTP_STATUS_CREATED;
1768    xs *fn = _following_fn(snac, actor);
1769    FILE *f;
1770    xs *p_object = NULL;
1771
1772    if (valid_status(following_get(snac, actor, &p_object))) {
1773        /* object already exists; if it's of type Accept,
1774           the actor is already being followed and confirmed,
1775           so do nothing */
1776        const char *type = xs_dict_get(p_object, "type");
1777
1778        if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1779            snac_debug(snac, 1, xs_fmt("following_add actor already confirmed %s", actor));
1780            return HTTP_STATUS_OK;
1781        }
1782    }
1783
1784    if ((f = fopen(fn, "w")) != NULL) {
1785        xs_json_dump(msg, 4, f);
1786        fclose(f);
1787
1788        /* get the filename of the actor object */
1789        xs *actor_fn = _object_fn(actor);
1790
1791        /* increase its reference count */
1792        fn = xs_replace_i(fn, ".json", "_a.json");
1793        link(actor_fn, fn);
1794    }
1795    else
1796        ret = HTTP_STATUS_INTERNAL_SERVER_ERROR;
1797
1798    snac_debug(snac, 2, xs_fmt("following_add %s %s", actor, fn));
1799
1800    return ret;
1801}
1802
1803
1804int following_del(snac *snac, const char *actor)
1805/* we're not following this actor any longer */
1806{
1807    xs *fn = _following_fn(snac, actor);
1808
1809    snac_debug(snac, 2, xs_fmt("following_del %s %s", actor, fn));
1810
1811    unlink(fn);
1812
1813    /* also delete the reference to the author */
1814    fn = xs_replace_i(fn, ".json", "_a.json");
1815    unlink(fn);
1816
1817    return HTTP_STATUS_OK;
1818}
1819
1820
1821int following_check(snac *snac, const char *actor)
1822/* checks if we are following this actor */
1823{
1824    xs *fn = _following_fn(snac, actor);
1825
1826    return !!(mtime(fn) != 0.0);
1827}
1828
1829
1830int following_get(snac *snac, const char *actor, xs_dict **data)
1831/* returns the 'Follow' object */
1832{
1833    xs *fn = _following_fn(snac, actor);
1834    FILE *f;
1835    int status = HTTP_STATUS_OK;
1836
1837    if ((f = fopen(fn, "r")) != NULL) {
1838        *data = xs_json_load(f);
1839        fclose(f);
1840    }
1841    else
1842        status = HTTP_STATUS_NOT_FOUND;
1843
1844    return status;
1845}
1846
1847
1848int following_list_len(snac *snac)
1849/* returns number of people being followed */
1850{
1851    xs *spec = xs_fmt("%s/following/" "*_a.json", snac->basedir);
1852    xs *glist = xs_glob(spec, 0, 0);
1853    return xs_list_len(glist);
1854}
1855
1856
1857xs_list *following_list(snac *snac)
1858/* returns the list of people being followed */
1859{
1860    xs *spec = xs_fmt("%s/following/" "*.json", snac->basedir);
1861    xs *glist = xs_glob(spec, 0, 0);
1862    xs_list *p;
1863    const xs_str *v;
1864    xs_list *list = xs_list_new();
1865
1866    /* iterate the list of files */
1867    p = glist;
1868    while (xs_list_iter(&p, &v)) {
1869        FILE *f;
1870
1871        /* load the follower data */
1872        if ((f = fopen(v, "r")) != NULL) {
1873            xs *o = xs_json_load(f);
1874            fclose(f);
1875
1876            if (o != NULL) {
1877                const char *type = xs_dict_get(o, "type");
1878
1879                if (!xs_is_null(type) && strcmp(type, "Accept") == 0) {
1880                    const char *actor = xs_dict_get(o, "actor");
1881
1882                    if (!xs_is_null(actor)) {
1883                        list = xs_list_append(list, actor);
1884
1885                        /* check if there is a link to the actor object */
1886                        xs *v2 = xs_replace(v, ".json", "_a.json");
1887
1888                        if (mtime(v2) == 0.0) {
1889                            /* no; add a link to it */
1890                            xs *actor_fn = _object_fn(actor);
1891                            link(actor_fn, v2);
1892                        }
1893                    }
1894                }
1895            }
1896        }
1897    }
1898
1899    return list;
1900}
1901
1902
1903xs_str *_muted_fn(snac *snac, const char *actor)
1904{
1905    xs *md5 = xs_md5_hex(actor, strlen(actor));
1906    return xs_fmt("%s/muted/%s", snac->basedir, md5);
1907}
1908
1909
1910void mute(snac *snac, const char *actor)
1911/* mutes a moron */
1912{
1913    xs *fn = _muted_fn(snac, actor);
1914    FILE *f;
1915
1916    if ((f = fopen(fn, "w")) != NULL) {
1917        fprintf(f, "%s\n", actor);
1918        fclose(f);
1919
1920        snac_debug(snac, 2, xs_fmt("muted %s %s", actor, fn));
1921    }
1922}
1923
1924
1925void unmute(snac *snac, const char *actor)
1926/* actor is no longer a moron */
1927{
1928    xs *fn = _muted_fn(snac, actor);
1929
1930    unlink(fn);
1931
1932    snac_debug(snac, 2, xs_fmt("unmuted %s %s", actor, fn));
1933}
1934
1935
1936int is_muted(snac *snac, const char *actor)
1937/* check if someone is muted */
1938{
1939    xs *fn = _muted_fn(snac, actor);
1940
1941    return !!(mtime(fn) != 0.0);
1942}
1943
1944
1945xs_list *muted_list(snac *user)
1946/* returns the list (actor URLs) of the muted morons */
1947{
1948    xs_list *l = xs_list_new();
1949    xs *spec = xs_fmt("%s/muted/" "*", user->basedir);
1950    xs *files = xs_glob(spec, 0, 0);
1951    const char *fn;
1952
1953    xs_list_foreach(files, fn) {
1954        FILE *f;
1955
1956        if ((f = fopen(fn, "r")) != NULL) {
1957            xs *actor = xs_strip_i(xs_readline(f));
1958            fclose(f);
1959
1960            l = xs_list_append(l, actor);
1961        }
1962    }
1963
1964    return l;
1965}
1966
1967/** emojis react **/
1968
1969xs_str *emoji_reacted(snac *user, const char *id)
1970/* returns the emoji an user reacted to a message */
1971{
1972    xs *emojis = object_get_emoji_reacts(id);
1973    int c = 0;
1974    const char *v;
1975
1976    while (xs_list_next(emojis, &v, &c)) {
1977        xs *msg = NULL;
1978        if (object_get_by_md5(v, &msg)) {
1979            const xs_val *act = xs_dict_get(msg, "actor");
1980            if (act && strcmp(act, user->actor) == 0)
1981                return xs_dup(xs_dict_get(msg, "content"));
1982        }
1983    }
1984    return NULL;
1985}
1986
1987/** bookmarking **/
1988
1989int is_bookmarked(snac *user, const char *id)
1990/* returns true if this note is bookmarked */
1991{
1992    return object_user_cache_in(user, id, "bookmark");
1993}
1994
1995
1996int bookmark(snac *user, const char *id)
1997/* bookmarks a post */
1998{
1999    if (is_bookmarked(user, id))
2000        return -3;
2001
2002    return object_user_cache_add(user, id, "bookmark");
2003}
2004
2005
2006int unbookmark(snac *user, const char *id)
2007/* unbookmarks a post */
2008{
2009    return object_user_cache_del(user, id, "bookmark");
2010}
2011
2012
2013xs_list *bookmark_list(snac *user)
2014/* return the lists of bookmarked posts */
2015{
2016    return object_user_cache_list(user, "bookmark", XS_ALL, 1);
2017}
2018
2019
2020xs_str *bookmark_index_fn(snac *user)
2021{
2022    return object_user_cache_index_fn(user, "bookmark");
2023}
2024
2025
2026/** pinning **/
2027
2028int is_pinned(snac *user, const char *id)
2029/* returns true if this note is pinned */
2030{
2031    return object_user_cache_in(user, id, "pinned");
2032}
2033
2034
2035int is_pinned_by_md5(snac *user, const char *md5)
2036{
2037    return object_user_cache_in_by_md5(user, md5, "pinned");
2038}
2039
2040
2041int pin(snac *user, const char *id)
2042/* pins a message */
2043{
2044    int ret = -2;
2045
2046    if (is_msg_mine(user, id)) {
2047        if (is_pinned(user, id))
2048            ret = -3;
2049        else
2050            ret = object_user_cache_add(user, id, "pinned");
2051    }
2052
2053    return ret;
2054}
2055
2056
2057int unpin(snac *user, const char *id)
2058/* unpin a message */
2059{
2060    return object_user_cache_del(user, id, "pinned");
2061}
2062
2063
2064xs_list *pinned_list(snac *user)
2065/* return the lists of pinned posts */
2066{
2067    return object_user_cache_list(user, "pinned", XS_ALL, 1);
2068}
2069
2070
2071/** drafts **/
2072
2073int is_draft(snac *user, const char *id)
2074/* returns true if this note is a draft */
2075{
2076    return object_user_cache_in(user, id, "draft");
2077}
2078
2079
2080void draft_del(snac *user, const char *id)
2081/* delete a message from the draft cache */
2082{
2083    object_user_cache_del(user, id, "draft");
2084}
2085
2086
2087void draft_add(snac *user, const char *id, const xs_dict *msg)
2088/* store the message as a draft */
2089{
2090    /* delete from the index, in case it was already there */
2091    draft_del(user, id);
2092
2093    /* overwrite object */
2094    object_add_ow(id, msg);
2095
2096    /* [re]add to the index */
2097    object_user_cache_add(user, id, "draft");
2098}
2099
2100
2101xs_list *draft_list(snac *user)
2102/* return the lists of drafts */
2103{
2104    return object_user_cache_list(user, "draft", XS_ALL, 1);
2105}
2106
2107
2108/** scheduled posts **/
2109
2110int is_scheduled(snac *user, const char *id)
2111/* returns true if this note is scheduled for future sending */
2112{
2113    return object_user_cache_in(user, id, "sched");
2114}
2115
2116
2117void schedule_del(snac *user, const char *id)
2118/* deletes an scheduled post */
2119{
2120    object_user_cache_del(user, id, "sched");
2121}
2122
2123
2124void schedule_add(snac *user, const char *id, const xs_dict *msg)
2125/* schedules this post for later */
2126{
2127    /* delete from the index, in case it was already there */
2128    schedule_del(user, id);
2129
2130    /* overwrite object */
2131    object_add_ow(id, msg);
2132
2133    /* [re]add to the index */
2134    object_user_cache_add(user, id, "sched");
2135}
2136
2137
2138xs_list *scheduled_list(snac *user)
2139/* return the list of scheduled posts */
2140{
2141    return object_user_cache_list(user, "sched", XS_ALL, 1);
2142}
2143
2144
2145void scheduled_process(snac *user)
2146/* processes the scheduled list, sending those ready to be sent */
2147{
2148    xs *posts = scheduled_list(user);
2149    const char *md5;
2150    xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
2151
2152    xs_list_foreach(posts, md5) {
2153        xs *msg = NULL;
2154
2155        if (valid_status(object_get_by_md5(md5, &msg))) {
2156            if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
2157                /* due date! */
2158                const char *id = xs_dict_get(msg, "id");
2159
2160                timeline_add(user, id, msg);
2161
2162                xs *c_msg = msg_create(user, msg);
2163                enqueue_message(user, c_msg);
2164
2165                schedule_del(user, id);
2166            }
2167        }
2168    }
2169}
2170
2171
2172/** hiding **/
2173
2174xs_str *_hidden_fn(snac *snac, const char *id)
2175{
2176    xs *md5 = xs_md5_hex(id, strlen(id));
2177    return xs_fmt("%s/hidden/%s", snac->basedir, md5);
2178}
2179
2180
2181void hide(snac *snac, const char *id)
2182/* hides an object and its children (if it's a post) */
2183{
2184    xs *fn = _hidden_fn(snac, id);
2185    FILE *f;
2186
2187    if ((f = fopen(fn, "w")) != NULL) {
2188        fprintf(f, "%s\n", id);
2189        fclose(f);
2190
2191        snac_debug(snac, 2, xs_fmt("hidden %s %s", id, fn));
2192
2193        /* hide all the children */
2194        xs *chld = object_children(id);
2195        char *p;
2196        const char *v;
2197
2198        p = chld;
2199        while (xs_list_iter(&p, &v)) {
2200            xs *co = NULL;
2201
2202            /* resolve to get the id */
2203            if (valid_status(object_get_by_md5(v, &co))) {
2204                const char *id = xs_dict_get(co, "id");
2205                if (id != NULL)
2206                    hide(snac, id);
2207            }
2208        }
2209    }
2210}
2211
2212
2213int is_hidden(snac *snac, const char *id)
2214/* check is id is hidden */
2215{
2216    xs *fn = _hidden_fn(snac, id);
2217
2218    return !!(mtime(fn) != 0.0);
2219}
2220
2221
2222int unhide(snac *user, const char *id)
2223/* unhides an object */
2224{
2225    xs *fn = _hidden_fn(user, id);
2226    return unlink(fn);
2227}
2228
2229
2230int actor_add(const char *actor, const xs_dict *msg)
2231/* adds an actor */
2232{
2233    return object_add_ow(actor, msg);
2234}
2235
2236
2237int actor_get(const char *actor, xs_dict **data)
2238/* returns an already downloaded actor */
2239{
2240    int status = HTTP_STATUS_OK;
2241    xs_dict *d = NULL;
2242
2243    if (xs_startswith(actor, srv_baseurl)) {
2244        /* it's a (possible) local user */
2245        xs *l = xs_split(actor, "/");
2246        const char *uid = xs_list_get(l, -1);
2247        snac user;
2248
2249        if (!xs_is_null(uid) && user_open(&user, uid)) {
2250            if (data)
2251                *data = msg_actor(&user);
2252
2253            user_free(&user);
2254            return HTTP_STATUS_OK;
2255        }
2256        else
2257            return HTTP_STATUS_NOT_FOUND;
2258    }
2259
2260    /* read the object */
2261    if (!valid_status(status = object_get(actor, &d))) {
2262        d = xs_free(d);
2263        return status;
2264    }
2265
2266    /* if the object is corrupted, discard it */
2267    if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
2268        srv_debug(1, xs_fmt("corrupted actor object %s", actor));
2269        d = xs_free(d);
2270        return HTTP_STATUS_NOT_FOUND;
2271    }
2272
2273    if (data)
2274        *data = d;
2275    else
2276        d = xs_free(d);
2277
2278    xs *fn = _object_fn(actor);
2279    double max_time;
2280
2281    /* maximum time for the actor data to be considered stale */
2282    max_time = 3600.0 * 36.0;
2283
2284    if (mtime(fn) + max_time < (double) time(NULL)) {
2285        /* actor data exists but also stinks */
2286        status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
2287    }
2288
2289    return status;
2290}
2291
2292
2293int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
2294/* gets an actor and requests a refresh if it's stale */
2295{
2296    int status = actor_get(actor, data);
2297
2298    if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
2299        enqueue_actor_refresh(user, actor, 0);
2300
2301    return status;
2302}
2303
2304
2305/** user limiting (announce blocks) **/
2306
2307int limited(snac *user, const char *id, int cmd)
2308/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
2309{
2310    int ret = 0;
2311    xs *dir = xs_fmt("%s/limited", user->basedir);
2312    xs *md5 = xs_md5_hex(id, strlen(id));
2313    xs *fn  = xs_fmt("%s/%s", dir, md5);
2314
2315    switch (cmd) {
2316    case 0: /** check **/
2317        ret = !!(mtime(fn) > 0.0);
2318        break;
2319
2320    case 1: /** limit **/
2321        mkdirx(dir);
2322
2323        if (mtime(fn) > 0.0)
2324            ret = -1;
2325        else {
2326            FILE *f;
2327
2328            if ((f = fopen(fn, "w")) != NULL) {
2329                fprintf(f, "%s\n", id);
2330                fclose(f);
2331            }
2332            else
2333                ret = -2;
2334        }
2335        break;
2336
2337    case 2: /** unlimit **/
2338        if (mtime(fn) > 0.0)
2339            ret = unlink(fn);
2340        else
2341            ret = -1;
2342        break;
2343    }
2344
2345    return ret;
2346}
2347
2348
2349/** tag indexing **/
2350
2351void tag_index(const char *id, const xs_dict *obj)
2352/* update the tag indexes for this object */
2353{
2354    const xs_list *tags = xs_dict_get(obj, "tag");
2355    xs *md5_id = xs_md5_hex(id, strlen(id));
2356
2357    if (get_msg_visibility(obj) != SCOPE_PUBLIC)
2358        return;
2359
2360    if (xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
2361        xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
2362
2363        mkdirx(g_tag_dir);
2364
2365        const xs_dict *v;
2366        int ct = 0;
2367        while (xs_list_next(tags, &v, &ct)) {
2368            const char *type = xs_dict_get(v, "type");
2369            const char *name = xs_dict_get(v, "name");
2370
2371            if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
2372                while (*name == '#' || *name == '@')
2373                    name++;
2374
2375                if (*name == '\0')
2376                    continue;
2377
2378                name = xs_utf8_to_lower((xs_str *)name);
2379
2380                xs *md5_tag   = xs_md5_hex(name, strlen(name));
2381                xs *tag_dir   = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
2382                mkdirx(tag_dir);
2383
2384                xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
2385
2386                if (!index_in_md5(g_tag_idx, md5_id))
2387                    index_add_md5(g_tag_idx, md5_id);
2388
2389                FILE *f;
2390                xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
2391                if ((f = fopen(g_tag_name, "w")) != NULL) {
2392                    fprintf(f, "%s\n", name);
2393                    fclose(f);
2394                }
2395
2396                srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
2397            }
2398        }
2399    }
2400}
2401
2402
2403xs_str *tag_fn(const char *tag)
2404{
2405    if (*tag == '#')
2406        tag++;
2407
2408    xs *lw_tag = xs_utf8_to_lower(tag);
2409    xs *md5    = xs_md5_hex(lw_tag, strlen(lw_tag));
2410
2411    return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
2412}
2413
2414
2415xs_list *tag_search(const char *tag, int skip, int show)
2416/* returns the list of posts tagged with tag */
2417{
2418    xs *idx = tag_fn(tag);
2419
2420    return index_list_desc(idx, skip, show);
2421}
2422
2423
2424/** lists **/
2425
2426xs_val *list_maint(snac *user, const char *list, int op)
2427/* list maintenance */
2428{
2429    xs_val *l = NULL;
2430
2431    switch (op) {
2432    case 0: /** list of lists **/
2433        {
2434            FILE *f;
2435            xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
2436            xs *ls   = xs_glob(spec, 0, 0);
2437            int c = 0;
2438            const char *v;
2439
2440            l = xs_list_new();
2441
2442            while (xs_list_next(ls, &v, &c)) {
2443                if ((f = fopen(v, "r")) != NULL) {
2444                    xs *title = xs_readline(f);
2445                    fclose(f);
2446
2447                    title = xs_strip_i(title);
2448
2449                    xs *v2 = xs_replace(v, ".id", "");
2450                    xs *l2 = xs_split(v2, "/");
2451
2452                    /* return [ list_id, list_title ] */
2453                    xs *tmp_list = xs_list_append(xs_list_new(), xs_list_get(l2, -1), title);
2454                    l = xs_list_append(l, tmp_list);
2455                }
2456            }
2457        }
2458
2459        break;
2460
2461    case 1: /** create new list (list is the name) **/
2462        {
2463            xs *lol = list_maint(user, NULL, 0);
2464            int c = 0;
2465            const xs_list *v;
2466            int add = 1;
2467
2468            /* check if this list name already exists */
2469            while (xs_list_next(lol, &v, &c)) {
2470                if (strcmp(xs_list_get(v, 1), list) == 0) {
2471                    add = 0;
2472
2473                    l = xs_dup(xs_list_get(v, 0));
2474
2475                    break;
2476                }
2477            }
2478
2479            if (add) {
2480                FILE *f;
2481                xs *dir = xs_fmt("%s/list/", user->basedir);
2482
2483                l = xs_fmt("%010x", time(NULL));
2484
2485                mkdirx(dir);
2486
2487                xs *fn = xs_fmt("%s%s.id", dir, l);
2488
2489                if ((f = fopen(fn, "w")) != NULL) {
2490                    fprintf(f, "%s\n", list);
2491                    fclose(f);
2492                }
2493            }
2494        }
2495
2496        break;
2497
2498    case 2: /** delete list (list is the id) **/
2499        {
2500            if (xs_is_hex(list)) {
2501                xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2502                unlink(fn);
2503
2504                fn = xs_replace_i(fn, ".id", ".lst");
2505                unlink(fn);
2506
2507                fn = xs_replace_i(fn, ".lst", ".idx");
2508                unlink(fn);
2509
2510                fn = xs_str_cat(fn, ".bak");
2511                unlink(fn);
2512            }
2513        }
2514
2515        break;
2516
2517    case 3: /** get list name **/
2518        if (xs_is_hex(list)) {
2519            FILE *f;
2520            xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2521
2522            if ((f = fopen(fn, "r")) != NULL) {
2523                l = xs_strip_i(xs_readline(f));
2524                fclose(f);
2525            }
2526        }
2527
2528        break;
2529
2530    case 4: /** find list id by name **/
2531        if (xs_is_string(list)) {
2532            xs *lol = list_maint(user, NULL, 0);
2533            const xs_list *li;
2534
2535            xs_list_foreach(lol, li) {
2536                if (strcmp(list, xs_list_get(li, 1)) == 0) {
2537                    l = xs_dup(xs_list_get(li, 0));
2538                    break;
2539                }
2540            }
2541        }
2542    }
2543
2544    return l;
2545}
2546
2547
2548xs_str *list_timeline_fn(snac *user, const char *list)
2549{
2550    return xs_fmt("%s/list/%s.idx", user->basedir, list);
2551}
2552
2553
2554xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2555/* returns the timeline of a list */
2556{
2557    xs_list *l = NULL;
2558
2559    if (!xs_is_hex(list))
2560        return NULL;
2561
2562    xs *fn = list_timeline_fn(user, list);
2563
2564    if (mtime(fn) > 0.0)
2565        l = index_list_desc(fn, skip, show);
2566    else
2567        l = xs_list_new();
2568
2569    return l;
2570}
2571
2572
2573xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
2574/* list member management */
2575{
2576    xs_val *l = NULL;
2577
2578    if (!xs_is_hex(list))
2579        return NULL;
2580
2581    if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2582        return NULL;
2583
2584    xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2585
2586    switch (op) {
2587    case 0: /** list members **/
2588        l = index_list(fn, XS_ALL);
2589
2590        break;
2591
2592    case 1: /** append actor to list **/
2593        if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) {
2594            if (!index_in_md5(fn, actor_md5))
2595                index_add_md5(fn, actor_md5);
2596        }
2597
2598        break;
2599
2600    case 2: /** delete actor from list **/
2601        if (xs_is_string(actor_md5) && xs_is_hex(actor_md5))
2602            index_del_md5(fn, actor_md5);
2603
2604        break;
2605
2606    default:
2607        srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2608        break;
2609    }
2610
2611    return l;
2612}
2613
2614
2615void list_distribute(snac *user, const char *who, const xs_dict *post)
2616/* distributes the post to all appropriate lists */
2617{
2618    const char *id = xs_dict_get(post, "id");
2619
2620    /* if who is not set, use the attributedTo in the message */
2621    if (xs_is_null(who))
2622        who = get_atto(post);
2623
2624    if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2625        xs *a_md5 = xs_md5_hex(who, strlen(who));
2626        xs *i_md5 = xs_md5_hex(id, strlen(id));
2627        xs *spec  = xs_fmt("%s/list/" "*.lst", user->basedir);
2628        xs *ls    = xs_glob(spec, 0, 0);
2629        int c = 0;
2630        const char *v;
2631
2632        while (xs_list_next(ls, &v, &c)) {
2633            /* is the actor in this list? */
2634            if (index_in_md5(v, a_md5)) {
2635                /* it is; add post md5 to its timeline */
2636                xs *idx = xs_replace(v, ".lst", ".idx");
2637                index_add_md5(idx, i_md5);
2638
2639                snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2640            }
2641        }
2642    }
2643}
2644
2645
2646/** static data **/
2647
2648static int _load_raw_file(const char *fn, xs_val **data, int *size,
2649                        const char *inm, xs_str **etag)
2650/* loads a cached file */
2651{
2652    int status = HTTP_STATUS_NOT_FOUND;
2653
2654    if (fn) {
2655        double tm = mtime(fn);
2656
2657        if (tm > 0.0) {
2658            /* file exists; build the etag */
2659            xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2660
2661            /* if if-none-match is set, check if it's the same */
2662            if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2663                /* client has the newest version */
2664                status = HTTP_STATUS_NOT_MODIFIED;
2665            }
2666            else {
2667                /* newer or never downloaded; read the full file */
2668                FILE *f;
2669
2670                if ((f = fopen(fn, "rb")) != NULL) {
2671                    *size = XS_ALL;
2672                    *data = xs_read(f, size);
2673                    fclose(f);
2674
2675                    status = HTTP_STATUS_OK;
2676                }
2677            }
2678
2679            /* if caller wants the etag, return it */
2680            if (etag != NULL)
2681                *etag = xs_dup(e);
2682
2683            srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2684        }
2685    }
2686
2687    return status;
2688}
2689
2690
2691xs_str *_static_fn(snac *snac, const char *id)
2692/* gets the filename for a static file */
2693{
2694    if (strchr(id, '/'))
2695        return NULL;
2696    else
2697        return xs_fmt("%s/static/%s", snac->basedir, id);
2698}
2699
2700
2701int static_get(snac *snac, const char *id, xs_val **data, int *size,
2702                const char *inm, xs_str **etag)
2703/* returns static content */
2704{
2705    xs *fn = _static_fn(snac, id);
2706
2707    return _load_raw_file(fn, data, size, inm, etag);
2708}
2709
2710
2711void static_put(snac *snac, const char *id, const char *data, int size)
2712/* writes status content */
2713{
2714    xs *fn = _static_fn(snac, id);
2715    FILE *f;
2716
2717    if (fn && (f = fopen(fn, "wb")) != NULL) {
2718        fwrite(data, size, 1, f);
2719        fclose(f);
2720
2721        strip_media(fn);
2722    }
2723}
2724
2725
2726void static_put_meta(snac *snac, const char *id, const char *str)
2727/* puts metadata (i.e. a media description string) to id */
2728{
2729    xs *fn = _static_fn(snac, id);
2730
2731    if (fn) {
2732        fn = xs_str_cat(fn, ".txt");
2733        FILE *f;
2734
2735        if ((f = fopen(fn, "w")) != NULL) {
2736            fprintf(f, "%s\n", str);
2737            fclose(f);
2738        }
2739    }
2740}
2741
2742
2743xs_str *static_get_meta(snac *snac, const char *id)
2744/* gets metadata from a media */
2745{
2746    xs *fn    = _static_fn(snac, id);
2747    xs_str *r = NULL;
2748
2749    if (fn) {
2750        fn = xs_str_cat(fn, ".txt");
2751        FILE *f;
2752
2753        if ((f = fopen(fn, "r")) != NULL) {
2754            r = xs_strip_i(xs_readline(f));
2755            fclose(f);
2756        }
2757    }
2758    else
2759        r = xs_str_new("");
2760
2761    return r;
2762}
2763
2764
2765/** history **/
2766
2767xs_str *_history_fn(snac *snac, const char *id)
2768/* gets the filename for the history */
2769{
2770    if (strchr(id, '/'))
2771        return NULL;
2772    else
2773        return xs_fmt("%s/history/%s", snac->basedir, id);
2774}
2775
2776
2777double history_mtime(snac *snac, const char *id)
2778{
2779    double t = 0.0;
2780    xs *fn = _history_fn(snac, id);
2781
2782    if (fn != NULL)
2783        t = mtime(fn);
2784
2785    return t;
2786}
2787
2788
2789void history_add(snac *snac, const char *id, const char *content, int size,
2790                    xs_str **etag)
2791/* adds something to the history */
2792{
2793    xs *fn = _history_fn(snac, id);
2794    FILE *f;
2795
2796    if (fn && (f = fopen(fn, "w")) != NULL) {
2797        fwrite(content, size, 1, f);
2798        fclose(f);
2799
2800        if (etag) {
2801            double tm = mtime(fn);
2802            *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2803        }
2804    }
2805}
2806
2807
2808int history_get(snac *snac, const char *id, xs_str **content, int *size,
2809                const char *inm, xs_str **etag)
2810{
2811    xs *fn = _history_fn(snac, id);
2812
2813    return _load_raw_file(fn, content, size, inm, etag);
2814}
2815
2816
2817int history_del(snac *snac, const char *id)
2818{
2819    xs *fn = _history_fn(snac, id);
2820
2821    if (fn)
2822        return unlink(fn);
2823    else
2824        return -1;
2825}
2826
2827
2828xs_list *history_list(snac *snac)
2829{
2830    xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2831
2832    return xs_glob(spec, 1, 1);
2833}
2834
2835
2836void lastlog_write(snac *snac, const char *source)
2837/* writes the last time the user logged in */
2838{
2839    xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2840    FILE *f;
2841
2842    if ((f = fopen(fn, "w")) != NULL) {
2843        fprintf(f, "%lf %s\n", ftime(), source);
2844        fclose(f);
2845    }
2846}
2847
2848
2849/** inbox collection **/
2850
2851void inbox_add(const char *inbox)
2852/* collects a shared inbox */
2853{
2854    /* don't collect ourselves */
2855    if (xs_startswith(inbox, srv_baseurl))
2856        return;
2857
2858    xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2859    xs *fn  = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2860    FILE *f;
2861
2862    if ((f = fopen(fn, "w")) != NULL) {
2863        fprintf(f, "%s\n", inbox);
2864        fclose(f);
2865    }
2866}
2867
2868
2869void inbox_add_by_actor(const xs_dict *actor)
2870/* collects an actor's shared inbox, if it has one */
2871{
2872    const char *v;
2873
2874    if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2875        !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2876        /* only collect this inbox if its instance is not blocked */
2877        if (!is_instance_blocked(v))
2878            inbox_add(v);
2879    }
2880}
2881
2882
2883xs_list *inbox_list(void)
2884/* returns the collected inboxes as a list */
2885{
2886    xs_list *ibl = xs_list_new();
2887    xs *spec     = xs_fmt("%s/inbox/" "*", srv_basedir);
2888    xs *files    = xs_glob(spec, 0, 0);
2889    const xs_val *v;
2890
2891    xs_list_foreach(files, v) {
2892        FILE *f;
2893
2894        if ((f = fopen(v, "r")) != NULL) {
2895            xs *line = xs_readline(f);
2896
2897            if (line && *line) {
2898                line = xs_strip_i(line);
2899
2900                if (!is_instance_blocked(line))
2901                    ibl = xs_list_append(ibl, line);
2902            }
2903
2904            fclose(f);
2905        }
2906    }
2907
2908    return ibl;
2909}
2910
2911
2912/** instance-wide operations **/
2913
2914xs_str *_instance_block_fn(const char *instance)
2915{
2916    xs *s   = xs_replace(instance, "http:/" "/", "");
2917    xs *s1  = xs_replace(s, "https:/" "/", "");
2918    xs *l   = xs_split(s1, "/");
2919    const char *p = xs_list_get(l, 0);
2920    xs *md5 = xs_md5_hex(p, strlen(p));
2921
2922    return xs_fmt("%s/block/%s", srv_basedir, md5);
2923}
2924
2925
2926int is_instance_blocked(const char *instance)
2927{
2928    xs *fn = _instance_block_fn(instance);
2929
2930    return !!(mtime(fn) != 0.0);
2931}
2932
2933
2934int instance_block(const char *instance)
2935/* blocks a full instance */
2936{
2937    int ret;
2938
2939    /* create the subdir */
2940    xs *dir = xs_fmt("%s/block/", srv_basedir);
2941    mkdirx(dir);
2942
2943    if (!is_instance_blocked(instance)) {
2944        xs *fn = _instance_block_fn(instance);
2945        FILE *f;
2946
2947        if ((f = fopen(fn, "w")) != NULL) {
2948            fprintf(f, "%s\n", instance);
2949            fclose(f);
2950
2951            ret = 0;
2952        }
2953        else
2954            ret = -1;
2955    }
2956    else
2957        ret = -2;
2958
2959    return ret;
2960}
2961
2962
2963int instance_unblock(const char *instance)
2964/* unblocks a full instance */
2965{
2966    int ret;
2967
2968    if (is_instance_blocked(instance)) {
2969        xs *fn = _instance_block_fn(instance);
2970        ret = unlink(fn);
2971    }
2972    else
2973        ret = -2;
2974
2975    return ret;
2976}
2977
2978
2979/** operations by content **/
2980
2981int content_match(const char *file, const xs_dict *msg)
2982/* checks if a message's content matches any of the regexes in file */
2983/* file format: one regex per line */
2984{
2985    xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2986    FILE *f;
2987    int r = 0;
2988    const char *v = xs_dict_get(msg, "content");
2989
2990    if (xs_type(v) == XSTYPE_STRING && *v) {
2991        if ((f = fopen(fn, "r")) != NULL) {
2992            srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2993
2994            /* massage content (strip HTML tags, etc.) */
2995            xs *c1 = xs_regex_replace(v, "<[^>]+>", " ");
2996            c1 = xs_regex_replace_i(c1, " {2,}", " ");
2997            xs *c = xs_utf8_to_lower(c1);
2998
2999            while (!r && !feof(f)) {
3000                xs *rx = xs_strip_i(xs_readline(f));
3001
3002                if (*rx && xs_regex_match(c, rx)) {
3003                    srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
3004                    r = 1;
3005                }
3006            }
3007
3008            fclose(f);
3009        }
3010    }
3011
3012    return r;
3013}
3014
3015
3016xs_list *content_search(snac *user, const char *regex,
3017                int priv, int skip, int show, int max_secs, int *timeout)
3018/* returns a list of posts which content matches the regex */
3019{
3020    if (regex == NULL || *regex == '\0')
3021        return xs_list_new();
3022
3023    xs *i_regex = xs_utf8_to_lower(regex);
3024
3025    xs_set seen;
3026
3027    xs_set_init(&seen);
3028
3029    if (max_secs == 0)
3030        max_secs = 3;
3031
3032    time_t t = time(NULL) + max_secs;
3033    *timeout = 0;
3034
3035    /* iterate all timelines simultaneously */
3036    xs_list *tls[3] = {0};
3037    const char *md5s[3] = {0};
3038    int c[3] = {0};
3039
3040    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL);   /* public */
3041    tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
3042    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
3043
3044    /* first positioning */
3045    for (int n = 0; n < 3; n++)
3046        xs_list_next(tls[n], &md5s[n], &c[n]);
3047
3048    show += skip;
3049
3050    while (show > 0) {
3051        /* timeout? */
3052        if (time(NULL) > t) {
3053            *timeout = 1;
3054            break;
3055        }
3056
3057        /* find the newest post */
3058        int newest = -1;
3059        double mtime = 0.0;
3060
3061        for (int n = 0; n < 3; n++) {
3062            if (md5s[n] != NULL) {
3063                xs *fn = _object_fn_by_md5(md5s[n], "content_search");
3064                double mt;
3065
3066                while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
3067                    /* object is not here: move to the next one */
3068                    if (xs_list_next(tls[n], &md5s[n], &c[n])) {
3069                        xs_free(fn);
3070                        fn = _object_fn_by_md5(md5s[n], "content_search_2");
3071                    }
3072                    else
3073                        md5s[n] = NULL;
3074                }
3075
3076                if (mt > mtime) {
3077                    newest = n;
3078                    mtime = mt;
3079                }
3080            }
3081        }
3082
3083        if (newest == -1)
3084            break;
3085
3086        const char *md5 = md5s[newest];
3087
3088        /* advance the chosen timeline */
3089        if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
3090            md5s[newest] = NULL;
3091
3092        xs *post = NULL;
3093
3094        if (!valid_status(object_get_by_md5(md5, &post)))
3095            continue;
3096
3097        if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
3098            continue;
3099
3100        const char *id = xs_dict_get(post, "id");
3101
3102        if (id == NULL || is_hidden(user, id))
3103            continue;
3104
3105        /* recalculate the md5 id to be sure it's not repeated
3106           (it may have been searched by the "url" field instead of "id") */
3107        xs *new_md5 = xs_md5_hex(id, strlen(id));
3108        md5 = new_md5;
3109
3110        /* test for the post URL */
3111        if (strcmp(id, regex) == 0) {
3112            if (xs_set_add(&seen, md5) == 1)
3113                show--;
3114
3115            continue;
3116        }
3117
3118        /* test for the alternate post id */
3119        const char *url = xs_dict_get(post, "url");
3120        if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
3121            if (xs_set_add(&seen, md5) == 1)
3122                show--;
3123
3124            continue;
3125        }
3126
3127        xs *c = xs_str_new(NULL);
3128        const char *content = xs_dict_get(post, "content");
3129        const char *name    = xs_dict_get(post, "name");
3130        const char *atto    = get_atto(post);
3131
3132        if (!xs_is_null(content))
3133            c = xs_str_cat(c, content);
3134        if (!xs_is_null(name))
3135            c = xs_str_cat(c, " ", name);
3136        if (!xs_is_null(atto))
3137            c = xs_str_cat(c, " ", atto);
3138
3139        /* add alt-texts from attachments */
3140        const xs_list *atts = xs_dict_get(post, "attachment");
3141        int tc = 0;
3142        const xs_dict *att;
3143
3144        while (xs_list_next(atts, &att, &tc)) {
3145            const char *name = xs_dict_get(att, "name");
3146
3147            if (name != NULL)
3148                c = xs_str_cat(c, " ", name);
3149        }
3150
3151        /* strip HTML */
3152        c = xs_regex_replace_i(c, "<[^>]+>", " ");
3153        c = xs_regex_replace_i(c, " {2,}", " ");
3154
3155        /* convert to lowercase */
3156        xs *lc = xs_utf8_to_lower(c);
3157
3158        /* apply regex */
3159        if (xs_regex_match(lc, i_regex)) {
3160            if (xs_set_add(&seen, md5) == 1)
3161            show--;
3162        }
3163    }
3164
3165    xs_list *r = xs_set_result(&seen);
3166
3167    if (skip) {
3168        /* BAD */
3169        while (skip--) {
3170            r = xs_list_del(r, 0);
3171        }
3172    }
3173
3174    xs_free(tls[0]);
3175    xs_free(tls[1]);
3176    xs_free(tls[2]);
3177
3178    return r;
3179}
3180
3181
3182int actor_failure(const char *actor, int op)
3183/* actor failure maintenance */
3184{
3185    int ret = 0;
3186
3187    xs *md5 = xs_md5_hex(actor, strlen(actor));
3188    xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3189
3190    switch (op) {
3191    case 0: /** check **/
3192        if (mtime(fn))
3193            ret = -1;
3194
3195        break;
3196
3197    case 1: /** register a failure **/
3198        if (mtime(fn) == 0.0) {
3199            FILE *f;
3200
3201            /* only create once, as the date will be used */
3202            if ((f = fopen(fn, "w")) != NULL) {
3203                fprintf(f, "%s\n", actor);
3204                fclose(f);
3205            }
3206        }
3207
3208        break;
3209
3210    case 2: /** clear a failure **/
3211        /* called whenever a message comes from this instance */
3212        unlink(fn);
3213
3214        break;
3215    }
3216
3217    return ret;
3218}
3219
3220
3221int instance_failure(const char *url, int op)
3222/* do some checks and accounting on instance failures */
3223{
3224    int ret = 0;
3225    xs *l = xs_split(url, "/");
3226    const char *hostname = xs_list_get(l, 2);
3227    double mt;
3228
3229    if (!xs_is_string(hostname))
3230        return 0;
3231
3232    xs *md5 = xs_md5_hex(hostname, strlen(hostname));
3233    xs *fn = xs_fmt("%s/failure/%s", srv_basedir, md5);
3234
3235    switch (op) {
3236    case 0: /** check **/
3237        if ((mt = mtime(fn)) != 0.0) {
3238            /* grace time */
3239            double seconds_failing = xs_number_get(xs_dict_get_def(srv_config, "max_failing_days", "15"))
3240                * (24 * 60 * 60);
3241
3242            if ((double)time(NULL) - mt > seconds_failing)
3243                ret = -1;
3244        }
3245
3246        break;
3247
3248    case 1: /** register a failure **/
3249        if (mtime(fn) == 0.0) {
3250            FILE *f;
3251
3252            /* only create once, as the date will be used */
3253            if ((f = fopen(fn, "w")) != NULL) {
3254                fprintf(f, "%s\n", hostname);
3255                fclose(f);
3256            }
3257        }
3258
3259        break;
3260
3261    case 2: /** clear a failure **/
3262        /* called whenever a message comes from this instance */
3263        unlink(fn);
3264
3265        break;
3266    }
3267
3268    return ret;
3269}
3270
3271
3272int grave(const char *objid, int op)
3273/* the graveyeard of deleted objects */
3274{
3275    int ret = 0;
3276    xs *dir = xs_fmt("%s/grave", srv_basedir);
3277    xs *md5 = xs_md5_hex(objid, strlen(objid));
3278    xs *fn  = xs_fmt("%s/%s", dir, md5);
3279    FILE *f;
3280
3281    switch (op) {
3282    case 0: /** check **/
3283        ret = mtime(fn) > 0.0 ? 1 : 0;
3284        break;
3285
3286    case 1: /** add **/
3287        mkdirx(dir);
3288
3289        if ((f = fopen(fn, "w")) != NULL) {
3290            fprintf(f, "%s\n", objid);
3291            fclose(f);
3292        }
3293
3294        break;
3295    }
3296
3297    return ret;
3298}
3299
3300
3301/** notifications **/
3302
3303xs_str *notify_check_time(snac *snac, int reset)
3304/* gets or resets the latest notification check time */
3305{
3306    xs_str *t = NULL;
3307    xs *fn    = xs_fmt("%s/notifydate.txt", snac->basedir);
3308    FILE *f;
3309
3310    if (reset) {
3311        if ((f = fopen(fn, "w")) != NULL) {
3312            t = tid(0);
3313            fprintf(f, "%s\n", t);
3314            fclose(f);
3315        }
3316    }
3317    else {
3318        if ((f = fopen(fn, "r")) != NULL) {
3319            t = xs_readline(f);
3320            fclose(f);
3321        }
3322        else
3323            /* never set before */
3324            t = xs_fmt("%16.6f", 0.0);
3325    }
3326
3327    return t;
3328}
3329
3330xs_dict *markers_get(snac *snac, const xs_list *markers)
3331{
3332    xs *data = NULL;
3333    xs_dict *returns = xs_dict_new();
3334    xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3335    const xs_str *v = NULL;
3336    FILE *f;
3337
3338    if ((f = fopen(fn, "r")) != NULL) {
3339        data = xs_json_load(f);
3340        fclose(f);
3341    }
3342
3343    if (xs_is_null(data))
3344        data = xs_dict_new();
3345
3346    xs_list_foreach(markers, v) {
3347        const xs_dict *mark = xs_dict_get(data, v);
3348        if (!xs_is_null(mark)) {
3349            returns = xs_dict_append(returns, v, mark);
3350        }
3351    }
3352    return returns;
3353}
3354
3355xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
3356/* gets or sets notification marker */
3357{
3358    xs *data = NULL;
3359    xs_dict *written = xs_dict_new();
3360    xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3361    FILE *f;
3362
3363    if ((f = fopen(fn, "r")) != NULL) {
3364        data = xs_json_load(f);
3365        fclose(f);
3366    }
3367
3368    if (xs_is_null(data))
3369        data = xs_dict_new();
3370
3371    if (!xs_is_null(home_marker)) {
3372        xs *home = xs_dict_new();
3373        xs *s_tid = tid(0);
3374        home = xs_dict_append(home, "last_read_id", home_marker);
3375        home = xs_dict_append(home, "version", xs_stock(0));
3376        home = xs_dict_append(home, "updated_at", s_tid);
3377        data = xs_dict_set(data, "home", home);
3378        written = xs_dict_append(written, "home", home);
3379    }
3380
3381    if (!xs_is_null(notify_marker)) {
3382        xs *notify = xs_dict_new();
3383        xs *s_tid = tid(0);
3384        notify = xs_dict_append(notify, "last_read_id", notify_marker);
3385        notify = xs_dict_append(notify, "version", xs_stock(0));
3386        notify = xs_dict_append(notify, "updated_at", s_tid);
3387        data = xs_dict_set(data, "notifications", notify);
3388        written = xs_dict_append(written, "notifications", notify);
3389    }
3390
3391    if ((f = fopen(fn, "w")) != NULL) {
3392        xs_json_dump(data, 4, f);
3393        fclose(f);
3394    }
3395
3396    return written;
3397}
3398
3399void notify_add(snac *snac, const char *type, const char *utype,
3400                const char *actor, const char *objid, const xs_dict *msg)
3401/* adds a new notification */
3402{
3403    xs *ntid = tid(0);
3404    xs *fn   = xs_fmt("%s/notify/", snac->basedir);
3405    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3406    FILE *f;
3407
3408    /* create the directory */
3409    mkdirx(fn);
3410    fn = xs_str_cat(fn, ntid);
3411    fn = xs_str_cat(fn, ".json");
3412
3413    xs *noti = xs_dict_new();
3414
3415    noti = xs_dict_append(noti, "id",    ntid);
3416    noti = xs_dict_append(noti, "type",  type);
3417    noti = xs_dict_append(noti, "utype", utype);
3418    noti = xs_dict_append(noti, "actor", actor);
3419    noti = xs_dict_append(noti, "date",  date);
3420    noti = xs_dict_append(noti, "msg",   msg);
3421
3422    if (!xs_is_null(objid))
3423        noti = xs_dict_append(noti, "objid", objid);
3424
3425    if ((f = fopen(fn, "w")) != NULL) {
3426        xs_json_dump(noti, 4, f);
3427        fclose(f);
3428    }
3429
3430    /* add it to the index if it already exists */
3431    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3432
3433    if (mtime(idx) != 0.0) {
3434        pthread_mutex_lock(&data_mutex);
3435
3436        if ((f = fopen(idx, "a")) != NULL) {
3437            fprintf(f, "%-32s\n", ntid);
3438            fclose(f);
3439        }
3440
3441        pthread_mutex_unlock(&data_mutex);
3442    }
3443
3444    if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook")))
3445        enqueue_notify_webhook(snac, noti, 0);
3446}
3447
3448
3449xs_dict *notify_get(snac *snac, const char *id)
3450/* gets a notification */
3451{
3452    /* base file */
3453    xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
3454
3455    /* strip spaces and add extension */
3456    fn = xs_strip_i(fn);
3457    fn = xs_str_cat(fn, ".json");
3458
3459    FILE *f;
3460    xs_dict *out = NULL;
3461
3462    if ((f = fopen(fn, "r")) != NULL) {
3463        out = xs_json_load(f);
3464        fclose(f);
3465    }
3466
3467    return out;
3468}
3469
3470
3471xs_list *notify_list(snac *snac, int skip, int show)
3472/* returns a list of notification ids */
3473{
3474    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3475
3476    if (mtime(idx) == 0.0) {
3477        /* create the index from scratch */
3478        FILE *f;
3479
3480        pthread_mutex_lock(&data_mutex);
3481
3482        if ((f = fopen(idx, "w")) != NULL) {
3483            xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
3484            xs *lst  = xs_glob(spec, 1, 0);
3485            xs_list *p = lst;
3486            const char *v;
3487
3488            while (xs_list_iter(&p, &v)) {
3489                char *p = strrchr(v, '.');
3490                if (p) {
3491                    *p = '\0';
3492                    fprintf(f, "%-32s\n", v);
3493                }
3494            }
3495
3496            fclose(f);
3497        }
3498
3499        pthread_mutex_unlock(&data_mutex);
3500    }
3501
3502    return index_list_desc(idx, skip, show);
3503}
3504
3505
3506int notify_new_num(snac *snac)
3507/* counts the number of new notifications */
3508{
3509    xs *t = notify_check_time(snac, 0);
3510    xs *lst = notify_list(snac, 0, XS_ALL);
3511    int cnt = 0;
3512
3513    xs_list *p = lst;
3514    const xs_str *v;
3515
3516    while (xs_list_iter(&p, &v)) {
3517        xs *id = xs_strip_i(xs_dup(v));
3518
3519        /* old? count no more */
3520        if (strcmp(id, t) < 0)
3521            break;
3522
3523        cnt++;
3524    }
3525
3526    return cnt;
3527}
3528
3529
3530void notify_clear(snac *snac)
3531/* clears all notifications */
3532{
3533    xs *spec   = xs_fmt("%s/notify/" "*", snac->basedir);
3534    xs *lst    = xs_glob(spec, 0, 0);
3535    xs_list *p = lst;
3536    const xs_str *v;
3537
3538    while (xs_list_iter(&p, &v))
3539        unlink(v);
3540
3541    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3542
3543    if (mtime(idx) != 0.0) {
3544        pthread_mutex_lock(&data_mutex);
3545        truncate(idx, 0);
3546        pthread_mutex_unlock(&data_mutex);
3547    }
3548}
3549
3550
3551/** the queue **/
3552
3553static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
3554/* writes safely to the queue */
3555{
3556    xs *tfn = xs_fmt("%s.tmp", fn);
3557    FILE *f;
3558
3559    if ((f = fopen(tfn, "w")) != NULL) {
3560        xs_json_dump(msg, 4, f);
3561        fclose(f);
3562
3563        rename(tfn, fn);
3564    }
3565
3566    return msg;
3567}
3568
3569
3570static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
3571/* creates a queue message */
3572{
3573    int qrt  = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
3574    xs *ntid = tid(retries * 60 * qrt);
3575    xs *rn   = xs_number_new(retries);
3576
3577    xs_dict *qmsg = xs_dict_new();
3578
3579    qmsg = xs_dict_append(qmsg, "type",    type);
3580    qmsg = xs_dict_append(qmsg, "message", msg);
3581    qmsg = xs_dict_append(qmsg, "retries", rn);
3582    qmsg = xs_dict_append(qmsg, "ntid",    ntid);
3583
3584    return qmsg;
3585}
3586
3587
3588void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3589/* enqueues an input message */
3590{
3591    xs *qmsg   = _new_qmsg("input", msg, retries);
3592    const char *ntid = xs_dict_get(qmsg, "ntid");
3593    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3594
3595    qmsg = xs_dict_append(qmsg, "req", req);
3596
3597    qmsg = _enqueue_put(fn, qmsg);
3598
3599    snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3600}
3601
3602
3603void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3604/* enqueues an input message from the shared input */
3605{
3606    xs *qmsg   = _new_qmsg("input", msg, retries);
3607    const char *ntid = xs_dict_get(qmsg, "ntid");
3608    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3609
3610    qmsg = xs_dict_append(qmsg, "req", req);
3611
3612    qmsg = _enqueue_put(fn, qmsg);
3613
3614    srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3615}
3616
3617
3618void enqueue_output_raw(const char *keyid, const char *seckey,
3619                        const xs_dict *msg, const xs_str *inbox,
3620                        int retries, int p_status)
3621/* enqueues an output message to an inbox */
3622{
3623    xs *qmsg   = _new_qmsg("output", msg, retries);
3624    const char *ntid = xs_dict_get(qmsg, "ntid");
3625    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3626
3627    xs *ns = xs_number_new(p_status);
3628    qmsg = xs_dict_append(qmsg, "p_status", ns);
3629
3630    qmsg = xs_dict_append(qmsg, "inbox",  inbox);
3631    qmsg = xs_dict_append(qmsg, "keyid",  keyid);
3632    qmsg = xs_dict_append(qmsg, "seckey", seckey);
3633
3634    /* if it's to be sent right now, bypass the disk queue and post the job */
3635    if (retries == 0 && p_state != NULL)
3636        job_post(qmsg, 0);
3637    else {
3638        qmsg = _enqueue_put(fn, qmsg);
3639        srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3640    }
3641}
3642
3643
3644void enqueue_output(snac *snac, const xs_dict *msg,
3645                    const xs_str *inbox, int retries, int p_status)
3646/* enqueues an output message to an inbox */
3647{
3648    if (is_msg_mine(snac, inbox)) {
3649        snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3650        return;
3651    }
3652
3653    const char *seckey = xs_dict_get(snac->key, "secret");
3654
3655    enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3656}
3657
3658
3659void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3660                            const xs_str *actor, int retries)
3661/* enqueues an output message for an actor */
3662{
3663    xs *inbox = get_actor_inbox(actor, 1);
3664
3665    if (!xs_is_null(inbox))
3666        enqueue_output(snac, msg, inbox, retries, 0);
3667    else
3668        snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3669}
3670
3671
3672void enqueue_email(const xs_dict *msg, int retries)
3673/* enqueues an email message to be sent */
3674{
3675    xs *qmsg   = _new_qmsg("email", msg, retries);
3676    const char *ntid = xs_dict_get(qmsg, "ntid");
3677    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3678
3679    qmsg = _enqueue_put(fn, qmsg);
3680
3681    srv_debug(1, xs_fmt("enqueue_email %d", retries));
3682}
3683
3684
3685void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3686/* enqueues a message to be sent via Telegram */
3687{
3688    xs *qmsg   = _new_qmsg("telegram", msg, 0);
3689    const char *ntid = xs_dict_get(qmsg, "ntid");
3690    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3691
3692    qmsg = xs_dict_append(qmsg, "bot",      bot);
3693    qmsg = xs_dict_append(qmsg, "chat_id",  chat_id);
3694
3695    qmsg = _enqueue_put(fn, qmsg);
3696
3697    srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3698}
3699
3700void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3701/* enqueues a message to be sent via ntfy */
3702{
3703    xs *qmsg   = _new_qmsg("ntfy", msg, 0);
3704    const char *ntid = xs_dict_get(qmsg, "ntid");
3705    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3706
3707    qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3708    qmsg = xs_dict_append(qmsg, "ntfy_token",  ntfy_token);
3709
3710
3711    qmsg = _enqueue_put(fn, qmsg);
3712
3713    srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3714}
3715
3716void enqueue_message(snac *snac, const xs_dict *msg)
3717/* enqueues an output message */
3718{
3719    xs *qmsg   = _new_qmsg("message", msg, 0);
3720    const char *ntid = xs_dict_get(qmsg, "ntid");
3721    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3722
3723    qmsg = _enqueue_put(fn, qmsg);
3724
3725    snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3726}
3727
3728
3729void enqueue_close_question(snac *user, const char *id, int end_secs)
3730/* enqueues the closing of a question */
3731{
3732    xs *qmsg = _new_qmsg("close_question", id, 0);
3733    xs *ntid = tid(end_secs);
3734    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3735
3736    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3737
3738    qmsg = _enqueue_put(fn, qmsg);
3739
3740    snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3741}
3742
3743
3744void enqueue_object_request(snac *user, const char *id, int forward_secs)
3745/* enqueues the request of an object in the future */
3746{
3747    xs *qmsg = _new_qmsg("object_request", id, 0);
3748    xs *ntid = tid(forward_secs);
3749    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3750
3751    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3752
3753    qmsg = _enqueue_put(fn, qmsg);
3754
3755    snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3756}
3757
3758
3759void enqueue_verify_links(snac *user)
3760/* enqueues a link verification */
3761{
3762    xs *qmsg   = _new_qmsg("verify_links", "", 0);
3763    const char *ntid = xs_dict_get(qmsg, "ntid");
3764    xs *fn     = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3765
3766    qmsg = _enqueue_put(fn, qmsg);
3767
3768    snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3769}
3770
3771
3772void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3773/* enqueues an actor refresh */
3774{
3775    xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3776    xs *ntid = tid(forward_secs);
3777    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3778
3779    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3780    qmsg = xs_dict_append(qmsg, "actor", actor);
3781
3782    qmsg = _enqueue_put(fn, qmsg);
3783
3784    snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3785}
3786
3787
3788void enqueue_webmention(const xs_dict *msg)
3789/* enqueues a webmention for the post */
3790{
3791    xs *qmsg   = _new_qmsg("webmention", msg, 0);
3792    const char *ntid = xs_dict_get(qmsg, "ntid");
3793    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3794
3795    qmsg = _enqueue_put(fn, qmsg);
3796
3797    srv_debug(1, xs_fmt("enqueue_webmention"));
3798}
3799
3800
3801void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries)
3802/* enqueues a notification webhook */
3803{
3804    const char *webhook = xs_dict_get(user->config, "notify_webhook");
3805
3806    if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3807        xs *msg = xs_dup(noti);
3808
3809        /* add more data */
3810        msg = xs_dict_set(msg, "target", user->actor);
3811        msg = xs_dict_set(msg, "uid", user->uid);
3812        msg = xs_dict_set(msg, "basedir", srv_basedir);
3813        msg = xs_dict_set(msg, "baseurl", srv_baseurl);
3814
3815        xs *actor_obj = NULL;
3816
3817        if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj)
3818            msg = xs_dict_set(msg, "account", actor_obj);
3819
3820        /* if this post is a reply, also add the inReplyTo object */
3821        const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo");
3822
3823        if (xs_is_string(in_reply_to)) {
3824            xs *irt_obj = NULL;
3825
3826            if (valid_status(object_get(in_reply_to, &irt_obj)))
3827                msg = xs_dict_set(msg, "reply", irt_obj);
3828        }
3829
3830        xs *qmsg = _new_qmsg("notify_webhook", msg, retries);
3831        const char *ntid = xs_dict_get(qmsg, "ntid");
3832        xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3833
3834        qmsg = _enqueue_put(fn, qmsg);
3835
3836        snac_debug(user, 1, xs_fmt("notify_webhook"));
3837    }
3838}
3839
3840
3841void enqueue_collect_replies(snac *user, const char *post)
3842/* enqueues a collect replies request */
3843{
3844    xs *qmsg = _new_qmsg("collect_replies", post, 0);
3845    const char *ntid = xs_dict_get(qmsg, "ntid");
3846    xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3847
3848    qmsg = _enqueue_put(fn, qmsg);
3849
3850    snac_debug(user, 1, xs_fmt("enqueue_collect_replies %s", post));
3851}
3852
3853
3854void enqueue_collect_outbox(snac *user, const char *actor_id)
3855/* enqueues a collect outbox request */
3856{
3857    xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
3858    const char *ntid = xs_dict_get(qmsg, "ntid");
3859    xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3860
3861    qmsg = _enqueue_put(fn, qmsg);
3862
3863    snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
3864}
3865
3866
3867void enqueue_fsck(void)
3868/* enqueues an fsck */
3869{
3870    xs *qmsg   = _new_qmsg("fsck", "", 0);
3871    const char *ntid = xs_dict_get(qmsg, "ntid");
3872    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3873
3874    qmsg = _enqueue_put(fn, qmsg);
3875}
3876
3877
3878int was_question_voted(snac *user, const char *id)
3879/* returns true if the user voted in this poll */
3880{
3881    xs *children = object_children(id);
3882    int voted = 0;
3883    xs_list *p;
3884    const xs_str *md5;
3885
3886    p = children;
3887    while (xs_list_iter(&p, &md5)) {
3888        xs *obj = NULL;
3889
3890        if (valid_status(object_get_by_md5(md5, &obj))) {
3891            const char *atto = get_atto(obj);
3892            if (atto && strcmp(atto, user->actor) == 0 &&
3893                !xs_is_null(xs_dict_get(obj, "name"))) {
3894                voted = 1;
3895                break;
3896            }
3897        }
3898    }
3899
3900    return voted;
3901}
3902
3903
3904xs_list *user_queue(snac *snac)
3905/* returns a list with filenames that can be dequeued */
3906{
3907    xs *spec      = xs_fmt("%s/queue/" "*.json", snac->basedir);
3908    xs_list *list = xs_list_new();
3909    time_t t      = time(NULL);
3910    xs_list *p;
3911    const xs_val *v;
3912
3913    xs *fns = xs_glob(spec, 0, 0);
3914
3915    p = fns;
3916    while (xs_list_iter(&p, &v)) {
3917        /* get the retry time from the basename */
3918        char *bn  = strrchr(v, '/');
3919        time_t t2 = atol(bn + 1);
3920
3921        if (t2 > t)
3922            snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3923        else {
3924            list = xs_list_append(list, v);
3925            snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3926        }
3927    }
3928
3929    return list;
3930}
3931
3932
3933xs_list *queue(void)
3934/* returns a list with filenames that can be dequeued */
3935{
3936    xs *spec      = xs_fmt("%s/queue/" "*.json", srv_basedir);
3937    xs_list *list = xs_list_new();
3938    time_t t      = time(NULL);
3939    xs_list *p;
3940    const xs_val *v;
3941
3942    xs *fns = xs_glob(spec, 0, 0);
3943
3944    p = fns;
3945    while (xs_list_iter(&p, &v)) {
3946        /* get the retry time from the basename */
3947        char *bn  = strrchr(v, '/');
3948        time_t t2 = atol(bn + 1);
3949
3950        if (t2 > t)
3951            srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3952        else {
3953            list = xs_list_append(list, v);
3954            srv_debug(2, xs_fmt("queue ready for %s", v));
3955        }
3956    }
3957
3958    return list;
3959}
3960
3961
3962xs_dict *queue_get(const char *fn)
3963/* gets a file from a queue */
3964{
3965    FILE *f;
3966    xs_dict *obj = NULL;
3967
3968    if ((f = fopen(fn, "r")) != NULL) {
3969        obj = xs_json_load(f);
3970        fclose(f);
3971    }
3972
3973    return obj;
3974}
3975
3976
3977xs_dict *dequeue(const char *fn)
3978/* dequeues a message */
3979{
3980    xs_dict *obj = queue_get(fn);
3981
3982    unlink(fn);
3983
3984    return obj;
3985}
3986
3987
3988/** the purge **/
3989
3990static int _purge_file(const char *fn, time_t mt)
3991/* purge fn if it's older than days */
3992{
3993    int ret = 0;
3994
3995    if (mtime(fn) < mt) {
3996        /* older than the minimum time: delete it */
3997        unlink(fn);
3998        srv_debug(2, xs_fmt("purged %s", fn));
3999        ret = 1;
4000    }
4001
4002    return ret;
4003}
4004
4005
4006static void _purge_dir(const char *dir, int days)
4007/* purges all files in a directory older than days */
4008{
4009    int cnt = 0;
4010
4011    if (days) {
4012        time_t mt = time(NULL) - days * 24 * 3600;
4013        xs *spec  = xs_fmt("%s/" "*", dir);
4014        xs *list  = xs_glob(spec, 0, 0);
4015        xs_list *p;
4016        const xs_str *v;
4017
4018        p = list;
4019        while (xs_list_iter(&p, &v))
4020            cnt += _purge_file(v, mt);
4021
4022        srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
4023    }
4024}
4025
4026
4027static void _purge_user_subdir(snac *snac, const char *subdir, int days)
4028/* purges all files in a user subdir older than days */
4029{
4030    xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
4031
4032    _purge_dir(u_subdir, days);
4033}
4034
4035
4036void purge_server(void)
4037/* purge global server data */
4038{
4039    xs *spec = xs_fmt("%s/object/??", srv_basedir);
4040    xs *dirs = xs_glob(spec, 0, 0);
4041    xs_list *p;
4042    const xs_str *v;
4043    int cnt = 0;
4044    int icnt = 0;
4045
4046    time_t mt = time(NULL) - 7 * 24 * 3600;
4047
4048    p = dirs;
4049    while (xs_list_iter(&p, &v)) {
4050        xs_list *p2;
4051        const xs_str *v2;
4052
4053        {
4054            xs *spec2 = xs_fmt("%s/" "*.json", v);
4055            xs *files = xs_glob(spec2, 0, 0);
4056
4057            p2 = files;
4058            while (xs_list_iter(&p2, &v2)) {
4059                int n_link;
4060
4061                /* old and with no hard links? */
4062                if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
4063                    xs *s1    = xs_replace(v2, ".json", "");
4064                    xs *l     = xs_split(s1, "/");
4065                    const char *md5 = xs_list_get(l, -1);
4066
4067                    object_del_by_md5(md5);
4068                    cnt++;
4069                }
4070            }
4071        }
4072
4073        {
4074            /* look for stray indexes */
4075            xs *speci = xs_fmt("%s/" "*_?.idx", v);
4076            xs *idxfs = xs_glob(speci, 0, 0);
4077
4078            p2 = idxfs;
4079            while (xs_list_iter(&p2, &v2)) {
4080                /* old enough to consider? */
4081                if (mtime(v2) < mt) {
4082                    /* check if the indexed object is here */
4083                    xs *o = xs_dup(v2);
4084                    char *ext = strchr(o, '_');
4085
4086                    if (ext) {
4087                        *ext = '\0';
4088                        o = xs_str_cat(o, ".json");
4089
4090                        if (mtime(o) == 0.0) {
4091                            /* delete */
4092                            unlink(v2);
4093                            srv_debug(1, xs_fmt("purged %s", v2));
4094                            icnt++;
4095                        }
4096                    }
4097                }
4098            }
4099
4100            /* delete index backups */
4101            xs *specb = xs_fmt("%s/" "*.bak", v);
4102            xs *bakfs = xs_glob(specb, 0, 0);
4103
4104            p2 = bakfs;
4105            while (xs_list_iter(&p2, &v2)) {
4106                unlink(v2);
4107                srv_debug(1, xs_fmt("purged %s", v2));
4108            }
4109        }
4110    }
4111
4112    /* purge collected inboxes */
4113    xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
4114    _purge_dir(ib_dir, 7);
4115
4116    /* purge the instance timeline */
4117    xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
4118    int itl_gc = index_gc(itl_fn);
4119
4120    /* purge tag indexes */
4121    xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
4122    xs *tag_dirs = xs_glob(tag_spec, 0, 0);
4123    p = tag_dirs;
4124
4125    int tag_gc = 0;
4126    while (xs_list_iter(&p, &v)) {
4127        xs *spec2 = xs_fmt("%s/" "*.idx", v);
4128        xs *files = xs_glob(spec2, 0, 0);
4129        xs_list *p2;
4130        const xs_str *v2;
4131
4132        p2 = files;
4133        while (xs_list_iter(&p2, &v2)) {
4134            tag_gc += index_gc(v2);
4135            xs *bak = xs_fmt("%s.bak", v2);
4136            unlink(bak);
4137
4138            if (index_len(v2) == 0) {
4139                /* there are no longer any entry with this tag;
4140                   purge it completely */
4141                unlink(v2);
4142                xs *dottag = xs_replace(v2, ".idx", ".tag");
4143                unlink(dottag);
4144            }
4145        }
4146    }
4147
4148    srv_debug(1, xs_fmt("purge: global "
4149            "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
4150}
4151
4152
4153void delete_purged_posts(snac *user, int days)
4154/* enqueues Delete activities for local purged messages */
4155{
4156    if (days == 0)
4157        return;
4158
4159    time_t mt = time(NULL) - days * 24 * 3600;
4160    xs *spec  = xs_fmt("%s/public/" "*.json", user->basedir);
4161    xs *list  = xs_glob(spec, 0, 0);
4162    const char *v;
4163
4164    xs_list_foreach(list, v) {
4165        if (mtime(v) < mt) {
4166            /* to be purged; is it a Note by us? */
4167            FILE *f;
4168
4169            if ((f = fopen(v, "r")) != NULL) {
4170                xs *msg = xs_json_load(f);
4171                fclose(f);
4172
4173                if (xs_is_dict(msg)) {
4174                    const char *id = xs_dict_get(msg, "id");
4175
4176                    if (xs_is_string(id) && is_msg_mine(user, id)) {
4177                        xs *d_msg = msg_delete(user, id);
4178
4179                        enqueue_message(user, d_msg);
4180
4181                        snac_log(user, xs_fmt("enqueued Delete for purged message %s", id));
4182                    }
4183                }
4184            }
4185        }
4186    }
4187}
4188
4189
4190void purge_user(snac *snac)
4191/* do the purge for this user */
4192{
4193    int priv_days, pub_days, user_days = 0;
4194    const char *v;
4195    int n;
4196
4197    priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
4198    pub_days  = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
4199
4200    if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
4201        (v = xs_dict_get(snac->config, "purge_days")) != NULL)
4202        user_days = xs_number_get(v);
4203
4204    if (user_days) {
4205        /* override admin settings only if they are lesser */
4206        if (priv_days == 0 || user_days < priv_days)
4207            priv_days = user_days;
4208
4209        if (pub_days == 0 || user_days < pub_days)
4210            pub_days = user_days;
4211    }
4212
4213    if (xs_is_true(xs_dict_get(srv_config, "propagate_local_purge")))
4214        delete_purged_posts(snac, pub_days);
4215
4216    _purge_user_subdir(snac, "hidden",  priv_days);
4217    _purge_user_subdir(snac, "private", priv_days);
4218
4219    _purge_user_subdir(snac, "public",  pub_days);
4220
4221    const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
4222                           "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
4223
4224    for (n = 0; idxs[n]; n++) {
4225        xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
4226        int gc = index_gc(idx);
4227        srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
4228    }
4229
4230    /* purge lists */
4231    {
4232        xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
4233        xs *lol  = xs_glob(spec, 0, 0);
4234        int c = 0;
4235        const char *v;
4236
4237        while (xs_list_next(lol, &v, &c)) {
4238            int gc = index_gc(v);
4239            srv_debug(1, xs_fmt("purge: %s %d", v, gc));
4240        }
4241    }
4242
4243    /* unrelated to purging, but it's a janitorial process, so what the hell */
4244    verify_links(snac);
4245}
4246
4247
4248void purge_all(void)
4249/* purge all users */
4250{
4251    snac snac;
4252    xs *list = user_list();
4253    char *p;
4254    const char *uid;
4255
4256    p = list;
4257    while (xs_list_iter(&p, &uid)) {
4258        if (user_open(&snac, uid)) {
4259            purge_user(&snac);
4260            user_free(&snac);
4261        }
4262    }
4263
4264    purge_server();
4265
4266#ifndef NO_MASTODON_API
4267    mastoapi_purge();
4268#endif
4269}
4270
4271
4272/** archive **/
4273
4274void srv_archive(const char *direction, const char *url, xs_dict *req,
4275                 const char *payload, int p_size,
4276                 int status, xs_dict *headers,
4277                 const char *body, int b_size)
4278/* archives a connection */
4279{
4280    /* obsessive archiving */
4281    xs *date = tid(0);
4282    xs *dir  = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
4283    FILE *f;
4284
4285    if (mkdirx(dir) != -1) {
4286        xs *meta_fn = xs_fmt("%s/_META", dir);
4287
4288        if ((f = fopen(meta_fn, "w")) != NULL) {
4289            xs *j1 = xs_json_dumps(req, 4);
4290            xs *j2 = xs_json_dumps(headers, 4);
4291
4292            fprintf(f, "dir: %s\n", direction);
4293
4294            if (url)
4295                fprintf(f, "url: %s\n", url);
4296
4297            fprintf(f, "req: %s\n", j1);
4298            fprintf(f, "p_size: %d\n", p_size);
4299            fprintf(f, "status: %d\n", status);
4300            fprintf(f, "response: %s\n", j2);
4301            fprintf(f, "b_size: %d\n", b_size);
4302            fclose(f);
4303        }
4304
4305        if (p_size && payload) {
4306            xs *payload_fn = NULL;
4307            xs *payload_fn_raw = NULL;
4308            const char *v = xs_dict_get(req, "content-type");
4309
4310            if (v && xs_str_in(v, "json") != -1) {
4311                payload_fn = xs_fmt("%s/payload.json", dir);
4312
4313                if ((f = fopen(payload_fn, "w")) != NULL) {
4314                    xs *v1 = xs_json_loads(payload);
4315                    xs *j1 = NULL;
4316
4317                    if (v1 != NULL)
4318                        j1 = xs_json_dumps(v1, 4);
4319
4320                    if (j1 != NULL)
4321                        fwrite(j1, strlen(j1), 1, f);
4322                    else
4323                        fwrite(payload, p_size, 1, f);
4324
4325                    fclose(f);
4326                }
4327            }
4328
4329            payload_fn_raw = xs_fmt("%s/payload", dir);
4330
4331            if ((f = fopen(payload_fn_raw, "w")) != NULL) {
4332                fwrite(payload, p_size, 1, f);
4333                fclose(f);
4334            }
4335        }
4336
4337        if (b_size && body) {
4338            xs *body_fn = NULL;
4339            const char *v = xs_dict_get(headers, "content-type");
4340
4341            if (v && xs_str_in(v, "json") != -1) {
4342                body_fn = xs_fmt("%s/body.json", dir);
4343
4344                if ((f = fopen(body_fn, "w")) != NULL) {
4345                    xs *v1 = xs_json_loads(body);
4346                    xs *j1 = NULL;
4347
4348                    if (v1 != NULL)
4349                        j1 = xs_json_dumps(v1, 4);
4350
4351                    if (j1 != NULL)
4352                        fwrite(j1, strlen(j1), 1, f);
4353                    else
4354                        fwrite(body, b_size, 1, f);
4355
4356                    fclose(f);
4357                }
4358            }
4359            else {
4360                body_fn = xs_fmt("%s/body", dir);
4361
4362                if ((f = fopen(body_fn, "w")) != NULL) {
4363                    fwrite(body, b_size, 1, f);
4364                    fclose(f);
4365                }
4366            }
4367        }
4368    }
4369}
4370
4371
4372void srv_archive_error(const char *prefix, const xs_str *err,
4373                       const xs_dict *req, const xs_val *data)
4374/* archives an error */
4375{
4376    xs *ntid = tid(0);
4377    xs *fn   = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
4378    FILE *f;
4379
4380    if ((f = fopen(fn, "w")) != NULL) {
4381        fprintf(f, "Error: %s\n", err);
4382
4383        if (req) {
4384            fprintf(f, "Request headers:\n");
4385
4386            xs_json_dump(req, 4, f);
4387
4388            fprintf(f, "\n");
4389        }
4390
4391        if (data) {
4392            fprintf(f, "Data:\n");
4393
4394            if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
4395                xs_json_dump(data, 4, f);
4396            }
4397            else
4398                fprintf(f, "%s", data);
4399
4400            fprintf(f, "\n");
4401        }
4402
4403        fclose(f);
4404    }
4405}
4406
4407
4408void srv_archive_qitem(const char *prefix, xs_dict *q_item)
4409/* archives a q_item in the error folder */
4410{
4411    xs *ntid = tid(0);
4412    xs *fn   = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
4413    FILE *f;
4414
4415    if ((f = fopen(fn, "w")) != NULL) {
4416        xs_json_dump(q_item, 4, f);
4417        fclose(f);
4418    }
4419}
4420
4421
4422t_announcement *announcement(const double after)
4423/* returns announcement text or NULL if none exists or it is olde than "after" */
4424{
4425    static const long int MAX_SIZE = 2048;
4426    static t_announcement a = {
4427        .text = NULL,
4428        .timestamp = 0.0,
4429    };
4430    static xs_str *fn = NULL;
4431    if (fn == NULL)
4432        fn = xs_fmt("%s/announcement.txt", srv_basedir);
4433
4434    const double ts = mtime(fn);
4435
4436    /* file does not exist or other than what was requested */
4437    if (ts == 0.0 || ts <= after)
4438        return NULL;
4439
4440    /* nothing changed, just return the current announcement */
4441    if (a.text != NULL && ts <= a.timestamp)
4442        return &a;
4443
4444    /* read and store new announcement */
4445    FILE *f;
4446
4447    if ((f = fopen(fn, "r")) != NULL) {
4448        fseek (f, 0, SEEK_END);
4449        const long int length = ftell(f);
4450
4451        if (length > MAX_SIZE) {
4452            /* this is probably unintentional */
4453            srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
4454        }
4455        else
4456        if (length > 0) {
4457            fseek (f, 0, SEEK_SET);
4458            char *buffer = malloc(length + 1);
4459            if (buffer) {
4460                fread(buffer, 1, length, f);
4461                buffer[length] = '\0';
4462
4463                free(a.text);
4464                a.text = buffer;
4465                a.timestamp = ts;
4466            }
4467            else {
4468                srv_log("Error allocating memory for announcement");
4469            }
4470        }
4471        else {
4472            /* an empty file means no announcement */
4473            free(a.text);
4474            a.text = NULL;
4475            a.timestamp = 0.0;
4476        }
4477
4478        fclose (f);
4479    }
4480
4481    if (a.text != NULL)
4482        return &a;
4483
4484    return NULL;
4485}
4486
4487
4488xs_str *make_url(const char *href, const char *proxy, int by_token)
4489/* makes an URL, possibly including proxying */
4490{
4491    xs_str *url = NULL;
4492
4493    if (proxy && !xs_startswith(href, srv_baseurl)) {
4494        xs *p = NULL;
4495
4496        if (by_token) {
4497            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
4498            xs *tk = xs_md5_hex(tks, strlen(tks));
4499
4500            p = xs_fmt("%s/y/%s/", proxy, tk);
4501        }
4502        else
4503            p = xs_fmt("%s/x/", proxy);
4504
4505        url = xs_replace(href, "https:/" "/", p);
4506    }
4507    else
4508        url = xs_dup(href);
4509
4510    return url;
4511}
4512
4513
4514/** bad login throttle **/
4515
4516xs_str *_badlogin_fn(const char *addr)
4517{
4518    xs *md5 = xs_md5_hex(addr, strlen(addr));
4519    xs *dir = xs_fmt("%s/badlogin", srv_basedir);
4520
4521    mkdirx(dir);
4522
4523    return xs_fmt("%s/%s", dir, md5);
4524}
4525
4526
4527int _badlogin_read(const char *fn, int *failures)
4528/* reads a badlogin file */
4529{
4530    int ok = 0;
4531    FILE *f;
4532
4533    pthread_mutex_lock(&data_mutex);
4534
4535    if ((f = fopen(fn, "r")) != NULL) {
4536        xs *l = xs_readline(f);
4537        fclose(f);
4538
4539        if (sscanf(l, "%d", failures) == 1)
4540            ok = 1;
4541    }
4542
4543    pthread_mutex_unlock(&data_mutex);
4544
4545    return ok;
4546}
4547
4548
4549int badlogin_check(const char *user, const char *addr)
4550/* checks if this address is authorized to try a login */
4551{
4552    int valid = 1;
4553
4554    if (xs_type(addr) == XSTYPE_STRING) {
4555        xs *fn = _badlogin_fn(addr);
4556        double mt = mtime(fn);
4557
4558        if (mt > 0) {
4559            int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
4560                                        "badlogin_expire", "300"));
4561
4562            mt += badlogin_expire;
4563
4564            /* if file is expired, delete and give pass */
4565            if (mt < time(NULL)) {
4566                srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
4567                unlink(fn);
4568            }
4569            else {
4570                int failures;
4571
4572                if (_badlogin_read(fn, &failures)) {
4573                    int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
4574                                            "badlogin_retries", "5"));
4575
4576                    if (failures >= badlogin_max) {
4577                        valid = 0;
4578
4579                        xs *d = xs_str_iso_date((time_t) mt);
4580
4581                        srv_debug(1,
4582                            xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
4583                    }
4584                }
4585            }
4586        }
4587    }
4588
4589    return valid;
4590}
4591
4592
4593void badlogin_inc(const char *user, const char *addr)
4594/* increments a bad login from this address */
4595{
4596    if (xs_type(addr) == XSTYPE_STRING) {
4597        int failures = 0;
4598        xs *fn = _badlogin_fn(addr);
4599        FILE *f;
4600
4601        _badlogin_read(fn, &failures);
4602
4603        pthread_mutex_lock(&data_mutex);
4604
4605        if ((f = fopen(fn, "w")) != NULL) {
4606            failures++;
4607
4608            fprintf(f, "%d %s %s\n", failures, addr, user);
4609            fclose(f);
4610
4611            srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
4612        }
4613
4614        pthread_mutex_unlock(&data_mutex);
4615    }
4616}
4617
4618
4619/** language strings **/
4620
4621const char *lang_str(const char *str, const snac *user)
4622/* returns a translated string */
4623{
4624    const char *n_str = str;
4625
4626    if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4627        n_str = xs_dict_get(user->lang, str);
4628
4629        if (xs_is_null(n_str) || *n_str == '\0')
4630            n_str = str;
4631    }
4632
4633    return n_str;
4634}
4635
4636
4637/** integrity checks **/
4638
4639void data_fsck(void)
4640{
4641    xs *list = user_list();
4642    const char *uid;
4643
4644    xs_list_foreach(list, uid) {
4645        snac user;
4646
4647        if (!user_open(&user, uid))
4648            continue;
4649
4650        {
4651            /* iterate all private posts and check that non-public posts
4652               from this user are also linked into the public directory,
4653               to avoid the don't-fucking-delete-my-own-private-posts purge bug */
4654
4655            xs *priv_spec = xs_fmt("%s/private/""*.json", user.basedir);
4656            xs *posts = xs_glob(priv_spec, 0, 0);
4657            const char *priv_fn;
4658
4659            xs_list_foreach(posts, priv_fn) {
4660                xs *pub_fn = xs_replace(priv_fn, "/private/", "/public/");
4661
4662                /* already there? look no more */
4663                if (mtime(pub_fn))
4664                    continue;
4665
4666                /* read the post */
4667                FILE *f;
4668                if ((f = fopen(priv_fn, "r")) == NULL)
4669                    continue;
4670
4671                xs *post = xs_json_load(f);
4672                fclose(f);
4673
4674                if (!xs_is_dict(post))
4675                    continue;
4676
4677                const char *attr_to = get_atto(post);
4678
4679                if (!xs_is_string(attr_to) || strcmp(attr_to, user.actor) != 0) {
4680                    /* not from this user */
4681                    continue;
4682                }
4683
4684                /* link */
4685                snac_debug(&user, 1, xs_fmt("fsck: fixed missing link %s", xs_dict_get(post, "id")));
4686                link(priv_fn, pub_fn);
4687            }
4688        }
4689
4690        user_free(&user);
4691    }
4692}