2.79 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 a message tree */
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 actor_add(const char *actor, const xs_dict *msg)
2085/* adds an actor */
2086{
2087    return object_add_ow(actor, msg);
2088}
2089
2090
2091int actor_get(const char *actor, xs_dict **data)
2092/* returns an already downloaded actor */
2093{
2094    int status = HTTP_STATUS_OK;
2095    xs_dict *d = NULL;
2096
2097    if (xs_startswith(actor, srv_baseurl)) {
2098        /* it's a (possible) local user */
2099        xs *l = xs_split(actor, "/");
2100        const char *uid = xs_list_get(l, -1);
2101        snac user;
2102
2103        if (!xs_is_null(uid) && user_open(&user, uid)) {
2104            if (data)
2105                *data = msg_actor(&user);
2106
2107            user_free(&user);
2108            return HTTP_STATUS_OK;
2109        }
2110        else
2111            return HTTP_STATUS_NOT_FOUND;
2112    }
2113
2114    /* read the object */
2115    if (!valid_status(status = object_get(actor, &d))) {
2116        d = xs_free(d);
2117        return status;
2118    }
2119
2120    /* if the object is corrupted, discard it */
2121    if (xs_is_null(xs_dict_get(d, "id")) || xs_is_null(xs_dict_get(d, "type"))) {
2122        srv_debug(1, xs_fmt("corrupted actor object %s", actor));
2123        d = xs_free(d);
2124        return HTTP_STATUS_NOT_FOUND;
2125    }
2126
2127    if (data)
2128        *data = d;
2129    else
2130        d = xs_free(d);
2131
2132    xs *fn = _object_fn(actor);
2133    double max_time;
2134
2135    /* maximum time for the actor data to be considered stale */
2136    max_time = 3600.0 * 36.0;
2137
2138    if (mtime(fn) + max_time < (double) time(NULL)) {
2139        /* actor data exists but also stinks */
2140        status = HTTP_STATUS_RESET_CONTENT; /* "110: Response Is Stale" */
2141    }
2142
2143    return status;
2144}
2145
2146
2147int actor_get_refresh(snac *user, const char *actor, xs_dict **data)
2148/* gets an actor and requests a refresh if it's stale */
2149{
2150    int status = actor_get(actor, data);
2151
2152    if (status == HTTP_STATUS_RESET_CONTENT && user && !xs_startswith(actor, srv_baseurl))
2153        enqueue_actor_refresh(user, actor, 0);
2154
2155    return status;
2156}
2157
2158
2159/** user limiting (announce blocks) **/
2160
2161int limited(snac *user, const char *id, int cmd)
2162/* announce messages from a followed (0: check, 1: limit; 2: unlimit) */
2163{
2164    int ret = 0;
2165    xs *dir = xs_fmt("%s/limited", user->basedir);
2166    xs *md5 = xs_md5_hex(id, strlen(id));
2167    xs *fn  = xs_fmt("%s/%s", dir, md5);
2168
2169    switch (cmd) {
2170    case 0: /** check **/
2171        ret = !!(mtime(fn) > 0.0);
2172        break;
2173
2174    case 1: /** limit **/
2175        mkdirx(dir);
2176
2177        if (mtime(fn) > 0.0)
2178            ret = -1;
2179        else {
2180            FILE *f;
2181
2182            if ((f = fopen(fn, "w")) != NULL) {
2183                fprintf(f, "%s\n", id);
2184                fclose(f);
2185            }
2186            else
2187                ret = -2;
2188        }
2189        break;
2190
2191    case 2: /** unlimit **/
2192        if (mtime(fn) > 0.0)
2193            ret = unlink(fn);
2194        else
2195            ret = -1;
2196        break;
2197    }
2198
2199    return ret;
2200}
2201
2202
2203/** tag indexing **/
2204
2205void tag_index(const char *id, const xs_dict *obj)
2206/* update the tag indexes for this object */
2207{
2208    const xs_list *tags = xs_dict_get(obj, "tag");
2209    xs *md5_id = xs_md5_hex(id, strlen(id));
2210
2211    if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
2212        xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
2213
2214        mkdirx(g_tag_dir);
2215
2216        const xs_dict *v;
2217        int ct = 0;
2218        while (xs_list_next(tags, &v, &ct)) {
2219            const char *type = xs_dict_get(v, "type");
2220            const char *name = xs_dict_get(v, "name");
2221
2222            if (!xs_is_null(type) && !xs_is_null(name) && strcmp(type, "Hashtag") == 0) {
2223                while (*name == '#' || *name == '@')
2224                    name++;
2225
2226                if (*name == '\0')
2227                    continue;
2228
2229                name = xs_utf8_to_lower((xs_str *)name);
2230
2231                xs *md5_tag   = xs_md5_hex(name, strlen(name));
2232                xs *tag_dir   = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]);
2233                mkdirx(tag_dir);
2234
2235                xs *g_tag_idx = xs_fmt("%s/%s.idx", tag_dir, md5_tag);
2236
2237                if (!index_in_md5(g_tag_idx, md5_id))
2238                    index_add_md5(g_tag_idx, md5_id);
2239
2240                FILE *f;
2241                xs *g_tag_name = xs_replace(g_tag_idx, ".idx", ".tag");
2242                if ((f = fopen(g_tag_name, "w")) != NULL) {
2243                    fprintf(f, "%s\n", name);
2244                    fclose(f);
2245                }
2246
2247                srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag));
2248            }
2249        }
2250    }
2251}
2252
2253
2254xs_str *tag_fn(const char *tag)
2255{
2256    if (*tag == '#')
2257        tag++;
2258
2259    xs *lw_tag = xs_utf8_to_lower(tag);
2260    xs *md5    = xs_md5_hex(lw_tag, strlen(lw_tag));
2261
2262    return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5);
2263}
2264
2265
2266xs_list *tag_search(const char *tag, int skip, int show)
2267/* returns the list of posts tagged with tag */
2268{
2269    xs *idx = tag_fn(tag);
2270
2271    return index_list_desc(idx, skip, show);
2272}
2273
2274
2275/** lists **/
2276
2277xs_val *list_maint(snac *user, const char *list, int op)
2278/* list maintenance */
2279{
2280    xs_val *l = NULL;
2281
2282    switch (op) {
2283    case 0: /** list of lists **/
2284        {
2285            FILE *f;
2286            xs *spec = xs_fmt("%s/list/" "*.id", user->basedir);
2287            xs *ls   = xs_glob(spec, 0, 0);
2288            int c = 0;
2289            const char *v;
2290
2291            l = xs_list_new();
2292
2293            while (xs_list_next(ls, &v, &c)) {
2294                if ((f = fopen(v, "r")) != NULL) {
2295                    xs *title = xs_readline(f);
2296                    fclose(f);
2297
2298                    title = xs_strip_i(title);
2299
2300                    xs *v2 = xs_replace(v, ".id", "");
2301                    xs *l2 = xs_split(v2, "/");
2302
2303                    /* return [ list_id, list_title ] */
2304                    xs *tmp_list = xs_list_append(xs_list_new(), xs_list_get(l2, -1), title);
2305                    l = xs_list_append(l, tmp_list);
2306                }
2307            }
2308        }
2309
2310        break;
2311
2312    case 1: /** create new list (list is the name) **/
2313        {
2314            xs *lol = list_maint(user, NULL, 0);
2315            int c = 0;
2316            const xs_list *v;
2317            int add = 1;
2318
2319            /* check if this list name already exists */
2320            while (xs_list_next(lol, &v, &c)) {
2321                if (strcmp(xs_list_get(v, 1), list) == 0) {
2322                    add = 0;
2323
2324                    l = xs_dup(xs_list_get(v, 0));
2325
2326                    break;
2327                }
2328            }
2329
2330            if (add) {
2331                FILE *f;
2332                xs *dir = xs_fmt("%s/list/", user->basedir);
2333
2334                l = xs_fmt("%010x", time(NULL));
2335
2336                mkdirx(dir);
2337
2338                xs *fn = xs_fmt("%s%s.id", dir, l);
2339
2340                if ((f = fopen(fn, "w")) != NULL) {
2341                    fprintf(f, "%s\n", list);
2342                    fclose(f);
2343                }
2344            }
2345        }
2346
2347        break;
2348
2349    case 2: /** delete list (list is the id) **/
2350        {
2351            if (xs_is_hex(list)) {
2352                xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2353                unlink(fn);
2354
2355                fn = xs_replace_i(fn, ".id", ".lst");
2356                unlink(fn);
2357
2358                fn = xs_replace_i(fn, ".lst", ".idx");
2359                unlink(fn);
2360
2361                fn = xs_str_cat(fn, ".bak");
2362                unlink(fn);
2363            }
2364        }
2365
2366        break;
2367
2368    case 3: /** get list name **/
2369        if (xs_is_hex(list)) {
2370            FILE *f;
2371            xs *fn = xs_fmt("%s/list/%s.id", user->basedir, list);
2372
2373            if ((f = fopen(fn, "r")) != NULL) {
2374                l = xs_strip_i(xs_readline(f));
2375                fclose(f);
2376            }
2377        }
2378
2379        break;
2380
2381    case 4: /** find list id by name **/
2382        if (xs_is_string(list)) {
2383            xs *lol = list_maint(user, NULL, 0);
2384            const xs_list *li;
2385
2386            xs_list_foreach(lol, li) {
2387                if (strcmp(list, xs_list_get(li, 1)) == 0) {
2388                    l = xs_dup(xs_list_get(li, 0));
2389                    break;
2390                }
2391            }
2392        }
2393    }
2394
2395    return l;
2396}
2397
2398
2399xs_str *list_timeline_fn(snac *user, const char *list)
2400{
2401    return xs_fmt("%s/list/%s.idx", user->basedir, list);
2402}
2403
2404
2405xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2406/* returns the timeline of a list */
2407{
2408    xs_list *l = NULL;
2409
2410    if (!xs_is_hex(list))
2411        return NULL;
2412
2413    xs *fn = list_timeline_fn(user, list);
2414
2415    if (mtime(fn) > 0.0)
2416        l = index_list_desc(fn, skip, show);
2417    else
2418        l = xs_list_new();
2419
2420    return l;
2421}
2422
2423
2424xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
2425/* list content management */
2426{
2427    xs_val *l = NULL;
2428
2429    if (!xs_is_hex(list))
2430        return NULL;
2431
2432    if (actor_md5 != NULL && !xs_is_hex(actor_md5))
2433        return NULL;
2434
2435    xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2436
2437    switch (op) {
2438    case 0: /** list content **/
2439        l = index_list(fn, XS_ALL);
2440
2441        break;
2442
2443    case 1: /** append actor to list **/
2444        if (xs_is_string(actor_md5) && xs_is_hex(actor_md5)) {
2445            if (!index_in_md5(fn, actor_md5))
2446                index_add_md5(fn, actor_md5);
2447        }
2448
2449        break;
2450
2451    case 2: /** delete actor from list **/
2452        if (xs_is_string(actor_md5) && xs_is_hex(actor_md5))
2453            index_del_md5(fn, actor_md5);
2454
2455        break;
2456
2457    default:
2458        srv_log(xs_fmt("ERROR: list_content: bad op %d", op));
2459        break;
2460    }
2461
2462    return l;
2463}
2464
2465
2466void list_distribute(snac *user, const char *who, const xs_dict *post)
2467/* distributes the post to all appropriate lists */
2468{
2469    const char *id = xs_dict_get(post, "id");
2470
2471    /* if who is not set, use the attributedTo in the message */
2472    if (xs_is_null(who))
2473        who = get_atto(post);
2474
2475    if (xs_type(who) == XSTYPE_STRING && xs_type(id) == XSTYPE_STRING) {
2476        xs *a_md5 = xs_md5_hex(who, strlen(who));
2477        xs *i_md5 = xs_md5_hex(id, strlen(id));
2478        xs *spec  = xs_fmt("%s/list/" "*.lst", user->basedir);
2479        xs *ls    = xs_glob(spec, 0, 0);
2480        int c = 0;
2481        const char *v;
2482
2483        while (xs_list_next(ls, &v, &c)) {
2484            /* is the actor in this list? */
2485            if (index_in_md5(v, a_md5)) {
2486                /* it is; add post md5 to its timeline */
2487                xs *idx = xs_replace(v, ".lst", ".idx");
2488                index_add_md5(idx, i_md5);
2489
2490                snac_debug(user, 1, xs_fmt("listed post %s in %s", id, idx));
2491            }
2492        }
2493    }
2494}
2495
2496
2497/** static data **/
2498
2499static int _load_raw_file(const char *fn, xs_val **data, int *size,
2500                        const char *inm, xs_str **etag)
2501/* loads a cached file */
2502{
2503    int status = HTTP_STATUS_NOT_FOUND;
2504
2505    if (fn) {
2506        double tm = mtime(fn);
2507
2508        if (tm > 0.0) {
2509            /* file exists; build the etag */
2510            xs *e = xs_fmt("W/\"snac-%.0lf\"", tm);
2511
2512            /* if if-none-match is set, check if it's the same */
2513            if (!xs_is_null(inm) && strcmp(e, inm) == 0) {
2514                /* client has the newest version */
2515                status = HTTP_STATUS_NOT_MODIFIED;
2516            }
2517            else {
2518                /* newer or never downloaded; read the full file */
2519                FILE *f;
2520
2521                if ((f = fopen(fn, "rb")) != NULL) {
2522                    *size = XS_ALL;
2523                    *data = xs_read(f, size);
2524                    fclose(f);
2525
2526                    status = HTTP_STATUS_OK;
2527                }
2528            }
2529
2530            /* if caller wants the etag, return it */
2531            if (etag != NULL)
2532                *etag = xs_dup(e);
2533
2534            srv_debug(1, xs_fmt("_load_raw_file(): %s %d", fn, status));
2535        }
2536    }
2537
2538    return status;
2539}
2540
2541
2542xs_str *_static_fn(snac *snac, const char *id)
2543/* gets the filename for a static file */
2544{
2545    if (strchr(id, '/'))
2546        return NULL;
2547    else
2548        return xs_fmt("%s/static/%s", snac->basedir, id);
2549}
2550
2551
2552int static_get(snac *snac, const char *id, xs_val **data, int *size,
2553                const char *inm, xs_str **etag)
2554/* returns static content */
2555{
2556    xs *fn = _static_fn(snac, id);
2557
2558    return _load_raw_file(fn, data, size, inm, etag);
2559}
2560
2561
2562void static_put(snac *snac, const char *id, const char *data, int size)
2563/* writes status content */
2564{
2565    xs *fn = _static_fn(snac, id);
2566    FILE *f;
2567
2568    if (fn && (f = fopen(fn, "wb")) != NULL) {
2569        fwrite(data, size, 1, f);
2570        fclose(f);
2571    }
2572}
2573
2574
2575void static_put_meta(snac *snac, const char *id, const char *str)
2576/* puts metadata (i.e. a media description string) to id */
2577{
2578    xs *fn = _static_fn(snac, id);
2579
2580    if (fn) {
2581        fn = xs_str_cat(fn, ".txt");
2582        FILE *f;
2583
2584        if ((f = fopen(fn, "w")) != NULL) {
2585            fprintf(f, "%s\n", str);
2586            fclose(f);
2587        }
2588    }
2589}
2590
2591
2592xs_str *static_get_meta(snac *snac, const char *id)
2593/* gets metadata from a media */
2594{
2595    xs *fn    = _static_fn(snac, id);
2596    xs_str *r = NULL;
2597
2598    if (fn) {
2599        fn = xs_str_cat(fn, ".txt");
2600        FILE *f;
2601
2602        if ((f = fopen(fn, "r")) != NULL) {
2603            r = xs_strip_i(xs_readline(f));
2604            fclose(f);
2605        }
2606    }
2607    else
2608        r = xs_str_new("");
2609
2610    return r;
2611}
2612
2613
2614/** history **/
2615
2616xs_str *_history_fn(snac *snac, const char *id)
2617/* gets the filename for the history */
2618{
2619    if (strchr(id, '/'))
2620        return NULL;
2621    else
2622        return xs_fmt("%s/history/%s", snac->basedir, id);
2623}
2624
2625
2626double history_mtime(snac *snac, const char *id)
2627{
2628    double t = 0.0;
2629    xs *fn = _history_fn(snac, id);
2630
2631    if (fn != NULL)
2632        t = mtime(fn);
2633
2634    return t;
2635}
2636
2637
2638void history_add(snac *snac, const char *id, const char *content, int size,
2639                    xs_str **etag)
2640/* adds something to the history */
2641{
2642    xs *fn = _history_fn(snac, id);
2643    FILE *f;
2644
2645    if (fn && (f = fopen(fn, "w")) != NULL) {
2646        fwrite(content, size, 1, f);
2647        fclose(f);
2648
2649        if (etag) {
2650            double tm = mtime(fn);
2651            *etag = xs_fmt("W/\"snac-%.0lf\"", tm);
2652        }
2653    }
2654}
2655
2656
2657int history_get(snac *snac, const char *id, xs_str **content, int *size,
2658                const char *inm, xs_str **etag)
2659{
2660    xs *fn = _history_fn(snac, id);
2661
2662    return _load_raw_file(fn, content, size, inm, etag);
2663}
2664
2665
2666int history_del(snac *snac, const char *id)
2667{
2668    xs *fn = _history_fn(snac, id);
2669
2670    if (fn)
2671        return unlink(fn);
2672    else
2673        return -1;
2674}
2675
2676
2677xs_list *history_list(snac *snac)
2678{
2679    xs *spec = xs_fmt("%s/history/" "*.html", snac->basedir);
2680
2681    return xs_glob(spec, 1, 1);
2682}
2683
2684
2685void lastlog_write(snac *snac, const char *source)
2686/* writes the last time the user logged in */
2687{
2688    xs *fn = xs_fmt("%s/lastlog.txt", snac->basedir);
2689    FILE *f;
2690
2691    if ((f = fopen(fn, "w")) != NULL) {
2692        fprintf(f, "%lf %s\n", ftime(), source);
2693        fclose(f);
2694    }
2695}
2696
2697
2698/** inbox collection **/
2699
2700void inbox_add(const char *inbox)
2701/* collects a shared inbox */
2702{
2703    /* don't collect ourselves */
2704    if (xs_startswith(inbox, srv_baseurl))
2705        return;
2706
2707    xs *md5 = xs_md5_hex(inbox, strlen(inbox));
2708    xs *fn  = xs_fmt("%s/inbox/%s", srv_basedir, md5);
2709    FILE *f;
2710
2711    if ((f = fopen(fn, "w")) != NULL) {
2712        fprintf(f, "%s\n", inbox);
2713        fclose(f);
2714    }
2715}
2716
2717
2718void inbox_add_by_actor(const xs_dict *actor)
2719/* collects an actor's shared inbox, if it has one */
2720{
2721    const char *v;
2722
2723    if (!xs_is_null(v = xs_dict_get(actor, "endpoints")) &&
2724        !xs_is_null(v = xs_dict_get(v, "sharedInbox"))) {
2725        /* only collect this inbox if its instance is not blocked */
2726        if (!is_instance_blocked(v))
2727            inbox_add(v);
2728    }
2729}
2730
2731
2732xs_list *inbox_list(void)
2733/* returns the collected inboxes as a list */
2734{
2735    xs_list *ibl = xs_list_new();
2736    xs *spec     = xs_fmt("%s/inbox/" "*", srv_basedir);
2737    xs *files    = xs_glob(spec, 0, 0);
2738    const xs_val *v;
2739
2740    xs_list_foreach(files, v) {
2741        FILE *f;
2742
2743        if ((f = fopen(v, "r")) != NULL) {
2744            xs *line = xs_readline(f);
2745
2746            if (line && *line) {
2747                line = xs_strip_i(line);
2748
2749                if (!is_instance_blocked(line))
2750                    ibl = xs_list_append(ibl, line);
2751            }
2752
2753            fclose(f);
2754        }
2755    }
2756
2757    return ibl;
2758}
2759
2760
2761/** instance-wide operations **/
2762
2763xs_str *_instance_block_fn(const char *instance)
2764{
2765    xs *s   = xs_replace(instance, "http:/" "/", "");
2766    xs *s1  = xs_replace(s, "https:/" "/", "");
2767    xs *l   = xs_split(s1, "/");
2768    const char *p = xs_list_get(l, 0);
2769    xs *md5 = xs_md5_hex(p, strlen(p));
2770
2771    return xs_fmt("%s/block/%s", srv_basedir, md5);
2772}
2773
2774
2775int is_instance_blocked(const char *instance)
2776{
2777    xs *fn = _instance_block_fn(instance);
2778
2779    return !!(mtime(fn) != 0.0);
2780}
2781
2782
2783int instance_block(const char *instance)
2784/* blocks a full instance */
2785{
2786    int ret;
2787
2788    /* create the subdir */
2789    xs *dir = xs_fmt("%s/block/", srv_basedir);
2790    mkdirx(dir);
2791
2792    if (!is_instance_blocked(instance)) {
2793        xs *fn = _instance_block_fn(instance);
2794        FILE *f;
2795
2796        if ((f = fopen(fn, "w")) != NULL) {
2797            fprintf(f, "%s\n", instance);
2798            fclose(f);
2799
2800            ret = 0;
2801        }
2802        else
2803            ret = -1;
2804    }
2805    else
2806        ret = -2;
2807
2808    return ret;
2809}
2810
2811
2812int instance_unblock(const char *instance)
2813/* unblocks a full instance */
2814{
2815    int ret;
2816
2817    if (is_instance_blocked(instance)) {
2818        xs *fn = _instance_block_fn(instance);
2819        ret = unlink(fn);
2820    }
2821    else
2822        ret = -2;
2823
2824    return ret;
2825}
2826
2827
2828/** operations by content **/
2829
2830int content_match(const char *file, const xs_dict *msg)
2831/* checks if a message's content matches any of the regexes in file */
2832/* file format: one regex per line */
2833{
2834    xs *fn = xs_fmt("%s/%s", srv_basedir, file);
2835    FILE *f;
2836    int r = 0;
2837    const char *v = xs_dict_get(msg, "content");
2838
2839    if (xs_type(v) == XSTYPE_STRING && *v) {
2840        if ((f = fopen(fn, "r")) != NULL) {
2841            srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn));
2842
2843            /* massage content (strip HTML tags, etc.) */
2844            xs *c1 = xs_regex_replace(v, "<[^>]+>", " ");
2845            c1 = xs_regex_replace_i(c1, " {2,}", " ");
2846            xs *c = xs_utf8_to_lower(c1);
2847
2848            while (!r && !feof(f)) {
2849                xs *rx = xs_strip_i(xs_readline(f));
2850
2851                if (*rx && xs_regex_match(c, rx)) {
2852                    srv_debug(1, xs_fmt("content_match: match for '%s'", rx));
2853                    r = 1;
2854                }
2855            }
2856
2857            fclose(f);
2858        }
2859    }
2860
2861    return r;
2862}
2863
2864
2865xs_list *content_search(snac *user, const char *regex,
2866                int priv, int skip, int show, int max_secs, int *timeout)
2867/* returns a list of posts which content matches the regex */
2868{
2869    if (regex == NULL || *regex == '\0')
2870        return xs_list_new();
2871
2872    xs *i_regex = xs_utf8_to_lower(regex);
2873
2874    xs_set seen;
2875
2876    xs_set_init(&seen);
2877
2878    if (max_secs == 0)
2879        max_secs = 3;
2880
2881    time_t t = time(NULL) + max_secs;
2882    *timeout = 0;
2883
2884    /* iterate all timelines simultaneously */
2885    xs_list *tls[3] = {0};
2886    const char *md5s[3] = {0};
2887    int c[3] = {0};
2888
2889    tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL);   /* public */
2890    tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2891    tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
2892
2893    /* first positioning */
2894    for (int n = 0; n < 3; n++)
2895        xs_list_next(tls[n], &md5s[n], &c[n]);
2896
2897    show += skip;
2898
2899    while (show > 0) {
2900        /* timeout? */
2901        if (time(NULL) > t) {
2902            *timeout = 1;
2903            break;
2904        }
2905
2906        /* find the newest post */
2907        int newest = -1;
2908        double mtime = 0.0;
2909
2910        for (int n = 0; n < 3; n++) {
2911            if (md5s[n] != NULL) {
2912                xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2913                double mt;
2914
2915                while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
2916                    /* object is not here: move to the next one */
2917                    if (xs_list_next(tls[n], &md5s[n], &c[n])) {
2918                        xs_free(fn);
2919                        fn = _object_fn_by_md5(md5s[n], "content_search_2");
2920                    }
2921                    else
2922                        md5s[n] = NULL;
2923                }
2924
2925                if (mt > mtime) {
2926                    newest = n;
2927                    mtime = mt;
2928                }
2929            }
2930        }
2931
2932        if (newest == -1)
2933            break;
2934
2935        const char *md5 = md5s[newest];
2936
2937        /* advance the chosen timeline */
2938        if (!xs_list_next(tls[newest], &md5s[newest], &c[newest]))
2939            md5s[newest] = NULL;
2940
2941        xs *post = NULL;
2942
2943        if (!valid_status(object_get_by_md5(md5, &post)))
2944            continue;
2945
2946        if (!xs_match(xs_dict_get_def(post, "type", "-"), POSTLIKE_OBJECT_TYPE))
2947            continue;
2948
2949        const char *id = xs_dict_get(post, "id");
2950
2951        if (id == NULL || is_hidden(user, id))
2952            continue;
2953
2954        /* recalculate the md5 id to be sure it's not repeated
2955           (it may have been searched by the "url" field instead of "id") */
2956        xs *new_md5 = xs_md5_hex(id, strlen(id));
2957        md5 = new_md5;
2958
2959        /* test for the post URL */
2960        if (strcmp(id, regex) == 0) {
2961            if (xs_set_add(&seen, md5) == 1)
2962                show--;
2963
2964            continue;
2965        }
2966
2967        /* test for the alternate post id */
2968        const char *url = xs_dict_get(post, "url");
2969        if (xs_type(url) == XSTYPE_STRING && strcmp(url, regex) == 0) {
2970            if (xs_set_add(&seen, md5) == 1)
2971                show--;
2972
2973            continue;
2974        }
2975
2976        xs *c = xs_str_new(NULL);
2977        const char *content = xs_dict_get(post, "content");
2978        const char *name    = xs_dict_get(post, "name");
2979
2980        if (!xs_is_null(content))
2981            c = xs_str_cat(c, content);
2982        if (!xs_is_null(name))
2983            c = xs_str_cat(c, " ", name);
2984
2985        /* add alt-texts from attachments */
2986        const xs_list *atts = xs_dict_get(post, "attachment");
2987        int tc = 0;
2988        const xs_dict *att;
2989
2990        while (xs_list_next(atts, &att, &tc)) {
2991            const char *name = xs_dict_get(att, "name");
2992
2993            if (name != NULL)
2994                c = xs_str_cat(c, " ", name);
2995        }
2996
2997        /* strip HTML */
2998        c = xs_regex_replace_i(c, "<[^>]+>", " ");
2999        c = xs_regex_replace_i(c, " {2,}", " ");
3000
3001        /* convert to lowercase */
3002        xs *lc = xs_utf8_to_lower(c);
3003
3004        /* apply regex */
3005        if (xs_regex_match(lc, i_regex)) {
3006            if (xs_set_add(&seen, md5) == 1)
3007            show--;
3008        }
3009    }
3010
3011    xs_list *r = xs_set_result(&seen);
3012
3013    if (skip) {
3014        /* BAD */
3015        while (skip--) {
3016            r = xs_list_del(r, 0);
3017        }
3018    }
3019
3020    xs_free(tls[0]);
3021    xs_free(tls[1]);
3022    xs_free(tls[2]);
3023
3024    return r;
3025}
3026
3027
3028/** notifications **/
3029
3030xs_str *notify_check_time(snac *snac, int reset)
3031/* gets or resets the latest notification check time */
3032{
3033    xs_str *t = NULL;
3034    xs *fn    = xs_fmt("%s/notifydate.txt", snac->basedir);
3035    FILE *f;
3036
3037    if (reset) {
3038        if ((f = fopen(fn, "w")) != NULL) {
3039            t = tid(0);
3040            fprintf(f, "%s\n", t);
3041            fclose(f);
3042        }
3043    }
3044    else {
3045        if ((f = fopen(fn, "r")) != NULL) {
3046            t = xs_readline(f);
3047            fclose(f);
3048        }
3049        else
3050            /* never set before */
3051            t = xs_fmt("%16.6f", 0.0);
3052    }
3053
3054    return t;
3055}
3056
3057xs_dict *markers_get(snac *snac, const xs_list *markers)
3058{
3059    xs *data = NULL;
3060    xs_dict *returns = xs_dict_new();
3061    xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3062    const xs_str *v = NULL;
3063    FILE *f;
3064
3065    if ((f = fopen(fn, "r")) != NULL) {
3066        data = xs_json_load(f);
3067        fclose(f);
3068    }
3069
3070    if (xs_is_null(data))
3071        data = xs_dict_new();
3072
3073    xs_list_foreach(markers, v) {
3074        const xs_dict *mark = xs_dict_get(data, v);
3075        if (!xs_is_null(mark)) {
3076            returns = xs_dict_append(returns, v, mark);
3077        }
3078    }
3079    return returns;
3080}
3081
3082xs_dict *markers_set(snac *snac, const char *home_marker, const char *notify_marker)
3083/* gets or sets notification marker */
3084{
3085    xs *data = NULL;
3086    xs_dict *written = xs_dict_new();
3087    xs *fn = xs_fmt("%s/markers.json", snac->basedir);
3088    FILE *f;
3089
3090    if ((f = fopen(fn, "r")) != NULL) {
3091        data = xs_json_load(f);
3092        fclose(f);
3093    }
3094
3095    if (xs_is_null(data))
3096        data = xs_dict_new();
3097
3098    if (!xs_is_null(home_marker)) {
3099        xs *home = xs_dict_new();
3100        xs *s_tid = tid(0);
3101        home = xs_dict_append(home, "last_read_id", home_marker);
3102        home = xs_dict_append(home, "version", xs_stock(0));
3103        home = xs_dict_append(home, "updated_at", s_tid);
3104        data = xs_dict_set(data, "home", home);
3105        written = xs_dict_append(written, "home", home);
3106    }
3107
3108    if (!xs_is_null(notify_marker)) {
3109        xs *notify = xs_dict_new();
3110        xs *s_tid = tid(0);
3111        notify = xs_dict_append(notify, "last_read_id", notify_marker);
3112        notify = xs_dict_append(notify, "version", xs_stock(0));
3113        notify = xs_dict_append(notify, "updated_at", s_tid);
3114        data = xs_dict_set(data, "notifications", notify);
3115        written = xs_dict_append(written, "notifications", notify);
3116    }
3117
3118    if ((f = fopen(fn, "w")) != NULL) {
3119        xs_json_dump(data, 4, f);
3120        fclose(f);
3121    }
3122
3123    return written;
3124}
3125
3126void notify_add(snac *snac, const char *type, const char *utype,
3127                const char *actor, const char *objid, const xs_dict *msg)
3128/* adds a new notification */
3129{
3130    xs *ntid = tid(0);
3131    xs *fn   = xs_fmt("%s/notify/", snac->basedir);
3132    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3133    FILE *f;
3134
3135    /* create the directory */
3136    mkdirx(fn);
3137    fn = xs_str_cat(fn, ntid);
3138    fn = xs_str_cat(fn, ".json");
3139
3140    xs *noti = xs_dict_new();
3141
3142    noti = xs_dict_append(noti, "id",    ntid);
3143    noti = xs_dict_append(noti, "type",  type);
3144    noti = xs_dict_append(noti, "utype", utype);
3145    noti = xs_dict_append(noti, "actor", actor);
3146    noti = xs_dict_append(noti, "date",  date);
3147    noti = xs_dict_append(noti, "msg",   msg);
3148
3149    if (!xs_is_null(objid))
3150        noti = xs_dict_append(noti, "objid", objid);
3151
3152    if ((f = fopen(fn, "w")) != NULL) {
3153        xs_json_dump(noti, 4, f);
3154        fclose(f);
3155    }
3156
3157    /* add it to the index if it already exists */
3158    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3159
3160    if (mtime(idx) != 0.0) {
3161        pthread_mutex_lock(&data_mutex);
3162
3163        if ((f = fopen(idx, "a")) != NULL) {
3164            fprintf(f, "%-32s\n", ntid);
3165            fclose(f);
3166        }
3167
3168        pthread_mutex_unlock(&data_mutex);
3169    }
3170
3171    if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook")))
3172        enqueue_notify_webhook(snac, noti, 0);
3173}
3174
3175
3176xs_dict *notify_get(snac *snac, const char *id)
3177/* gets a notification */
3178{
3179    /* base file */
3180    xs *fn = xs_fmt("%s/notify/%s", snac->basedir, id);
3181
3182    /* strip spaces and add extension */
3183    fn = xs_strip_i(fn);
3184    fn = xs_str_cat(fn, ".json");
3185
3186    FILE *f;
3187    xs_dict *out = NULL;
3188
3189    if ((f = fopen(fn, "r")) != NULL) {
3190        out = xs_json_load(f);
3191        fclose(f);
3192    }
3193
3194    return out;
3195}
3196
3197
3198xs_list *notify_list(snac *snac, int skip, int show)
3199/* returns a list of notification ids */
3200{
3201    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3202
3203    if (mtime(idx) == 0.0) {
3204        /* create the index from scratch */
3205        FILE *f;
3206
3207        pthread_mutex_lock(&data_mutex);
3208
3209        if ((f = fopen(idx, "w")) != NULL) {
3210            xs *spec = xs_fmt("%s/notify/" "*.json", snac->basedir);
3211            xs *lst  = xs_glob(spec, 1, 0);
3212            xs_list *p = lst;
3213            const char *v;
3214
3215            while (xs_list_iter(&p, &v)) {
3216                char *p = strrchr(v, '.');
3217                if (p) {
3218                    *p = '\0';
3219                    fprintf(f, "%-32s\n", v);
3220                }
3221            }
3222
3223            fclose(f);
3224        }
3225
3226        pthread_mutex_unlock(&data_mutex);
3227    }
3228
3229    return index_list_desc(idx, skip, show);
3230}
3231
3232
3233int notify_new_num(snac *snac)
3234/* counts the number of new notifications */
3235{
3236    xs *t = notify_check_time(snac, 0);
3237    xs *lst = notify_list(snac, 0, XS_ALL);
3238    int cnt = 0;
3239
3240    xs_list *p = lst;
3241    const xs_str *v;
3242
3243    while (xs_list_iter(&p, &v)) {
3244        xs *id = xs_strip_i(xs_dup(v));
3245
3246        /* old? count no more */
3247        if (strcmp(id, t) < 0)
3248            break;
3249
3250        cnt++;
3251    }
3252
3253    return cnt;
3254}
3255
3256
3257void notify_clear(snac *snac)
3258/* clears all notifications */
3259{
3260    xs *spec   = xs_fmt("%s/notify/" "*", snac->basedir);
3261    xs *lst    = xs_glob(spec, 0, 0);
3262    xs_list *p = lst;
3263    const xs_str *v;
3264
3265    while (xs_list_iter(&p, &v))
3266        unlink(v);
3267
3268    xs *idx = xs_fmt("%s/notify.idx", snac->basedir);
3269
3270    if (mtime(idx) != 0.0) {
3271        pthread_mutex_lock(&data_mutex);
3272        truncate(idx, 0);
3273        pthread_mutex_unlock(&data_mutex);
3274    }
3275}
3276
3277
3278/** the queue **/
3279
3280static xs_dict *_enqueue_put(const char *fn, xs_dict *msg)
3281/* writes safely to the queue */
3282{
3283    xs *tfn = xs_fmt("%s.tmp", fn);
3284    FILE *f;
3285
3286    if ((f = fopen(tfn, "w")) != NULL) {
3287        xs_json_dump(msg, 4, f);
3288        fclose(f);
3289
3290        rename(tfn, fn);
3291    }
3292
3293    return msg;
3294}
3295
3296
3297static xs_dict *_new_qmsg(const char *type, const xs_val *msg, int retries)
3298/* creates a queue message */
3299{
3300    int qrt  = xs_number_get(xs_dict_get(srv_config, "queue_retry_minutes"));
3301    xs *ntid = tid(retries * 60 * qrt);
3302    xs *rn   = xs_number_new(retries);
3303
3304    xs_dict *qmsg = xs_dict_new();
3305
3306    qmsg = xs_dict_append(qmsg, "type",    type);
3307    qmsg = xs_dict_append(qmsg, "message", msg);
3308    qmsg = xs_dict_append(qmsg, "retries", rn);
3309    qmsg = xs_dict_append(qmsg, "ntid",    ntid);
3310
3311    return qmsg;
3312}
3313
3314
3315void enqueue_input(snac *snac, const xs_dict *msg, const xs_dict *req, int retries)
3316/* enqueues an input message */
3317{
3318    xs *qmsg   = _new_qmsg("input", msg, retries);
3319    const char *ntid = xs_dict_get(qmsg, "ntid");
3320    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3321
3322    qmsg = xs_dict_append(qmsg, "req", req);
3323
3324    qmsg = _enqueue_put(fn, qmsg);
3325
3326    snac_debug(snac, 1, xs_fmt("enqueue_input %s", xs_dict_get(msg, "id")));
3327}
3328
3329
3330void enqueue_shared_input(const xs_dict *msg, const xs_dict *req, int retries)
3331/* enqueues an input message from the shared input */
3332{
3333    xs *qmsg   = _new_qmsg("input", msg, retries);
3334    const char *ntid = xs_dict_get(qmsg, "ntid");
3335    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3336
3337    qmsg = xs_dict_append(qmsg, "req", req);
3338
3339    qmsg = _enqueue_put(fn, qmsg);
3340
3341    srv_debug(1, xs_fmt("enqueue_shared_input %s", xs_dict_get(msg, "id")));
3342}
3343
3344
3345void enqueue_output_raw(const char *keyid, const char *seckey,
3346                        const xs_dict *msg, const xs_str *inbox,
3347                        int retries, int p_status)
3348/* enqueues an output message to an inbox */
3349{
3350    xs *qmsg   = _new_qmsg("output", msg, retries);
3351    const char *ntid = xs_dict_get(qmsg, "ntid");
3352    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3353
3354    xs *ns = xs_number_new(p_status);
3355    qmsg = xs_dict_append(qmsg, "p_status", ns);
3356
3357    qmsg = xs_dict_append(qmsg, "inbox",  inbox);
3358    qmsg = xs_dict_append(qmsg, "keyid",  keyid);
3359    qmsg = xs_dict_append(qmsg, "seckey", seckey);
3360
3361    /* if it's to be sent right now, bypass the disk queue and post the job */
3362    if (retries == 0 && p_state != NULL)
3363        job_post(qmsg, 0);
3364    else {
3365        qmsg = _enqueue_put(fn, qmsg);
3366        srv_debug(1, xs_fmt("enqueue_output %s %s %d", inbox, fn, retries));
3367    }
3368}
3369
3370
3371void enqueue_output(snac *snac, const xs_dict *msg,
3372                    const xs_str *inbox, int retries, int p_status)
3373/* enqueues an output message to an inbox */
3374{
3375    if (xs_startswith(inbox, snac->actor)) {
3376        snac_debug(snac, 1, xs_str_new("refusing enqueue to myself"));
3377        return;
3378    }
3379
3380    const char *seckey = xs_dict_get(snac->key, "secret");
3381
3382    enqueue_output_raw(snac->actor, seckey, msg, inbox, retries, p_status);
3383}
3384
3385
3386void enqueue_output_by_actor(snac *snac, const xs_dict *msg,
3387                            const xs_str *actor, int retries)
3388/* enqueues an output message for an actor */
3389{
3390    xs *inbox = get_actor_inbox(actor, 1);
3391
3392    if (!xs_is_null(inbox))
3393        enqueue_output(snac, msg, inbox, retries, 0);
3394    else
3395        snac_log(snac, xs_fmt("enqueue_output_by_actor cannot get inbox %s", actor));
3396}
3397
3398
3399void enqueue_email(const xs_dict *msg, int retries)
3400/* enqueues an email message to be sent */
3401{
3402    xs *qmsg   = _new_qmsg("email", msg, retries);
3403    const char *ntid = xs_dict_get(qmsg, "ntid");
3404    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3405
3406    qmsg = _enqueue_put(fn, qmsg);
3407
3408    srv_debug(1, xs_fmt("enqueue_email %d", retries));
3409}
3410
3411
3412void enqueue_telegram(const xs_str *msg, const char *bot, const char *chat_id)
3413/* enqueues a message to be sent via Telegram */
3414{
3415    xs *qmsg   = _new_qmsg("telegram", msg, 0);
3416    const char *ntid = xs_dict_get(qmsg, "ntid");
3417    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3418
3419    qmsg = xs_dict_append(qmsg, "bot",      bot);
3420    qmsg = xs_dict_append(qmsg, "chat_id",  chat_id);
3421
3422    qmsg = _enqueue_put(fn, qmsg);
3423
3424    srv_debug(1, xs_fmt("enqueue_telegram %s %s", bot, chat_id));
3425}
3426
3427void enqueue_ntfy(const xs_str *msg, const char *ntfy_server, const char *ntfy_token)
3428/* enqueues a message to be sent via ntfy */
3429{
3430    xs *qmsg   = _new_qmsg("ntfy", msg, 0);
3431    const char *ntid = xs_dict_get(qmsg, "ntid");
3432    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3433
3434    qmsg = xs_dict_append(qmsg, "ntfy_server", ntfy_server);
3435    qmsg = xs_dict_append(qmsg, "ntfy_token",  ntfy_token);
3436
3437
3438    qmsg = _enqueue_put(fn, qmsg);
3439
3440    srv_debug(1, xs_fmt("enqueue_ntfy %s %s", ntfy_server, ntfy_token));
3441}
3442
3443void enqueue_message(snac *snac, const xs_dict *msg)
3444/* enqueues an output message */
3445{
3446    xs *qmsg   = _new_qmsg("message", msg, 0);
3447    const char *ntid = xs_dict_get(qmsg, "ntid");
3448    xs *fn     = xs_fmt("%s/queue/%s.json", snac->basedir, ntid);
3449
3450    qmsg = _enqueue_put(fn, qmsg);
3451
3452    snac_debug(snac, 0, xs_fmt("enqueue_message %s", xs_dict_get(msg, "id")));
3453}
3454
3455
3456void enqueue_close_question(snac *user, const char *id, int end_secs)
3457/* enqueues the closing of a question */
3458{
3459    xs *qmsg = _new_qmsg("close_question", id, 0);
3460    xs *ntid = tid(end_secs);
3461    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3462
3463    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3464
3465    qmsg = _enqueue_put(fn, qmsg);
3466
3467    snac_debug(user, 0, xs_fmt("enqueue_close_question %s", id));
3468}
3469
3470
3471void enqueue_object_request(snac *user, const char *id, int forward_secs)
3472/* enqueues the request of an object in the future */
3473{
3474    xs *qmsg = _new_qmsg("object_request", id, 0);
3475    xs *ntid = tid(forward_secs);
3476    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3477
3478    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3479
3480    qmsg = _enqueue_put(fn, qmsg);
3481
3482    snac_debug(user, 0, xs_fmt("enqueue_object_request %s %d", id, forward_secs));
3483}
3484
3485
3486void enqueue_verify_links(snac *user)
3487/* enqueues a link verification */
3488{
3489    xs *qmsg   = _new_qmsg("verify_links", "", 0);
3490    const char *ntid = xs_dict_get(qmsg, "ntid");
3491    xs *fn     = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3492
3493    qmsg = _enqueue_put(fn, qmsg);
3494
3495    snac_debug(user, 1, xs_fmt("enqueue_verify_links %s", user->actor));
3496}
3497
3498
3499void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs)
3500/* enqueues an actor refresh */
3501{
3502    xs *qmsg = _new_qmsg("actor_refresh", "", 0);
3503    xs *ntid = tid(forward_secs);
3504    xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3505
3506    qmsg = xs_dict_set(qmsg, "ntid", ntid);
3507    qmsg = xs_dict_append(qmsg, "actor", actor);
3508
3509    qmsg = _enqueue_put(fn, qmsg);
3510
3511    snac_debug(user, 1, xs_fmt("enqueue_actor_refresh %s", actor));
3512}
3513
3514
3515void enqueue_webmention(const xs_dict *msg)
3516/* enqueues a webmention for the post */
3517{
3518    xs *qmsg   = _new_qmsg("webmention", msg, 0);
3519    const char *ntid = xs_dict_get(qmsg, "ntid");
3520    xs *fn     = xs_fmt("%s/queue/%s.json", srv_basedir, ntid);
3521
3522    qmsg = _enqueue_put(fn, qmsg);
3523
3524    srv_debug(1, xs_fmt("enqueue_webmention"));
3525}
3526
3527
3528void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries)
3529/* enqueues a notification webhook */
3530{
3531    const char *webhook = xs_dict_get(user->config, "notify_webhook");
3532
3533    if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/
3534        xs *msg = xs_dup(noti);
3535
3536        /* add more data */
3537        msg = xs_dict_set(msg, "target", user->actor);
3538        msg = xs_dict_set(msg, "uid", user->uid);
3539        msg = xs_dict_set(msg, "basedir", srv_basedir);
3540        msg = xs_dict_set(msg, "baseurl", srv_baseurl);
3541
3542        xs *actor_obj = NULL;
3543
3544        if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj)
3545            msg = xs_dict_set(msg, "account", actor_obj);
3546
3547        /* if this post is a reply, also add the inReplyTo object */
3548        const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo");
3549
3550        if (xs_is_string(in_reply_to)) {
3551            xs *irt_obj = NULL;
3552
3553            if (valid_status(object_get(in_reply_to, &irt_obj)))
3554                msg = xs_dict_set(msg, "reply", irt_obj);
3555        }
3556
3557        xs *qmsg = _new_qmsg("notify_webhook", msg, retries);
3558        const char *ntid = xs_dict_get(qmsg, "ntid");
3559        xs *fn   = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3560
3561        qmsg = _enqueue_put(fn, qmsg);
3562
3563        snac_debug(user, 1, xs_fmt("notify_webhook"));
3564    }
3565}
3566
3567
3568int was_question_voted(snac *user, const char *id)
3569/* returns true if the user voted in this poll */
3570{
3571    xs *children = object_children(id);
3572    int voted = 0;
3573    xs_list *p;
3574    const xs_str *md5;
3575
3576    p = children;
3577    while (xs_list_iter(&p, &md5)) {
3578        xs *obj = NULL;
3579
3580        if (valid_status(object_get_by_md5(md5, &obj))) {
3581            const char *atto = get_atto(obj);
3582            if (atto && strcmp(atto, user->actor) == 0 &&
3583                !xs_is_null(xs_dict_get(obj, "name"))) {
3584                voted = 1;
3585                break;
3586            }
3587        }
3588    }
3589
3590    return voted;
3591}
3592
3593
3594xs_list *user_queue(snac *snac)
3595/* returns a list with filenames that can be dequeued */
3596{
3597    xs *spec      = xs_fmt("%s/queue/" "*.json", snac->basedir);
3598    xs_list *list = xs_list_new();
3599    time_t t      = time(NULL);
3600    xs_list *p;
3601    const xs_val *v;
3602
3603    xs *fns = xs_glob(spec, 0, 0);
3604
3605    p = fns;
3606    while (xs_list_iter(&p, &v)) {
3607        /* get the retry time from the basename */
3608        char *bn  = strrchr(v, '/');
3609        time_t t2 = atol(bn + 1);
3610
3611        if (t2 > t)
3612            snac_debug(snac, 2, xs_fmt("user_queue not yet time for %s [%ld]", v, t));
3613        else {
3614            list = xs_list_append(list, v);
3615            snac_debug(snac, 2, xs_fmt("user_queue ready for %s", v));
3616        }
3617    }
3618
3619    return list;
3620}
3621
3622
3623xs_list *queue(void)
3624/* returns a list with filenames that can be dequeued */
3625{
3626    xs *spec      = xs_fmt("%s/queue/" "*.json", srv_basedir);
3627    xs_list *list = xs_list_new();
3628    time_t t      = time(NULL);
3629    xs_list *p;
3630    const xs_val *v;
3631
3632    xs *fns = xs_glob(spec, 0, 0);
3633
3634    p = fns;
3635    while (xs_list_iter(&p, &v)) {
3636        /* get the retry time from the basename */
3637        char *bn  = strrchr(v, '/');
3638        time_t t2 = atol(bn + 1);
3639
3640        if (t2 > t)
3641            srv_debug(2, xs_fmt("queue not yet time for %s [%ld]", v, t));
3642        else {
3643            list = xs_list_append(list, v);
3644            srv_debug(2, xs_fmt("queue ready for %s", v));
3645        }
3646    }
3647
3648    return list;
3649}
3650
3651
3652xs_dict *queue_get(const char *fn)
3653/* gets a file from a queue */
3654{
3655    FILE *f;
3656    xs_dict *obj = NULL;
3657
3658    if ((f = fopen(fn, "r")) != NULL) {
3659        obj = xs_json_load(f);
3660        fclose(f);
3661    }
3662
3663    return obj;
3664}
3665
3666
3667xs_dict *dequeue(const char *fn)
3668/* dequeues a message */
3669{
3670    xs_dict *obj = queue_get(fn);
3671
3672    unlink(fn);
3673
3674    return obj;
3675}
3676
3677
3678/** the purge **/
3679
3680static int _purge_file(const char *fn, time_t mt)
3681/* purge fn if it's older than days */
3682{
3683    int ret = 0;
3684
3685    if (mtime(fn) < mt) {
3686        /* older than the minimum time: delete it */
3687        unlink(fn);
3688        srv_debug(2, xs_fmt("purged %s", fn));
3689        ret = 1;
3690    }
3691
3692    return ret;
3693}
3694
3695
3696static void _purge_dir(const char *dir, int days)
3697/* purges all files in a directory older than days */
3698{
3699    int cnt = 0;
3700
3701    if (days) {
3702        time_t mt = time(NULL) - days * 24 * 3600;
3703        xs *spec  = xs_fmt("%s/" "*", dir);
3704        xs *list  = xs_glob(spec, 0, 0);
3705        xs_list *p;
3706        const xs_str *v;
3707
3708        p = list;
3709        while (xs_list_iter(&p, &v))
3710            cnt += _purge_file(v, mt);
3711
3712        srv_debug(1, xs_fmt("purge: %s %d", dir, cnt));
3713    }
3714}
3715
3716
3717static void _purge_user_subdir(snac *snac, const char *subdir, int days)
3718/* purges all files in a user subdir older than days */
3719{
3720    xs *u_subdir = xs_fmt("%s/%s", snac->basedir, subdir);
3721
3722    _purge_dir(u_subdir, days);
3723}
3724
3725
3726void purge_server(void)
3727/* purge global server data */
3728{
3729    xs *spec = xs_fmt("%s/object/??", srv_basedir);
3730    xs *dirs = xs_glob(spec, 0, 0);
3731    xs_list *p;
3732    const xs_str *v;
3733    int cnt = 0;
3734    int icnt = 0;
3735
3736    time_t mt = time(NULL) - 7 * 24 * 3600;
3737
3738    p = dirs;
3739    while (xs_list_iter(&p, &v)) {
3740        xs_list *p2;
3741        const xs_str *v2;
3742
3743        {
3744            xs *spec2 = xs_fmt("%s/" "*.json", v);
3745            xs *files = xs_glob(spec2, 0, 0);
3746
3747            p2 = files;
3748            while (xs_list_iter(&p2, &v2)) {
3749                int n_link;
3750
3751                /* old and with no hard links? */
3752                if (mtime_nl(v2, &n_link) < mt && n_link < 2) {
3753                    xs *s1    = xs_replace(v2, ".json", "");
3754                    xs *l     = xs_split(s1, "/");
3755                    const char *md5 = xs_list_get(l, -1);
3756
3757                    object_del_by_md5(md5);
3758                    cnt++;
3759                }
3760            }
3761        }
3762
3763        {
3764            /* look for stray indexes */
3765            xs *speci = xs_fmt("%s/" "*_?.idx", v);
3766            xs *idxfs = xs_glob(speci, 0, 0);
3767
3768            p2 = idxfs;
3769            while (xs_list_iter(&p2, &v2)) {
3770                /* old enough to consider? */
3771                if (mtime(v2) < mt) {
3772                    /* check if the indexed object is here */
3773                    xs *o = xs_dup(v2);
3774                    char *ext = strchr(o, '_');
3775
3776                    if (ext) {
3777                        *ext = '\0';
3778                        o = xs_str_cat(o, ".json");
3779
3780                        if (mtime(o) == 0.0) {
3781                            /* delete */
3782                            unlink(v2);
3783                            srv_debug(1, xs_fmt("purged %s", v2));
3784                            icnt++;
3785                        }
3786                    }
3787                }
3788            }
3789
3790            /* delete index backups */
3791            xs *specb = xs_fmt("%s/" "*.bak", v);
3792            xs *bakfs = xs_glob(specb, 0, 0);
3793
3794            p2 = bakfs;
3795            while (xs_list_iter(&p2, &v2)) {
3796                unlink(v2);
3797                srv_debug(1, xs_fmt("purged %s", v2));
3798            }
3799        }
3800    }
3801
3802    /* purge collected inboxes */
3803    xs *ib_dir = xs_fmt("%s/inbox", srv_basedir);
3804    _purge_dir(ib_dir, 7);
3805
3806    /* purge the instance timeline */
3807    xs *itl_fn = xs_fmt("%s/public.idx", srv_basedir);
3808    int itl_gc = index_gc(itl_fn);
3809
3810    /* purge tag indexes */
3811    xs *tag_spec = xs_fmt("%s/tag/??", srv_basedir);
3812    xs *tag_dirs = xs_glob(tag_spec, 0, 0);
3813    p = tag_dirs;
3814
3815    int tag_gc = 0;
3816    while (xs_list_iter(&p, &v)) {
3817        xs *spec2 = xs_fmt("%s/" "*.idx", v);
3818        xs *files = xs_glob(spec2, 0, 0);
3819        xs_list *p2;
3820        const xs_str *v2;
3821
3822        p2 = files;
3823        while (xs_list_iter(&p2, &v2)) {
3824            tag_gc += index_gc(v2);
3825            xs *bak = xs_fmt("%s.bak", v2);
3826            unlink(bak);
3827
3828            if (index_len(v2) == 0) {
3829                /* there are no longer any entry with this tag;
3830                   purge it completely */
3831                unlink(v2);
3832                xs *dottag = xs_replace(v2, ".idx", ".tag");
3833                unlink(dottag);
3834            }
3835        }
3836    }
3837
3838    srv_debug(1, xs_fmt("purge: global "
3839            "(obj: %d, idx: %d, itl: %d, tag: %d)", cnt, icnt, itl_gc, tag_gc));
3840}
3841
3842
3843void purge_user(snac *snac)
3844/* do the purge for this user */
3845{
3846    int priv_days, pub_days, user_days = 0;
3847    const char *v;
3848    int n;
3849
3850    priv_days = xs_number_get(xs_dict_get(srv_config, "timeline_purge_days"));
3851    pub_days  = xs_number_get(xs_dict_get(srv_config, "local_purge_days"));
3852
3853    if ((v = xs_dict_get(snac->config_o, "purge_days")) != NULL ||
3854        (v = xs_dict_get(snac->config, "purge_days")) != NULL)
3855        user_days = xs_number_get(v);
3856
3857    if (user_days) {
3858        /* override admin settings only if they are lesser */
3859        if (priv_days == 0 || user_days < priv_days)
3860            priv_days = user_days;
3861
3862        if (pub_days == 0 || user_days < pub_days)
3863            pub_days = user_days;
3864    }
3865
3866    _purge_user_subdir(snac, "hidden",  priv_days);
3867    _purge_user_subdir(snac, "private", priv_days);
3868
3869    _purge_user_subdir(snac, "public",  pub_days);
3870
3871    const char *idxs[] = { "followers.idx", "private.idx", "public.idx",
3872                           "pinned.idx", "bookmark.idx", "draft.idx", "sched.idx", NULL };
3873
3874    for (n = 0; idxs[n]; n++) {
3875        xs *idx = xs_fmt("%s/%s", snac->basedir, idxs[n]);
3876        int gc = index_gc(idx);
3877        srv_debug(1, xs_fmt("purge: %s %d", idx, gc));
3878    }
3879
3880    /* purge lists */
3881    {
3882        xs *spec = xs_fmt("%s/list/" "*.idx", snac->basedir);
3883        xs *lol  = xs_glob(spec, 0, 0);
3884        int c = 0;
3885        const char *v;
3886
3887        while (xs_list_next(lol, &v, &c)) {
3888            int gc = index_gc(v);
3889            srv_debug(1, xs_fmt("purge: %s %d", v, gc));
3890        }
3891    }
3892
3893    /* unrelated to purging, but it's a janitorial process, so what the hell */
3894    verify_links(snac);
3895}
3896
3897
3898void purge_all(void)
3899/* purge all users */
3900{
3901    snac snac;
3902    xs *list = user_list();
3903    char *p;
3904    const char *uid;
3905
3906    p = list;
3907    while (xs_list_iter(&p, &uid)) {
3908        if (user_open(&snac, uid)) {
3909            purge_user(&snac);
3910            user_free(&snac);
3911        }
3912    }
3913
3914    purge_server();
3915
3916#ifndef NO_MASTODON_API
3917    mastoapi_purge();
3918#endif
3919}
3920
3921
3922/** archive **/
3923
3924void srv_archive(const char *direction, const char *url, xs_dict *req,
3925                 const char *payload, int p_size,
3926                 int status, xs_dict *headers,
3927                 const char *body, int b_size)
3928/* archives a connection */
3929{
3930    /* obsessive archiving */
3931    xs *date = tid(0);
3932    xs *dir  = xs_fmt("%s/archive/%s_%s", srv_basedir, date, direction);
3933    FILE *f;
3934
3935    if (mkdirx(dir) != -1) {
3936        xs *meta_fn = xs_fmt("%s/_META", dir);
3937
3938        if ((f = fopen(meta_fn, "w")) != NULL) {
3939            xs *j1 = xs_json_dumps(req, 4);
3940            xs *j2 = xs_json_dumps(headers, 4);
3941
3942            fprintf(f, "dir: %s\n", direction);
3943
3944            if (url)
3945                fprintf(f, "url: %s\n", url);
3946
3947            fprintf(f, "req: %s\n", j1);
3948            fprintf(f, "p_size: %d\n", p_size);
3949            fprintf(f, "status: %d\n", status);
3950            fprintf(f, "response: %s\n", j2);
3951            fprintf(f, "b_size: %d\n", b_size);
3952            fclose(f);
3953        }
3954
3955        if (p_size && payload) {
3956            xs *payload_fn = NULL;
3957            xs *payload_fn_raw = NULL;
3958            const char *v = xs_dict_get(req, "content-type");
3959
3960            if (v && xs_str_in(v, "json") != -1) {
3961                payload_fn = xs_fmt("%s/payload.json", dir);
3962
3963                if ((f = fopen(payload_fn, "w")) != NULL) {
3964                    xs *v1 = xs_json_loads(payload);
3965                    xs *j1 = NULL;
3966
3967                    if (v1 != NULL)
3968                        j1 = xs_json_dumps(v1, 4);
3969
3970                    if (j1 != NULL)
3971                        fwrite(j1, strlen(j1), 1, f);
3972                    else
3973                        fwrite(payload, p_size, 1, f);
3974
3975                    fclose(f);
3976                }
3977            }
3978
3979            payload_fn_raw = xs_fmt("%s/payload", dir);
3980
3981            if ((f = fopen(payload_fn_raw, "w")) != NULL) {
3982                fwrite(payload, p_size, 1, f);
3983                fclose(f);
3984            }
3985        }
3986
3987        if (b_size && body) {
3988            xs *body_fn = NULL;
3989            const char *v = xs_dict_get(headers, "content-type");
3990
3991            if (v && xs_str_in(v, "json") != -1) {
3992                body_fn = xs_fmt("%s/body.json", dir);
3993
3994                if ((f = fopen(body_fn, "w")) != NULL) {
3995                    xs *v1 = xs_json_loads(body);
3996                    xs *j1 = NULL;
3997
3998                    if (v1 != NULL)
3999                        j1 = xs_json_dumps(v1, 4);
4000
4001                    if (j1 != NULL)
4002                        fwrite(j1, strlen(j1), 1, f);
4003                    else
4004                        fwrite(body, b_size, 1, f);
4005
4006                    fclose(f);
4007                }
4008            }
4009            else {
4010                body_fn = xs_fmt("%s/body", dir);
4011
4012                if ((f = fopen(body_fn, "w")) != NULL) {
4013                    fwrite(body, b_size, 1, f);
4014                    fclose(f);
4015                }
4016            }
4017        }
4018    }
4019}
4020
4021
4022void srv_archive_error(const char *prefix, const xs_str *err,
4023                       const xs_dict *req, const xs_val *data)
4024/* archives an error */
4025{
4026    xs *ntid = tid(0);
4027    xs *fn   = xs_fmt("%s/error/%s_%s", srv_basedir, ntid, prefix);
4028    FILE *f;
4029
4030    if ((f = fopen(fn, "w")) != NULL) {
4031        fprintf(f, "Error: %s\n", err);
4032
4033        if (req) {
4034            fprintf(f, "Request headers:\n");
4035
4036            xs_json_dump(req, 4, f);
4037
4038            fprintf(f, "\n");
4039        }
4040
4041        if (data) {
4042            fprintf(f, "Data:\n");
4043
4044            if (xs_type(data) == XSTYPE_LIST || xs_type(data) == XSTYPE_DICT) {
4045                xs_json_dump(data, 4, f);
4046            }
4047            else
4048                fprintf(f, "%s", data);
4049
4050            fprintf(f, "\n");
4051        }
4052
4053        fclose(f);
4054    }
4055}
4056
4057
4058void srv_archive_qitem(const char *prefix, xs_dict *q_item)
4059/* archives a q_item in the error folder */
4060{
4061    xs *ntid = tid(0);
4062    xs *fn   = xs_fmt("%s/error/%s_qitem_%s", srv_basedir, ntid, prefix);
4063    FILE *f;
4064
4065    if ((f = fopen(fn, "w")) != NULL) {
4066        xs_json_dump(q_item, 4, f);
4067        fclose(f);
4068    }
4069}
4070
4071
4072t_announcement *announcement(const double after)
4073/* returns announcement text or NULL if none exists or it is olde than "after" */
4074{
4075    static const long int MAX_SIZE = 2048;
4076    static t_announcement a = {
4077        .text = NULL,
4078        .timestamp = 0.0,
4079    };
4080    static xs_str *fn = NULL;
4081    if (fn == NULL)
4082        fn = xs_fmt("%s/announcement.txt", srv_basedir);
4083
4084    const double ts = mtime(fn);
4085
4086    /* file does not exist or other than what was requested */
4087    if (ts == 0.0 || ts <= after)
4088        return NULL;
4089
4090    /* nothing changed, just return the current announcement */
4091    if (a.text != NULL && ts <= a.timestamp)
4092        return &a;
4093
4094    /* read and store new announcement */
4095    FILE *f;
4096
4097    if ((f = fopen(fn, "r")) != NULL) {
4098        fseek (f, 0, SEEK_END);
4099        const long int length = ftell(f);
4100
4101        if (length > MAX_SIZE) {
4102            /* this is probably unintentional */
4103            srv_log(xs_fmt("announcement.txt too big: %ld bytes, max is %ld, ignoring.", length, MAX_SIZE));
4104        }
4105        else
4106        if (length > 0) {
4107            fseek (f, 0, SEEK_SET);
4108            char *buffer = malloc(length + 1);
4109            if (buffer) {
4110                fread(buffer, 1, length, f);
4111                buffer[length] = '\0';
4112
4113                free(a.text);
4114                a.text = buffer;
4115                a.timestamp = ts;
4116            }
4117            else {
4118                srv_log("Error allocating memory for announcement");
4119            }
4120        }
4121        else {
4122            /* an empty file means no announcement */
4123            free(a.text);
4124            a.text = NULL;
4125            a.timestamp = 0.0;
4126        }
4127
4128        fclose (f);
4129    }
4130
4131    if (a.text != NULL)
4132        return &a;
4133
4134    return NULL;
4135}
4136
4137
4138xs_str *make_url(const char *href, const char *proxy, int by_token)
4139/* makes an URL, possibly including proxying */
4140{
4141    xs_str *url = NULL;
4142
4143    if (proxy && !xs_startswith(href, srv_baseurl)) {
4144        xs *p = NULL;
4145
4146        if (by_token) {
4147            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, proxy);
4148            xs *tk = xs_md5_hex(tks, strlen(tks));
4149
4150            p = xs_fmt("%s/y/%s/", proxy, tk);
4151        }
4152        else
4153            p = xs_fmt("%s/x/", proxy);
4154
4155        url = xs_replace(href, "https:/" "/", p);
4156    }
4157    else
4158        url = xs_dup(href);
4159
4160    return url;
4161}
4162
4163
4164/** bad login throttle **/
4165
4166xs_str *_badlogin_fn(const char *addr)
4167{
4168    xs *md5 = xs_md5_hex(addr, strlen(addr));
4169    xs *dir = xs_fmt("%s/badlogin", srv_basedir);
4170
4171    mkdirx(dir);
4172
4173    return xs_fmt("%s/%s", dir, md5);
4174}
4175
4176
4177int _badlogin_read(const char *fn, int *failures)
4178/* reads a badlogin file */
4179{
4180    int ok = 0;
4181    FILE *f;
4182
4183    pthread_mutex_lock(&data_mutex);
4184
4185    if ((f = fopen(fn, "r")) != NULL) {
4186        xs *l = xs_readline(f);
4187        fclose(f);
4188
4189        if (sscanf(l, "%d", failures) == 1)
4190            ok = 1;
4191    }
4192
4193    pthread_mutex_unlock(&data_mutex);
4194
4195    return ok;
4196}
4197
4198
4199int badlogin_check(const char *user, const char *addr)
4200/* checks if this address is authorized to try a login */
4201{
4202    int valid = 1;
4203
4204    if (xs_type(addr) == XSTYPE_STRING) {
4205        xs *fn = _badlogin_fn(addr);
4206        double mt = mtime(fn);
4207
4208        if (mt > 0) {
4209            int badlogin_expire = xs_number_get(xs_dict_get_def(srv_config,
4210                                        "badlogin_expire", "300"));
4211
4212            mt += badlogin_expire;
4213
4214            /* if file is expired, delete and give pass */
4215            if (mt < time(NULL)) {
4216                srv_debug(1, xs_fmt("Login from %s for %s allowed again", addr, user));
4217                unlink(fn);
4218            }
4219            else {
4220                int failures;
4221
4222                if (_badlogin_read(fn, &failures)) {
4223                    int badlogin_max = xs_number_get(xs_dict_get_def(srv_config,
4224                                            "badlogin_retries", "5"));
4225
4226                    if (failures >= badlogin_max) {
4227                        valid = 0;
4228
4229                        xs *d = xs_str_iso_date((time_t) mt);
4230
4231                        srv_debug(1,
4232                            xs_fmt("Login from %s for %s forbidden until %s", addr, user, d));
4233                    }
4234                }
4235            }
4236        }
4237    }
4238
4239    return valid;
4240}
4241
4242
4243void badlogin_inc(const char *user, const char *addr)
4244/* increments a bad login from this address */
4245{
4246    if (xs_type(addr) == XSTYPE_STRING) {
4247        int failures = 0;
4248        xs *fn = _badlogin_fn(addr);
4249        FILE *f;
4250
4251        _badlogin_read(fn, &failures);
4252
4253        pthread_mutex_lock(&data_mutex);
4254
4255        if ((f = fopen(fn, "w")) != NULL) {
4256            failures++;
4257
4258            fprintf(f, "%d %s %s\n", failures, addr, user);
4259            fclose(f);
4260
4261            srv_log(xs_fmt("Registered %d login failure(s) from %s for %s", failures, addr, user));
4262        }
4263
4264        pthread_mutex_unlock(&data_mutex);
4265    }
4266}
4267
4268
4269/** language strings **/
4270
4271const char *lang_str(const char *str, const snac *user)
4272/* returns a translated string */
4273{
4274    const char *n_str = str;
4275
4276    if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4277        n_str = xs_dict_get(user->lang, str);
4278
4279        if (xs_is_null(n_str) || *n_str == '\0')
4280            n_str = str;
4281    }
4282
4283    return n_str;
4284}