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