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