2.90 html.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
   3
   4#include "xs.h"
   5#include "xs_io.h"
   6#include "xs_json.h"
   7#include "xs_regex.h"
   8#include "xs_set.h"
   9#include "xs_openssl.h"
  10#include "xs_time.h"
  11#include "xs_mime.h"
  12#include "xs_match.h"
  13#include "xs_html.h"
  14#include "xs_curl.h"
  15#include "xs_unicode.h"
  16#include "xs_url.h"
  17#include "xs_random.h"
  18#include "xs_http.h"
  19#include "xs_list_tools.h"
  20
  21#include "snac.h"
  22
  23int login(snac *user, const xs_dict *headers)
  24/* tries a login */
  25{
  26    int logged_in = 0;
  27    const char *auth = xs_dict_get(headers, "authorization");
  28
  29    if (auth && xs_startswith(auth, "Basic ")) {
  30        int sz;
  31        xs *s1 = xs_crop_i(xs_dup(auth), 6, 0);
  32        xs *s2 = xs_base64_dec(s1, &sz);
  33
  34        xs *l1 = xs_split_n(s2, ":", 1);
  35
  36        if (xs_list_len(l1) == 2) {
  37            const char *uid  = xs_list_get(l1, 0);
  38            const char *pwd  = xs_list_get(l1, 1);
  39            const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
  40                                     xs_dict_get(headers, "x-forwarded-for"));
  41
  42            if (badlogin_check(uid, addr)) {
  43                logged_in = check_password(uid, pwd,
  44                    xs_dict_get(user->config, "passwd"));
  45
  46                if (!logged_in)
  47                    badlogin_inc(uid, addr);
  48            }
  49        }
  50    }
  51
  52    if (logged_in)
  53        lastlog_write(user, "web");
  54
  55    return logged_in;
  56}
  57
  58xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems,
  59                            const char *proxy, const xs_list *cl, const char *act_o)
  60/* replace but also adds a class list and an actor in its alt text.
  61 * Used for emoji reactions */
  62{
  63    if (!xs_is_null(tag)) {
  64        xs *tag_list = NULL;
  65        if (xs_type(tag) == XSTYPE_DICT) {
  66            /* not a list */
  67            tag_list = xs_list_new();
  68            tag_list = xs_list_append(tag_list, tag);
  69        } else {
  70            /* is a list */
  71            tag_list = xs_dup(tag);
  72        }
  73
  74        xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems);
  75        xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
  76        if (cl) {
  77            xs *l = xs_join(cl, " ");
  78            class = xs_str_cat(class, " ", l);
  79        }
  80
  81        int c = 0;
  82        const xs_val *v;
  83
  84        c = 0;
  85
  86        xs_set rep_emoji;
  87        xs_set_init(&rep_emoji);
  88
  89        while (xs_list_next(tag_list, &v, &c)) {
  90            const char *t = xs_dict_get(v, "type");
  91
  92            if (t && strcmp(t, "Emoji") == 0) {
  93                const char *n = xs_dict_get(v, "name");
  94                const xs_dict *i = xs_dict_get(v, "icon");
  95
  96                /* avoid repeated emojis (Misskey seems to return this) */
  97                if (xs_set_add(&rep_emoji, n) == 0)
  98                    continue;
  99
 100                if (xs_is_string(n) && xs_is_dict(i)) {
 101                    const char *u = xs_dict_get(i, "url");
 102                    const char *mt = xs_dict_get(i, "mediaType");
 103
 104                    if (xs_is_string(u)) {
 105                        // on akkoma instances mediaType is not present.
 106                        // but we need to to know if the image is an svg or not.
 107                        // for now, i just use the file extention, which may not be the most reliable...
 108                        if (!xs_is_string(mt))
 109                            mt = xs_mime_by_ext(u);
 110
 111                        xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n);
 112
 113                        if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
 114                            s = xs_replace_i(s, n, "");
 115                        else {
 116                            xs *url = make_url(u, proxy, 0);
 117
 118                            xs_html *img = xs_html_sctag("img",
 119                                xs_html_attr("loading", "lazy"),
 120                                xs_html_attr("src", url),
 121                                xs_html_attr("alt", act),
 122                                xs_html_attr("title", act),
 123                                xs_html_attr("class", class),
 124                                xs_html_attr("style", style));
 125
 126                            xs *s1 = xs_html_render(img);
 127                            s = xs_replace_i(s, n, s1);
 128                        }
 129                    }
 130                    else
 131                        s = xs_replace_i(s, n, "");
 132                }
 133            }
 134        }
 135
 136        xs_set_free(&rep_emoji);
 137    }
 138
 139    return s;
 140}
 141
 142
 143xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
 144/* replaces all the :shortnames: with the emojis in tag */
 145{
 146    return _replace_shortnames(s, tag, ems, proxy, NULL, NULL);
 147}
 148
 149
 150xs_str *actor_name(xs_dict *actor, const char *proxy)
 151/* gets the actor name */
 152{
 153    const char *v;
 154
 155    if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
 156        if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
 157            v = "anonymous";
 158        }
 159    }
 160
 161    return replace_shortnames(xs_html_encode(v), xs_dict_get(actor, "tag"), 1, proxy);
 162}
 163
 164
 165xs_str *actor_pronouns(xs_dict *actor)
 166/* gets the actor name */
 167{
 168    const xs_list *attachment;
 169    const xs_dict *d;
 170    const char *v;
 171    const char *pronouns = "";
 172    xs_str *ret;
 173
 174    if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) {
 175        xs_list_foreach(attachment, d) {
 176            xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name"));
 177            /* make sure that we are reading the correct metadata */
 178            if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) {
 179                /* safeguard from NULL values */
 180                v = xs_dict_get(d, "value");
 181                pronouns = v ? v : pronouns;
 182                break;
 183            }
 184        }
 185    }
 186
 187    /* strip all HTML tags */
 188    ret = xs_regex_replace(pronouns, "</?[^>]+>", "");
 189    return ret;
 190}
 191
 192
 193xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
 194/* needed when we have local text with no tags attached */
 195{
 196    xs *tags = xs_list_new();
 197    xs *name1 = not_really_markdown(text, NULL, &tags);
 198
 199    xs_str *name3;
 200    if (user) {
 201        xs *name2 = process_tags(user, name1, &tags);
 202        name3 = sanitize(name2);
 203    }
 204    else {
 205        name3 = sanitize(name1);
 206        name3 = xs_replace_i(name3, "<br>", "");
 207    }
 208
 209    return replace_shortnames(name3, tags, ems, proxy);
 210}
 211
 212
 213xs_str *html_date_label(snac *user, const char *date)
 214{
 215    time_t t;
 216
 217    /* check if a user has actually set a timezone */
 218    if (user != NULL && xs_dict_get(user->config, "tz") != NULL &&
 219        (t = xs_parse_iso_date(date, 0)) != 0) {
 220        t += xs_tz_offset(user->tz);
 221
 222        time_t today = time(NULL);
 223        if (today - t >= 3600 * 24 * 30 * 6) {
 224            /* more than half a year ago */
 225            return xs_str_utctime(t, "%Y-%m-%d %H:%M");
 226        }
 227
 228        xs_str *date = xs_str_utctime(t, "%b%d %H:%M");
 229
 230        struct tm tm;
 231        localtime_r(&t, &tm);
 232
 233        if (tm.tm_mon >= 0 && tm.tm_mon <= 11) {
 234            /* copy the 3letter translated month name */
 235            const char *m = L(months[tm.tm_mon]);
 236
 237            if (xs_is_string(m))
 238                date = xs_replace_i(date, months[tm.tm_mon], m);
 239        }
 240
 241        return date;
 242    }
 243
 244    return xs_crop_i(xs_dup(date), 0, 10);
 245}
 246
 247
 248xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
 249                        const char *udate, const char *url, int scope,
 250                        int in_people, const char *proxy, const char *lang,
 251                        const char *md5)
 252{
 253    xs_html *actor_icon = xs_html_tag("p", NULL);
 254
 255    xs *avatar = NULL;
 256    const char *v;
 257    int fwing = 0;
 258    int fwer = 0;
 259
 260    xs *name = actor_name(actor, proxy);
 261    xs *pronouns = actor_pronouns(actor);
 262
 263    /* get the avatar */
 264    if ((v = xs_dict_get(actor, "icon")) != NULL) {
 265        /* if it's a list (Peertube), get the first one */
 266        if (xs_type(v) == XSTYPE_LIST)
 267            v = xs_list_get(v, 0);
 268
 269        if ((v = xs_dict_get(v, "url")) != NULL)
 270            avatar = make_url(v, proxy, 0);
 271    }
 272
 273    if (avatar == NULL)
 274        avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
 275
 276    const char *actor_id = xs_dict_get(actor, "id");
 277    const char *html_url = xs_dict_get_def(actor, "url", actor_id);
 278
 279    if (xs_is_list(html_url))
 280        html_url = xs_list_get(html_url, 0);
 281
 282    if (!xs_is_string(html_url))
 283        html_url = actor_id;
 284
 285    xs *href = NULL;
 286
 287    if (user) {
 288        fwer = follower_check(user, actor_id);
 289        fwing = following_check(user, actor_id);
 290    }
 291
 292    if (user && !in_people) {
 293        xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
 294        href = xs_fmt("%s/people/%s", user->actor, md5);
 295    }
 296
 297    if (href == NULL)
 298        href = xs_dup(html_url);
 299
 300    xs_html *name_link = xs_html_tag("a",
 301            xs_html_attr("href",    href),
 302            xs_html_attr("class",   "p-author h-card snac-author"),
 303            xs_html_raw(name)); /* name is already html-escaped */
 304
 305    if (*pronouns) {
 306        xs_html_add(name_link,
 307            xs_html_text(" ["),
 308            xs_html_tag("span",
 309                xs_html_attr("class",   "snac-pronouns"),
 310                xs_html_attr("title",   "user's pronouns"),
 311                xs_html_raw(pronouns)),
 312            xs_html_text("]"));
 313    }
 314
 315    xs_html_add(actor_icon,
 316        xs_html_sctag("img",
 317            xs_html_attr("loading", "lazy"),
 318            xs_html_attr("class",   "snac-avatar"),
 319            xs_html_attr("src",     avatar),
 320            xs_html_attr("alt",     "[?]")),
 321        name_link);
 322
 323    if (!xs_is_null(url)) {
 324        xs *md5 = xs_md5_hex(url, strlen(url));
 325
 326        xs_html_add(actor_icon,
 327            xs_html_text(" "),
 328            xs_html_tag("a",
 329                xs_html_attr("href", (char *)url),
 330                xs_html_attr("title", md5),
 331                xs_html_text("»")));
 332    }
 333
 334
 335    if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
 336        xs_html_add(actor_icon,
 337            xs_html_text(" "),
 338            xs_html_tag("span",
 339                xs_html_attr("title", "bot"),
 340                xs_html_raw("&#129302;")));
 341    }
 342
 343    if (fwing && fwer) {
 344        xs_html_add(actor_icon,
 345            xs_html_text(" "),
 346            xs_html_tag("span",
 347                xs_html_attr("title", "mutual relation"),
 348                xs_html_raw("&#129309;")));
 349    }
 350
 351    if (instance_failure(actor_id, 0) == -1) {
 352        xs_html_add(actor_icon,
 353            xs_html_text(" "),
 354            xs_html_tag("span",
 355                xs_html_attr("title", "broken instance"),
 356                xs_html_raw("&#128148;")));
 357    }
 358
 359    if (actor_failure(actor_id, 0) == -1) {
 360        xs_html_add(actor_icon,
 361            xs_html_text(" "),
 362            xs_html_tag("span",
 363                xs_html_attr("title", "account no longer exists"),
 364                xs_html_raw("&#128128;")));
 365    }
 366
 367    if (scope != -1) {
 368        if (scope == SCOPE_FOLLOWERS) {
 369            xs_html_add(actor_icon,
 370                xs_html_text(" "),
 371                xs_html_tag("span",
 372                    xs_html_attr("title", "followers"),
 373                    xs_html_raw("&#128274;"))); // emoji of a lock
 374        }
 375        else if (scope == SCOPE_PUBLIC) {
 376            xs_html_add(actor_icon,
 377                xs_html_text(" "),
 378                xs_html_tag("span",
 379                    xs_html_attr("title", "public"),
 380                    xs_html_raw("&#127760;"))); // emoji of a globe
 381        }
 382        else if (scope == SCOPE_UNLISTED) {
 383            xs_html_add(actor_icon,
 384                xs_html_text(" "),
 385                xs_html_tag("span",
 386                    xs_html_attr("title", "unlisted"),
 387                    xs_html_raw("&#128275;"))); // emoji of an unlocked lock
 388        }
 389        else if (scope == SCOPE_MENTIONED) {
 390            xs_html_add(actor_icon,
 391                xs_html_text(" "),
 392                xs_html_tag("span",
 393                    xs_html_attr("title", "mentioned"),
 394                    xs_html_raw("&#9993;&#65039;"))); // emoji of a mail
 395        }
 396    }
 397
 398    if (xs_is_null(date)) {
 399        xs_html_add(actor_icon,
 400            xs_html_raw("&nbsp;"));
 401    }
 402    else {
 403        xs *date_label = html_date_label(user, date);
 404        xs *date_title = xs_dup(date);
 405
 406        if (!xs_is_null(udate)) {
 407            xs *sd = html_date_label(user, udate);
 408
 409            date_label = xs_str_cat(date_label, " / ", sd);
 410
 411            date_title = xs_str_cat(date_title, " / ", udate);
 412        }
 413
 414        if (xs_is_string(lang))
 415            date_title = xs_str_cat(date_title, " (", lang, ")");
 416
 417        xs_html *date_text = xs_html_text(date_label);
 418
 419        if (user && md5) {
 420            xs *lpost_url = xs_fmt("%s/admin/p/%s#%s_entry",
 421                                   user->actor, md5, md5);
 422            date_text = xs_html_tag("a",
 423                                    xs_html_attr("href", lpost_url),
 424                                    xs_html_attr("class", "snac-pubdate"),
 425                                    date_text);
 426        }
 427        else if (user && url) {
 428            xs *lpost_url = xs_fmt("%s/admin?q=%s",
 429                                   user->actor, xs_url_enc(url));
 430            date_text = xs_html_tag("a",
 431                                    xs_html_attr("href", lpost_url),
 432                                    xs_html_attr("class", "snac-pubdate"),
 433                                    date_text);
 434        }
 435
 436        xs_html_add(actor_icon,
 437            xs_html_text(" "),
 438            xs_html_tag("time",
 439                xs_html_attr("class", "dt-published snac-pubdate"),
 440                xs_html_attr("title", date_title),
 441                date_text));
 442    }
 443
 444    {
 445        const char *username, *id;
 446
 447        if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
 448            /* This should never be reached */
 449            username = "anonymous";
 450        }
 451
 452        if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') {
 453            /* This should never be reached */
 454            id = "https://social.example.org/anonymous";
 455        }
 456
 457        /* "LIKE AN ANIMAL" */
 458        xs *domain = xs_split(id, "/");
 459        xs *user   = xs_fmt("@%s@%s", username, xs_list_get(domain, 2));
 460
 461        xs_html_add(actor_icon,
 462            xs_html_sctag("br", NULL),
 463            xs_html_tag("a",
 464                xs_html_attr("href",  html_url),
 465                xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
 466                xs_html_text(user)));
 467    }
 468
 469    return actor_icon;
 470}
 471
 472
 473xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
 474                       const char *proxy, const char *md5, const char *lang)
 475{
 476    xs *actor = NULL;
 477    xs_html *actor_icon = NULL;
 478
 479    if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
 480        const char *date  = NULL;
 481        const char *udate = NULL;
 482        const char *url   = NULL;
 483        const char *type = xs_dict_get(msg, "type");
 484        const int scope = get_msg_visibility(msg);
 485
 486        if (xs_match(type, POSTLIKE_OBJECT_TYPE))
 487            url = xs_dict_get(msg, "id");
 488
 489
 490        date  = xs_dict_get(msg, "published");
 491        udate = xs_dict_get(msg, "updated");
 492
 493        actor_icon = html_actor_icon(user, actor, date, udate, url, scope, 0, proxy, lang, md5);
 494    }
 495
 496    return actor_icon;
 497}
 498
 499void html_note_render_visibility(snac* user, xs_html *form, const int scope)
 500{
 501    // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here
 502    static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
 503    static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
 504    static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
 505    static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1};
 506    static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes};
 507    static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"};
 508    static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"};
 509
 510    xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
 511    const int* to_render = scopes[scope];
 512    for( int i = 0; to_render[i] != -1; i++ ) {
 513        const int scope_i = to_render[i];
 514        const char* value = scopes_tags[scope_i];
 515        const char* name = scopes_names[scope_i];
 516        xs_html_add(paragraph,
 517            xs_html_tag("label",
 518                xs_html_sctag("input",
 519                    xs_html_attr("type", "radio"),
 520                    xs_html_attr("name", "visibility"),
 521                    xs_html_attr("value", value),
 522                    xs_html_attr(scope == scope_i ? "checked" : "", NULL)),
 523                xs_html_text(" "),
 524                xs_html_text(L(name)),
 525                xs_html_text(" "))
 526        );
 527    }
 528    xs_html_add(form, paragraph);
 529}
 530
 531/* html_note but moddled for emoji's needs. here and not bellow, since the
 532 * other one is already so complex. */
 533xs_html *html_emoji(snac *user, const char *summary,
 534                   const char *div_id, const char *form_id,
 535                   const char* placeholder, const char *post_id,
 536                   const char* eid)
 537{
 538    xs *action = xs_fmt("%s/admin/action", user->actor);
 539
 540    xs_html *form;
 541    const int react = eid == NULL ? 0 : 1;
 542
 543    xs_html *note = xs_html_tag("div",
 544        xs_html_tag("details",
 545            xs_html_tag("summary",
 546                xs_html_text(summary)),
 547                xs_html_tag("p", NULL),
 548                xs_html_tag("div",
 549                    xs_html_attr("class", "snac-note"),
 550                    xs_html_attr("id",    div_id),
 551                    form = xs_html_tag("form",
 552                        xs_html_attr("autocomplete", "off"),
 553                        xs_html_attr("method",       "post"),
 554                        xs_html_attr("action",       action),
 555                        xs_html_attr("enctype",      "multipart/form-data"),
 556                        xs_html_attr("id",           form_id),
 557                        xs_html_sctag("input",
 558                            xs_html_attr("type",     "hidden"),
 559                            xs_html_attr("name",     "id"),
 560                            xs_html_attr("value",    post_id)),
 561                        xs_html_sctag("input",
 562                            xs_html_attr("type",     react ? "hidden" : "text"),
 563                            xs_html_attr("name",     "eid"),
 564                            xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)),
 565                        xs_html_text(" "),
 566                        xs_html_sctag("input",
 567                            xs_html_attr("type",    "submit"),
 568                            xs_html_attr("name",    "action"),
 569                            xs_html_attr("eid",    "action"),
 570                            xs_html_attr("value",   react ? L("EmojiUnreact") : L("EmojiReact"))),
 571                        xs_html_text(" "),
 572                    xs_html_tag("p", NULL)))));
 573
 574    return note;
 575}
 576
 577xs_html *html_note(snac *user, const char *summary,
 578                   const char *div_id, const char *form_id,
 579                   const char *ta_plh, const char *ta_content,
 580                   const char *edit_id, const char *actor_id,
 581                   const xs_val *cw_yn, const char *cw_text,
 582                   const int scope, const char *redir,
 583                   const char *in_reply_to, int poll,
 584                   const xs_list *att_files, const xs_list *att_alt_texts,
 585                   int is_draft, const char *published,
 586                   const char *note_lang)
 587/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
 588{
 589    xs *action = xs_fmt("%s/admin/note", user->actor);
 590
 591    xs_html *form;
 592
 593    xs_html *note = xs_html_tag("div",
 594        xs_html_tag("details",
 595            xs_html_tag("summary",
 596                xs_html_text(summary)),
 597                xs_html_tag("p", NULL),
 598                xs_html_tag("div",
 599                    xs_html_attr("class", "snac-note"),
 600                    xs_html_attr("id",    div_id),
 601                    form = xs_html_tag("form",
 602                        xs_html_attr("autocomplete", "off"),
 603                        xs_html_attr("method",       "post"),
 604                        xs_html_attr("action",       action),
 605                        xs_html_attr("enctype",      "multipart/form-data"),
 606                        xs_html_attr("id",           form_id),
 607                        xs_html_tag("textarea",
 608                            xs_html_attr("class",    "snac-textarea"),
 609                            xs_html_attr("name",     "content"),
 610                            xs_html_attr("rows",     "4"),
 611                            xs_html_attr("wrap",     "virtual"),
 612                            xs_html_attr("required", "required"),
 613                            xs_html_attr("placeholder", ta_plh),
 614                            xs_html_text(ta_content)),
 615                        xs_html_tag("p", NULL),
 616                        xs_html_text(L("Sensitive content: ")),
 617                        xs_html_sctag("input",
 618                            xs_html_attr("type", "checkbox"),
 619                            xs_html_attr("name", "sensitive"),
 620                            xs_html_attr(xs_type(cw_yn) == XSTYPE_TRUE ? "checked" : "", NULL)),
 621                        xs_html_sctag("input",
 622                            xs_html_attr("type", "text"),
 623                            xs_html_attr("name", "summary"),
 624                            xs_html_attr("placeholder", L("Sensitive content description")),
 625                            xs_html_attr("value", xs_is_null(cw_text) ? "" : cw_text))))));
 626
 627    if (actor_id)
 628        xs_html_add(form,
 629            xs_html_sctag("input",
 630                xs_html_attr("type",  "hidden"),
 631                xs_html_attr("name",  "to"),
 632                xs_html_attr("value", actor_id)));
 633    if (edit_id == NULL)
 634        html_note_render_visibility(user, form, scope);
 635
 636    if (redir)
 637        xs_html_add(form,
 638            xs_html_sctag("input",
 639                xs_html_attr("type",  "hidden"),
 640                xs_html_attr("name",  "redir"),
 641                xs_html_attr("value", redir)));
 642
 643    if (in_reply_to)
 644        xs_html_add(form,
 645            xs_html_sctag("input",
 646                xs_html_attr("type",  "hidden"),
 647                xs_html_attr("name",  "in_reply_to"),
 648                xs_html_attr("value", in_reply_to)));
 649    else
 650        xs_html_add(form,
 651            xs_html_tag("p", NULL),
 652            xs_html_text(L("Reply to (URL): ")),
 653            xs_html_sctag("input",
 654                xs_html_attr("type",     "url"),
 655                xs_html_attr("name",     "in_reply_to"),
 656                xs_html_attr("placeholder", L("Optional URL to reply to"))));
 657
 658    xs_html_add(form,
 659        xs_html_tag("p", NULL),
 660        xs_html_tag("span",
 661            xs_html_attr("title", L("Don't send, but store as a draft")),
 662            xs_html_text(L("Draft:")),
 663            xs_html_sctag("input",
 664                xs_html_attr("type", "checkbox"),
 665                xs_html_attr("name", "is_draft"),
 666                xs_html_attr(is_draft ? "checked" : "", NULL))));
 667
 668    const char *post_langs = xs_dict_get(user->config, "post_langs");
 669
 670    if (xs_is_string(post_langs) && *post_langs) {
 671        xs *ll = xs_split(post_langs, " ");
 672        const char *lang;
 673
 674        xs_html *post_lang = xs_html_tag("select",
 675            xs_html_attr("name", "post_lang"));
 676
 677        xs_list_foreach(ll, lang) {
 678            if (*lang) {
 679                if (xs_is_string(note_lang) && strcmp(lang, note_lang) == 0) {
 680                    xs_html_add(post_lang,
 681                        xs_html_tag("option",
 682                            xs_html_text(lang),
 683                            xs_html_attr("selected", NULL),
 684                            xs_html_attr("value", lang)));
 685                }
 686                else {
 687                    xs_html_add(post_lang,
 688                        xs_html_tag("option",
 689                            xs_html_text(lang),
 690                            xs_html_attr("value", lang)));
 691                }
 692            }
 693        }
 694
 695        xs_html_add(form,
 696            xs_html_tag("p", NULL),
 697            xs_html_text(L("Language:")),
 698            xs_html_text(" "),
 699            post_lang);
 700    }
 701
 702    /* post date and time */
 703    xs *post_date = NULL;
 704    xs *post_time = NULL;
 705
 706    if (xs_is_string(published)) {
 707        time_t t = xs_parse_iso_date(published, 0);
 708
 709        if (t > 0) {
 710            post_date = xs_str_time(t, "%Y-%m-%d", 1);
 711            post_time = xs_str_time(t, "%H:%M:%S", 1);
 712        }
 713    }
 714
 715    if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
 716        xs *pdat = xs_fmt(L("Post date and time (timezone: %s):"), user->tz);
 717
 718        xs_html_add(form,
 719            xs_html_tag("p",
 720                xs_html_tag("details",
 721                    xs_html_tag("summary",
 722                        xs_html_text(L("Scheduled post..."))),
 723                    xs_html_tag("p",
 724                        xs_html_text(pdat),
 725                        xs_html_sctag("br", NULL),
 726                        xs_html_sctag("input",
 727                            xs_html_attr("type",  "date"),
 728                            xs_html_attr("value", post_date ? post_date : ""),
 729                            xs_html_attr("name",  "post_date")),
 730                        xs_html_text(" "),
 731                        xs_html_sctag("input",
 732                            xs_html_attr("type",  "time"),
 733                            xs_html_attr("value", post_time ? post_time : ""),
 734                            xs_html_attr("step",  "1"),
 735                            xs_html_attr("name",  "post_time"))))));
 736    }
 737
 738    if (edit_id)
 739        xs_html_add(form,
 740            xs_html_sctag("input",
 741                xs_html_attr("type",  "hidden"),
 742                xs_html_attr("name",  "edit_id"),
 743                xs_html_attr("value", edit_id)));
 744
 745    /* attachment controls */
 746    xs_html *att;
 747
 748    xs_html_add(form,
 749        xs_html_tag("p", NULL),
 750        att = xs_html_tag("details",
 751            xs_html_tag("summary",
 752                xs_html_text(L("Attachments..."))),
 753            xs_html_tag("p", NULL)));
 754
 755    int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
 756    int att_n = 0;
 757
 758    /* fields for the currently existing attachments */
 759    if (xs_is_list(att_files) && xs_is_list(att_alt_texts)) {
 760        while (att_n < max_attachments) {
 761            const char *att_file = xs_list_get(att_files, att_n);
 762            const char *att_alt_text = xs_list_get(att_alt_texts, att_n);
 763
 764            if (!xs_is_string(att_file) || !xs_is_string(att_alt_text))
 765                break;
 766
 767            xs *att_lbl = xs_fmt("attach_url_%d", att_n);
 768            xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
 769
 770            if (att_n)
 771                xs_html_add(att,
 772                    xs_html_sctag("br", NULL));
 773
 774            xs_html_add(att,
 775                xs_html_text(L("File:")),
 776                xs_html_sctag("input",
 777                    xs_html_attr("type", "text"),
 778                    xs_html_attr("name", att_lbl),
 779                    xs_html_attr("title", L("Clear this field to delete the attachment")),
 780                    xs_html_attr("value", att_file)));
 781
 782            xs_html_add(att,
 783                xs_html_text(" "),
 784                xs_html_sctag("input",
 785                    xs_html_attr("type",    "text"),
 786                    xs_html_attr("name",    alt_lbl),
 787                    xs_html_attr("value",   att_alt_text),
 788                    xs_html_attr("placeholder", L("Attachment description"))));
 789
 790            att_n++;
 791        }
 792    }
 793
 794    /* the rest of possible attachments */
 795    while (att_n < max_attachments) {
 796        xs *att_lbl = xs_fmt("attach_%d", att_n);
 797        xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
 798
 799        if (att_n)
 800            xs_html_add(att,
 801                xs_html_sctag("br", NULL));
 802
 803        xs_html_add(att,
 804            xs_html_sctag("input",
 805                xs_html_attr("type",    "file"),
 806                xs_html_attr("name",    att_lbl)));
 807
 808        xs_html_add(att,
 809            xs_html_text(" "),
 810            xs_html_sctag("input",
 811                xs_html_attr("type",    "text"),
 812                xs_html_attr("name",    alt_lbl),
 813                xs_html_attr("placeholder", L("Attachment description"))));
 814
 815        att_n++;
 816    }
 817
 818    /* add poll controls */
 819    if (poll) {
 820        const xs_number *max_options = xs_dict_get(srv_config, "max_poll_options");
 821        xs *poll_limit_str = xs_dup(L("Poll options (one per line, up to 8):"));
 822        if (max_options != NULL)
 823            poll_limit_str = xs_replace_i(poll_limit_str, "8", xs_number_str(max_options));
 824
 825        xs_html_add(form,
 826            xs_html_tag("p", NULL),
 827            xs_html_tag("details",
 828                xs_html_tag("summary",
 829                    xs_html_text(L("Poll..."))),
 830                xs_html_tag("p",
 831                    xs_html_text(poll_limit_str),
 832                    xs_html_sctag("br", NULL),
 833                    xs_html_tag("textarea",
 834                        xs_html_attr("class",    "snac-textarea"),
 835                        xs_html_attr("name",     "poll_options"),
 836                        xs_html_attr("rows",     "4"),
 837                        xs_html_attr("wrap",     "virtual"),
 838                        xs_html_attr("placeholder", L("Option 1...\nOption 2...\nOption 3...\n...")))),
 839                xs_html_tag("select",
 840                    xs_html_attr("name",    "poll_multiple"),
 841                    xs_html_tag("option",
 842                        xs_html_attr("value", "off"),
 843                        xs_html_text(L("One choice"))),
 844                    xs_html_tag("option",
 845                        xs_html_attr("value", "on"),
 846                        xs_html_text(L("Multiple choices")))),
 847                xs_html_text(" "),
 848                xs_html_tag("select",
 849                    xs_html_attr("name",    "poll_end_secs"),
 850                    xs_html_tag("option",
 851                        xs_html_attr("value", "300"),
 852                        xs_html_text(L("End in 5 minutes"))),
 853                    xs_html_tag("option",
 854                        xs_html_attr("value", "3600"),
 855                        xs_html_attr("selected", NULL),
 856                        xs_html_text(L("End in 1 hour"))),
 857                    xs_html_tag("option",
 858                        xs_html_attr("value", "86400"),
 859                        xs_html_text(L("End in 1 day"))),
 860                    xs_html_tag("option",
 861                        xs_html_attr("value", "259200"),
 862                        xs_html_text(L("End in 3 days"))),
 863                    xs_html_tag("option",
 864                        xs_html_attr("value", "31536000"),
 865                        xs_html_text(L("End in 1 year"))))));
 866    }
 867
 868    xs_html_add(form,
 869        xs_html_tag("p", NULL),
 870        xs_html_sctag("input",
 871            xs_html_attr("type",     "submit"),
 872            xs_html_attr("class",    "button"),
 873            xs_html_attr("value",    L("Post"))),
 874        xs_html_tag("p", NULL));
 875
 876    return note;
 877}
 878
 879
 880static xs_html *html_base_head(void)
 881{
 882    xs_html *head = xs_html_tag("head",
 883        xs_html_sctag("meta",
 884            xs_html_attr("name", "viewport"),
 885            xs_html_attr("content", "width=device-width, initial-scale=1")),
 886        xs_html_sctag("meta",
 887            xs_html_attr("name", "generator"),
 888            xs_html_attr("content", USER_AGENT)));
 889
 890    /* add server CSS and favicon */
 891    xs *f = NULL;
 892    const char *favicon = xs_dict_get(srv_config, "favicon_url");
 893
 894    if (xs_is_string(favicon))
 895        f = xs_dup(favicon);
 896    else
 897        f = xs_fmt("%s/favicon.ico", srv_baseurl);
 898
 899    const xs_list *p = xs_dict_get(srv_config, "cssurls");
 900    const char *v;
 901
 902    xs_list_foreach(p, v) {
 903        xs_html_add(head,
 904            xs_html_sctag("link",
 905                xs_html_attr("rel",  "stylesheet"),
 906                xs_html_attr("type", "text/css"),
 907                xs_html_attr("href", v)));
 908    }
 909
 910    xs_html_add(head,
 911        xs_html_sctag("link",
 912            xs_html_attr("rel",  "icon"),
 913            xs_html_attr("type", "image/x-icon"),
 914            xs_html_attr("href", f)));
 915
 916    return head;
 917}
 918
 919
 920xs_html *html_instance_head(void)
 921{
 922    xs_html *head = html_base_head();
 923
 924    {
 925        FILE *f;
 926        xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
 927
 928        if ((f = fopen(g_css_fn, "r")) != NULL) {
 929            xs *css = xs_readall(f);
 930            fclose(f);
 931
 932            xs_html_add(head,
 933                xs_html_tag("style",
 934                    xs_html_raw(css)));
 935        }
 936    }
 937
 938    const char *host  = xs_dict_get(srv_config, "host");
 939    const char *title = xs_dict_get(srv_config, "title");
 940
 941    xs_html_add(head,
 942        xs_html_tag("title",
 943            xs_html_text(title && *title ? title : host)));
 944
 945    return head;
 946}
 947
 948
 949static xs_html *html_instance_body(void)
 950{
 951    const char *host     = xs_dict_get(srv_config, "host");
 952    const char *sdesc    = xs_dict_get(srv_config, "short_description");
 953    const char *sdescraw = xs_dict_get(srv_config, "short_description_raw");
 954    const char *email    = xs_dict_get(srv_config, "admin_email");
 955    const char *acct     = xs_dict_get(srv_config, "admin_account");
 956
 957    /* for L() */
 958    const snac *user = NULL;
 959
 960    xs *blurb = xs_replace(snac_blurb, "%host%", host);
 961
 962    xs_html *dl;
 963
 964    xs_html *body = xs_html_tag("body",
 965        xs_html_tag("div",
 966            xs_html_attr("class", "snac-instance-blurb"),
 967            xs_html_raw(blurb), /* pure html */
 968            dl = xs_html_tag("dl", NULL)));
 969
 970    if (sdesc && *sdesc) {
 971        if (!xs_is_null(sdescraw) && xs_type(sdescraw) == XSTYPE_TRUE) {
 972            xs_html_add(dl,
 973                xs_html_tag("di",
 974                    xs_html_tag("dt",
 975                        xs_html_text(L("Site description"))),
 976                    xs_html_tag("dd",
 977                        xs_html_raw(sdesc))));
 978        } else {
 979            xs_html_add(dl,
 980                xs_html_tag("di",
 981                    xs_html_tag("dt",
 982                        xs_html_text(L("Site description"))),
 983                    xs_html_tag("dd",
 984                        xs_html_text(sdesc))));
 985        }
 986    }
 987    if (email && *email) {
 988        xs *mailto = xs_fmt("mailto:%s", email);
 989
 990        xs_html_add(dl,
 991            xs_html_tag("di",
 992                xs_html_tag("dt",
 993                    xs_html_text(L("Admin email"))),
 994                xs_html_tag("dd",
 995                    xs_html_tag("a",
 996                        xs_html_attr("href", mailto),
 997                        xs_html_text(email)))));
 998    }
 999    if (acct && *acct) {
1000        xs *url    = xs_fmt("%s/%s", srv_baseurl, acct);
1001        xs *handle = xs_fmt("@%s@%s", acct, host);
1002
1003        xs_html_add(dl,
1004            xs_html_tag("di",
1005                xs_html_tag("dt",
1006                    xs_html_text(L("Admin account"))),
1007                xs_html_tag("dd",
1008                    xs_html_tag("a",
1009                        xs_html_attr("href", url),
1010                        xs_html_text(handle)))));
1011    }
1012
1013    return body;
1014}
1015
1016
1017xs_html *html_user_head(snac *user, const char *desc, const char *url)
1018{
1019    xs_html *head = html_base_head();
1020
1021    /* add the user CSS */
1022    {
1023        xs *css = NULL;
1024        int size;
1025
1026        /* try to open the user css */
1027        if (!valid_status(static_get(user, "style.css", &css, &size, NULL, NULL))) {
1028            /* it's not there; try to open the server-wide css */
1029            FILE *f;
1030            xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
1031
1032            if ((f = fopen(g_css_fn, "r")) != NULL) {
1033                css = xs_readall(f);
1034                fclose(f);
1035            }
1036        }
1037
1038        if (css != NULL) {
1039            xs_html_add(head,
1040                xs_html_tag("style",
1041                    xs_html_raw(css)));
1042        }
1043    }
1044
1045    /* title */
1046    xs *title = xs_fmt("%s (@%s@%s)", xs_dict_get(user->config, "name"),
1047        user->uid, xs_dict_get(srv_config, "host"));
1048
1049    xs_html_add(head,
1050        xs_html_tag("title",
1051            xs_html_text(title)));
1052
1053    xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
1054
1055    if (avatar == NULL || *avatar == '\0') {
1056        xs_free(avatar);
1057        avatar = xs_fmt("%s/susie.png", srv_baseurl);
1058    }
1059
1060    /* create a description field */
1061    xs *s_desc = NULL;
1062    int n;
1063
1064    if (desc == NULL)
1065        s_desc = xs_dup(xs_dict_get(user->config, "bio"));
1066    else
1067        s_desc = xs_dup(desc);
1068
1069    /* show metrics in og:description? */
1070    if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
1071        xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
1072
1073        s1 = xs_str_cat(s1, " · ");
1074
1075        s_desc = xs_str_prepend_i(s_desc, s1);
1076    }
1077
1078    /* shorten desc to a reasonable size */
1079    for (n = 0; s_desc[n]; n++) {
1080        if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
1081            break;
1082    }
1083
1084    s_desc[n] = '\0';
1085
1086    /* og properties */
1087    xs_html_add(head,
1088        xs_html_sctag("meta",
1089            xs_html_attr("property", "og:site_name"),
1090            xs_html_attr("content", xs_dict_get(srv_config, "host"))),
1091        xs_html_sctag("meta",
1092            xs_html_attr("property", "og:title"),
1093            xs_html_attr("content", title)),
1094        xs_html_sctag("meta",
1095            xs_html_attr("property", "og:description"),
1096            xs_html_attr("content", s_desc)),
1097        xs_html_sctag("meta",
1098            xs_html_attr("property", "og:image"),
1099            xs_html_attr("content", avatar)),
1100        xs_html_sctag("meta",
1101            xs_html_attr("property", "og:width"),
1102            xs_html_attr("content", "300")),
1103        xs_html_sctag("meta",
1104            xs_html_attr("property", "og:height"),
1105            xs_html_attr("content", "300")));
1106
1107    /* RSS link */
1108    xs *rss_url = xs_fmt("%s.rss", user->actor);
1109    xs_html_add(head,
1110        xs_html_sctag("link",
1111            xs_html_attr("rel", "alternate"),
1112            xs_html_attr("type", "application/rss+xml"),
1113            xs_html_attr("title", "RSS"),
1114            xs_html_attr("href", rss_url)));
1115
1116    /* ActivityPub alternate link (actor id) */
1117    xs_html_add(head,
1118        xs_html_sctag("link",
1119            xs_html_attr("rel", "alternate"),
1120            xs_html_attr("type", "application/activity+json"),
1121            xs_html_attr("href", url ? url : user->actor)));
1122
1123    /* webmention hook */
1124    xs *wbh = xs_fmt("%s/webmention-hook", srv_baseurl);
1125
1126    xs_html_add(head,
1127        xs_html_sctag("link",
1128            xs_html_attr("rel", "webmention"),
1129            xs_html_attr("href", wbh)));
1130
1131    return head;
1132}
1133
1134
1135static xs_html *html_user_body(snac *user, int read_only)
1136{
1137    const char *proxy = NULL;
1138
1139    if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
1140        proxy = user->actor;
1141
1142    xs_html *body = xs_html_tag("body", NULL);
1143
1144    /* top nav */
1145    xs_html *top_nav = xs_html_tag("nav",
1146        xs_html_attr("class", "snac-top-nav"));
1147
1148    xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
1149
1150    if (avatar == NULL || *avatar == '\0') {
1151        xs_free(avatar);
1152        avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
1153    }
1154
1155    xs_html_add(top_nav,
1156        xs_html_sctag("img",
1157            xs_html_attr("src", avatar),
1158            xs_html_attr("class", "snac-avatar"),
1159            xs_html_attr("alt", "")));
1160
1161    if (read_only) {
1162        xs *rss_url = xs_fmt("%s.rss", user->actor);
1163        xs *admin_url = xs_fmt("%s/admin", user->actor);
1164
1165        xs_html_add(top_nav,
1166            xs_html_tag("a",
1167                xs_html_attr("href", rss_url),
1168                xs_html_text(L("RSS"))),
1169            xs_html_text(" - "),
1170            xs_html_tag("a",
1171                xs_html_attr("href", admin_url),
1172                xs_html_attr("rel", "nofollow"),
1173                xs_html_text(L("private"))));
1174    }
1175    else {
1176        int n_len = notify_new_num(user);
1177        int p_len = pending_count(user);
1178        xs_html *notify_count = NULL;
1179        xs_html *pending_follow_count = NULL;
1180
1181        /* show the number of new notifications, if there are any */
1182        if (n_len) {
1183            xs *n_len_str = xs_fmt(" %d ", n_len);
1184            notify_count = xs_html_tag("sup",
1185                xs_html_attr("style", "background-color: red; color: white;"),
1186                xs_html_text(n_len_str));
1187        }
1188        else
1189            notify_count = xs_html_text("");
1190
1191        if (p_len) {
1192            xs *s = xs_fmt(" %d ", p_len);
1193            pending_follow_count = xs_html_tag("sup",
1194                xs_html_attr("style", "background-color: red; color: white;"),
1195                xs_html_text(s));
1196        }
1197        else
1198            pending_follow_count = xs_html_text("");
1199
1200        xs *admin_url = xs_fmt("%s/admin", user->actor);
1201        xs *notify_url = xs_fmt("%s/notifications", user->actor);
1202        xs *people_url = xs_fmt("%s/people", user->actor);
1203        xs *instance_url = xs_fmt("%s/instance", user->actor);
1204
1205        xs_html_add(top_nav,
1206            xs_html_tag("a",
1207                xs_html_attr("href", user->actor),
1208                xs_html_text(L("public"))),
1209            xs_html_text(" - "),
1210            xs_html_tag("a",
1211                xs_html_attr("href", admin_url),
1212                xs_html_text(L("private"))),
1213            xs_html_text(" - "),
1214            xs_html_tag("a",
1215                xs_html_attr("href", notify_url),
1216                xs_html_text(L("notifications"))),
1217            notify_count,
1218            xs_html_text(" - "),
1219            xs_html_tag("a",
1220                xs_html_attr("href", people_url),
1221                xs_html_text(L("people"))),
1222            pending_follow_count,
1223            xs_html_text(" - "),
1224            xs_html_tag("a",
1225                xs_html_attr("href", instance_url),
1226                xs_html_text(L("instance"))),
1227            xs_html_text(" "),
1228            xs_html_tag("form",
1229                xs_html_attr("style", "display: inline!important"),
1230                xs_html_attr("class", "snac-search-box"),
1231                xs_html_attr("action", admin_url),
1232                    xs_html_sctag("input",
1233                        xs_html_attr("type", "text"),
1234                        xs_html_attr("name", "q"),
1235                        xs_html_attr("title", L("Search posts by URL or content (regular expression), @user@host accounts, or #tag")),
1236                        xs_html_attr("placeholder", L("Content search")))));
1237    }
1238
1239    xs_html_add(body,
1240        top_nav);
1241
1242    /* user info */
1243    xs_html *top_user = xs_html_tag("div",
1244        xs_html_attr("class", "h-card snac-top-user"));
1245
1246    if (read_only) {
1247        const char *header = xs_dict_get(user->config, "header");
1248        if (header && *header) {
1249            xs_html_add(top_user,
1250                xs_html_tag("div",
1251                    xs_html_attr("class", "snac-top-user-banner"),
1252                    xs_html_attr("style", "clear: both"),
1253                    xs_html_sctag("br", NULL),
1254                    xs_html_sctag("img",
1255                        xs_html_attr("src", header))));
1256        }
1257    }
1258
1259    xs *handle = xs_fmt("@%s@%s",
1260        xs_dict_get(user->config, "uid"),
1261        xs_dict_get(srv_config, "host"));
1262
1263    xs *display_name = format_text_with_emoji(NULL, xs_dict_get(user->config, "name"), 1, proxy);
1264
1265    xs_html_add(top_user,
1266        xs_html_tag("p",
1267            xs_html_attr("class", "p-name snac-top-user-name"),
1268            xs_html_raw(display_name)),
1269        xs_html_tag("p",
1270            xs_html_attr("class", "snac-top-user-id"),
1271            xs_html_text(handle)));
1272
1273    /** instance announcement **/
1274
1275    if (!read_only) {
1276        double la = 0.0;
1277        xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement"));
1278        if (user_la != NULL)
1279            la = xs_number_get(user_la);
1280
1281        const t_announcement *an = announcement(la);
1282        if (an != NULL && (an->text != NULL)) {
1283            xs *s = xs_fmt("?da=%.0f", an->timestamp);
1284
1285            xs_html_add(top_user,  xs_html_tag("div",
1286                xs_html_attr("class", "snac-announcement"),
1287                    xs_html_text(an->text),
1288                    xs_html_text(" "),
1289                    xs_html_tag("a",
1290                            xs_html_attr("href", s),
1291                            xs_html_text("Dismiss"))));
1292        }
1293    }
1294
1295    if (read_only) {
1296        xs *bio = format_text_with_emoji(user, xs_dict_get(user->config, "bio"), 2, proxy);
1297
1298        xs_html *top_user_bio = xs_html_tag("div",
1299            xs_html_attr("class", "p-note snac-top-user-bio"),
1300            xs_html_raw(bio)); /* already sanitized */
1301
1302        xs_html_add(top_user,
1303            top_user_bio);
1304
1305        xs *metadata = NULL;
1306        const xs_dict *md = xs_dict_get(user->config, "metadata");
1307
1308        if (xs_type(md) == XSTYPE_DICT)
1309            metadata = xs_dup(md);
1310        else
1311        if (xs_type(md) == XSTYPE_STRING) {
1312            /* convert to dict for easier iteration */
1313            metadata = xs_dict_new();
1314            xs *l = xs_split(md, "\n");
1315            const char *ll;
1316
1317            xs_list_foreach(l, ll) {
1318                xs *kv = xs_split_n(ll, "=", 1);
1319                const char *k = xs_list_get(kv, 0);
1320                const char *v = xs_list_get(kv, 1);
1321
1322                if (k && v) {
1323                    xs *kk = xs_strip_i(xs_dup(k));
1324                    xs *vv = xs_strip_i(xs_dup(v));
1325                    metadata = xs_dict_set(metadata, kk, vv);
1326                }
1327            }
1328        }
1329
1330        if (xs_type(metadata) == XSTYPE_DICT) {
1331            const xs_str *k;
1332            const xs_str *v;
1333
1334            xs_dict *val_links = user->links;
1335            if (xs_is_null(val_links))
1336                val_links = xs_stock(XSTYPE_DICT);
1337
1338            xs_html *snac_metadata = xs_html_tag("div",
1339                xs_html_attr("class", "snac-metadata"));
1340
1341            int c = 0;
1342            while (xs_dict_next(metadata, &k, &v, &c)) {
1343                xs_html *value;
1344
1345                if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/") || *v == '@') {
1346                    /* is this link validated? */
1347                    xs *verified_link = NULL;
1348                    const xs_number *val_time = xs_dict_get(val_links, v);
1349                    const char *url = NULL;
1350
1351                    if (xs_is_string(val_time)) {
1352                        /* resolve again, as it may be an account handle */
1353                        url = val_time;
1354                        val_time = xs_dict_get(val_links, val_time);
1355                    }
1356
1357                    if (xs_type(val_time) == XSTYPE_NUMBER) {
1358                        time_t t = xs_number_get(val_time);
1359
1360                        if (t > 0) {
1361                            xs *s1 = xs_str_utctime(t, ISO_DATE_SPEC);
1362                            verified_link = xs_fmt("%s (%s)", L("verified link"), s1);
1363                        }
1364                    }
1365
1366                    if (!xs_is_null(verified_link)) {
1367                        value = xs_html_tag("span",
1368                            xs_html_attr("title", verified_link),
1369                            xs_html_raw("&#10004; "),
1370                            xs_html_tag("a",
1371                                xs_html_attr("rel", "me"),
1372                                xs_html_attr("target", "_blank"),
1373                                xs_html_attr("href", url ? url : v),
1374                                xs_html_text(v)));
1375                    }
1376                    else {
1377                        value = xs_html_tag("a",
1378                            xs_html_attr("rel", "me"),
1379                            xs_html_attr("href", url ? url : v),
1380                            xs_html_text(v));
1381                    }
1382                }
1383                else
1384                if (xs_startswith(v, "gemini:/") || xs_startswith(v, "xmpp:")) {
1385                    value = xs_html_tag("a",
1386                        xs_html_attr("rel", "me"),
1387                        xs_html_attr("href", v),
1388                        xs_html_text(v));
1389                }
1390                else
1391                    value = xs_html_text(v);
1392
1393                xs_html_add(snac_metadata,
1394                    xs_html_tag("span",
1395                        xs_html_attr("class", "snac-property-name"),
1396                        xs_html_text(k)),
1397                    xs_html_text(":"),
1398                    xs_html_raw("&nbsp;"),
1399                    xs_html_tag("span",
1400                        xs_html_attr("class", "snac-property-value"),
1401                        value),
1402                    xs_html_sctag("br", NULL));
1403            }
1404
1405            xs_html_add(top_user,
1406                snac_metadata);
1407        }
1408
1409        const char *latitude = xs_dict_get_def(user->config, "latitude", "");
1410        const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1411
1412        if (*latitude && *longitude) {
1413            xs *label = xs_fmt("%s,%s", latitude, longitude);
1414            xs *url   = xs_fmt("https://openstreetmap.org/search?query=%s,%s",
1415                        latitude, longitude);
1416
1417            xs_html_add(top_user,
1418                xs_html_tag("p",
1419                    xs_html_text(L("Location: ")),
1420                    xs_html_tag("a",
1421                        xs_html_attr("href", url),
1422                        xs_html_attr("target", "_blank"),
1423                        xs_html_text(label))));
1424        }
1425
1426        if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
1427            xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
1428
1429            xs_html_add(top_user,
1430                xs_html_tag("p",
1431                    xs_html_text(s1)));
1432        }
1433    }
1434
1435    xs_html_add(body,
1436        top_user);
1437
1438    return body;
1439}
1440
1441
1442xs_html *html_checkbox(const char *form_name, const char *label, int flag)
1443/* helper for checkbox rendering */
1444{
1445    return xs_html_tag("p",
1446        xs_html_sctag("input",
1447            xs_html_attr("type",     "checkbox"),
1448            xs_html_attr("name",     form_name),
1449            xs_html_attr(flag ? "checked" : "", NULL)),
1450        xs_html_tag("label",
1451            xs_html_attr("for", form_name),
1452            xs_html_text(label)));
1453}
1454
1455
1456xs_html *html_top_controls(snac *user)
1457/* generates the top controls */
1458{
1459    xs *ops_action = xs_fmt("%s/admin/action", user->actor);
1460
1461    xs_html *top_controls = xs_html_tag("div",
1462        xs_html_attr("class", "snac-top-controls"),
1463
1464        /** new post **/
1465        html_note(user, L("New Post..."),
1466            "new_post_div", "new_post_form",
1467            L("What's on your mind?"), "",
1468            NULL, NULL,
1469            xs_stock(XSTYPE_FALSE), "",
1470            SCOPE_PUBLIC, NULL,
1471            NULL, 1, NULL, NULL, 0, NULL, NULL),
1472
1473        /** operations **/
1474        xs_html_tag("details",
1475            xs_html_tag("summary",
1476                xs_html_text(L("Operations..."))),
1477            xs_html_tag("p", NULL),
1478            xs_html_tag("form",
1479                xs_html_attr("autocomplete", "off"),
1480                xs_html_attr("method",       "post"),
1481                xs_html_attr("action",       ops_action),
1482                xs_html_sctag("input",
1483                    xs_html_attr("type",     "text"),
1484                    xs_html_attr("name",     "actor"),
1485                    xs_html_attr("required", "required"),
1486                    xs_html_attr("placeholder", "bob@example.com")),
1487                xs_html_text(" "),
1488                xs_html_sctag("input",
1489                    xs_html_attr("type",    "submit"),
1490                    xs_html_attr("name",    "action"),
1491                    xs_html_attr("value",   L("Follow"))),
1492            xs_html_text(" "),
1493            xs_html_text(L("(by URL or user@host)"))),
1494            xs_html_tag("p", NULL),
1495            xs_html_tag("form",
1496                xs_html_attr("autocomplete", "off"),
1497                xs_html_attr("method",       "post"),
1498                xs_html_attr("action",       ops_action),
1499                xs_html_sctag("input",
1500                    xs_html_attr("type",     "url"),
1501                    xs_html_attr("name",     "id"),
1502                    xs_html_attr("required", "required"),
1503                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1504                xs_html_text(" "),
1505                xs_html_sctag("input",
1506                    xs_html_attr("type",    "submit"),
1507                    xs_html_attr("name",    "action"),
1508                    xs_html_attr("value",   L("Boost"))),
1509                xs_html_text(" "),
1510                xs_html_text(L("(by URL)"))),
1511            xs_html_tag("p", NULL),
1512            xs_html_tag("form",
1513                xs_html_attr("autocomplete", "off"),
1514                xs_html_attr("method",       "post"),
1515                xs_html_attr("action",       ops_action),
1516                xs_html_sctag("input",
1517                    xs_html_attr("type",     "text"),
1518                    xs_html_attr("name",     "id"),
1519                    xs_html_attr("required", "required"),
1520                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1521                xs_html_text(" "),
1522                xs_html_sctag("input",
1523                    xs_html_attr("type",    "submit"),
1524                    xs_html_attr("name",    "action"),
1525                    xs_html_attr("value",   L("Like"))),
1526                xs_html_text(" "),
1527                xs_html_text(L("(by URL)"))),
1528            xs_html_tag("form",
1529                xs_html_attr("autocomplete", "off"),
1530                xs_html_attr("method",       "post"),
1531                xs_html_attr("action",       ops_action),
1532                xs_html_sctag("input",
1533                    xs_html_attr("type",     "text"),
1534                    xs_html_attr("name",     "eid"),
1535                    xs_html_attr("required", "required"),
1536                    xs_html_attr("placeholder", ":neocat:")),
1537                xs_html_text(" "),
1538                xs_html_sctag("input",
1539                    xs_html_attr("type",     "text"),
1540                    xs_html_attr("name",     "id"),
1541                    xs_html_attr("required", "required"),
1542                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1543                xs_html_text(" "),
1544                xs_html_sctag("input",
1545                    xs_html_attr("type",    "submit"),
1546                    xs_html_attr("name",    "action"),
1547                    xs_html_attr("value",   L("EmojiReact"))),
1548                xs_html_text(" "),
1549                xs_html_text(L("(by URL)"))),
1550            xs_html_tag("p", NULL)));
1551
1552    /** user settings **/
1553
1554    const char *email = "[disabled by admin]";
1555
1556    if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1557        email = xs_dict_get(user->config_o, "email");
1558        if (xs_is_null(email)) {
1559            email = xs_dict_get(user->config, "email");
1560
1561            if (xs_is_null(email))
1562                email = "";
1563        }
1564    }
1565
1566    const char *cw = xs_dict_get(user->config, "cw");
1567    if (xs_is_null(cw))
1568        cw = "";
1569
1570    const char *telegram_bot = xs_dict_get(user->config, "telegram_bot");
1571    if (xs_is_null(telegram_bot))
1572        telegram_bot = "";
1573
1574    const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id");
1575    if (xs_is_null(telegram_chat_id))
1576        telegram_chat_id = "";
1577
1578    const char *ntfy_server = xs_dict_get(user->config, "ntfy_server");
1579    if (xs_is_null(ntfy_server))
1580        ntfy_server = "";
1581
1582    const char *ntfy_token = xs_dict_get(user->config, "ntfy_token");
1583    if (xs_is_null(ntfy_token))
1584        ntfy_token = "";
1585
1586    const char *purge_days = xs_dict_get(user->config, "purge_days");
1587    if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
1588        purge_days = (char *)xs_number_str(purge_days);
1589    else
1590        purge_days = "0";
1591
1592    const xs_val *d_dm_f_u  = xs_dict_get(user->config, "drop_dm_from_unknown");
1593    const xs_val *bot       = xs_dict_get(user->config, "bot");
1594    const xs_val *a_private = xs_dict_get(user->config, "private");
1595    const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost");
1596    const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads");
1597    const xs_val *pending    = xs_dict_get(user->config, "approve_followers");
1598    const xs_val *show_foll  = xs_dict_get(user->config, "show_contact_metrics");
1599    const char *latitude     = xs_dict_get_def(user->config, "latitude", "");
1600    const char *longitude    = xs_dict_get_def(user->config, "longitude", "");
1601    const char *webhook      = xs_dict_get_def(user->config, "notify_webhook", "");
1602    const char *post_langs   = xs_dict_get_def(user->config, "post_langs", "");
1603
1604    xs *metadata = NULL;
1605    const xs_dict *md = xs_dict_get(user->config, "metadata");
1606
1607    if (xs_type(md) == XSTYPE_DICT) {
1608        const xs_str *k;
1609        const xs_str *v;
1610
1611        metadata = xs_str_new(NULL);
1612
1613        xs_dict_foreach(md, k, v) {
1614            xs *kp = xs_fmt("%s=%s", k, v);
1615
1616            if (*metadata)
1617                metadata = xs_str_cat(metadata, "\n");
1618            metadata = xs_str_cat(metadata, kp);
1619        }
1620    }
1621    else
1622    if (xs_type(md) == XSTYPE_STRING)
1623        metadata = xs_dup(md);
1624    else
1625        metadata = xs_str_new(NULL);
1626
1627    /* ui language */
1628    xs_html *lang_select = xs_html_tag("select",
1629        xs_html_attr("name", "web_ui_lang"));
1630
1631    const char *u_lang = xs_dict_get_def(user->config, "lang", "en");
1632    const char *lang;
1633    const xs_dict *langs;
1634
1635    xs_dict_foreach(srv_langs, lang, langs) {
1636        if (strcmp(u_lang, lang) == 0)
1637            xs_html_add(lang_select,
1638                xs_html_tag("option",
1639                    xs_html_text(lang),
1640                    xs_html_attr("value", lang),
1641                    xs_html_attr("selected", "selected")));
1642        else
1643            xs_html_add(lang_select,
1644                xs_html_tag("option",
1645                    xs_html_text(lang),
1646                    xs_html_attr("value", lang)));
1647    }
1648
1649    /* timezone */
1650    xs_html *tz_select = xs_html_tag("select",
1651        xs_html_attr("name", "tz"));
1652
1653    xs *tzs = xs_tz_list();
1654    const char *tz;
1655
1656    xs_list_foreach(tzs, tz) {
1657        if (strcmp(tz, user->tz) == 0)
1658            xs_html_add(tz_select,
1659                xs_html_tag("option",
1660                xs_html_text(tz),
1661                xs_html_attr("value", tz),
1662                xs_html_attr("selected", "selected")));
1663        else
1664            xs_html_add(tz_select,
1665                xs_html_tag("option",
1666                xs_html_text(tz),
1667                xs_html_attr("value", tz)));
1668    }
1669
1670    xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
1671
1672    xs_html_add(top_controls,
1673        xs_html_tag("details",
1674        xs_html_tag("summary",
1675            xs_html_text(L("User Settings..."))),
1676        xs_html_tag("div",
1677            xs_html_attr("class", "snac-user-setup"),
1678            xs_html_tag("form",
1679                xs_html_attr("autocomplete", "off"),
1680                xs_html_attr("method",       "post"),
1681                xs_html_attr("action",       user_setup_action),
1682                xs_html_attr("enctype",      "multipart/form-data"),
1683                xs_html_tag("p",
1684                    xs_html_text(L("Display name:")),
1685                    xs_html_sctag("br", NULL),
1686                    xs_html_sctag("input",
1687                        xs_html_attr("type", "text"),
1688                        xs_html_attr("name", "name"),
1689                        xs_html_attr("value", xs_dict_get(user->config, "name")),
1690                        xs_html_attr("placeholder", L("Your name")))),
1691                xs_html_tag("p",
1692                    xs_html_text(L("Avatar: ")),
1693                    xs_html_sctag("input",
1694                        xs_html_attr("type", "file"),
1695                        xs_html_attr("name", "avatar_file"))),
1696                xs_html_tag("p",
1697                    xs_html_sctag("input",
1698                        xs_html_attr("type", "checkbox"),
1699                        xs_html_attr("name", "avatar_delete")),
1700                        xs_html_text(L("Delete current avatar"))),
1701                xs_html_tag("p",
1702                    xs_html_text(L("Header image (banner): ")),
1703                    xs_html_sctag("input",
1704                        xs_html_attr("type", "file"),
1705                        xs_html_attr("name", "header_file"))),
1706                xs_html_tag("p",
1707                    xs_html_sctag("input",
1708                        xs_html_attr("type", "checkbox"),
1709                        xs_html_attr("name", "header_delete")),
1710                        xs_html_text(L("Delete current header image"))),
1711                xs_html_tag("p",
1712                    xs_html_text(L("Bio:")),
1713                    xs_html_sctag("br", NULL),
1714                    xs_html_tag("textarea",
1715                        xs_html_attr("name", "bio"),
1716                        xs_html_attr("cols", "40"),
1717                        xs_html_attr("rows", "4"),
1718                        xs_html_attr("placeholder", L("Write about yourself here...")),
1719                        xs_html_text(xs_dict_get(user->config, "bio")))),
1720                xs_html_sctag("input",
1721                    xs_html_attr("type", "checkbox"),
1722                    xs_html_attr("name", "cw"),
1723                    xs_html_attr("id",   "cw"),
1724                    xs_html_attr(strcmp(cw, "open") == 0 ? "checked" : "", NULL)),
1725                xs_html_tag("label",
1726                    xs_html_attr("for", "cw"),
1727                    xs_html_text(L("Always show sensitive content"))),
1728                xs_html_tag("p",
1729                    xs_html_text(L("Email address for notifications:")),
1730                    xs_html_sctag("br", NULL),
1731                    xs_html_sctag("input",
1732                        xs_html_attr("type", "text"),
1733                        xs_html_attr("name", "email"),
1734                        xs_html_attr("value", email),
1735                        xs_html_attr("placeholder", "bob@example.com"))),
1736                xs_html_tag("p",
1737                    xs_html_text(L("Telegram notifications (bot key and chat id):")),
1738                    xs_html_sctag("br", NULL),
1739                    xs_html_sctag("input",
1740                        xs_html_attr("type", "text"),
1741                        xs_html_attr("name", "telegram_bot"),
1742                        xs_html_attr("value", telegram_bot),
1743                        xs_html_attr("placeholder", L("Bot API key"))),
1744                    xs_html_text(" "),
1745                    xs_html_sctag("input",
1746                        xs_html_attr("type", "text"),
1747                        xs_html_attr("name", "telegram_chat_id"),
1748                        xs_html_attr("value", telegram_chat_id),
1749                        xs_html_attr("placeholder", L("Chat id")))),
1750                xs_html_tag("p",
1751                    xs_html_text(L("ntfy notifications (ntfy server and token):")),
1752                    xs_html_sctag("br", NULL),
1753                    xs_html_sctag("input",
1754                        xs_html_attr("type", "text"),
1755                        xs_html_attr("name", "ntfy_server"),
1756                        xs_html_attr("value", ntfy_server),
1757                        xs_html_attr("placeholder", L("ntfy server - full URL (example: https://ntfy.sh/YourTopic)"))),
1758                    xs_html_text(" "),
1759                    xs_html_sctag("input",
1760                        xs_html_attr("type", "text"),
1761                        xs_html_attr("name", "ntfy_token"),
1762                        xs_html_attr("value", ntfy_token),
1763                        xs_html_attr("placeholder", L("ntfy token - if needed")))),
1764                xs_html_tag("p",
1765                    xs_html_text(L("Notify webhook:")),
1766                    xs_html_sctag("br", NULL),
1767                    xs_html_sctag("input",
1768                        xs_html_attr("type", "url"),
1769                        xs_html_attr("name", "notify_webhook"),
1770                        xs_html_attr("value", webhook),
1771                        xs_html_attr("placeholder", L("http://example.com/webhook")))),
1772                xs_html_tag("p",
1773                    xs_html_text(L("Maximum days to keep posts (0: server settings):")),
1774                    xs_html_sctag("br", NULL),
1775                    xs_html_sctag("input",
1776                        xs_html_attr("type", "number"),
1777                        xs_html_attr("name", "purge_days"),
1778                        xs_html_attr("value", purge_days))),
1779                html_checkbox("drop_dm_from_unknown", L("Drop direct messages from people you don't follow"),
1780                        xs_is_true(d_dm_f_u)),
1781                html_checkbox("bot", L("This account is a bot"),
1782                        xs_is_true(bot)),
1783                html_checkbox("auto_boost", L("Auto-boost all mentions to this account"),
1784                        xs_is_true(auto_boost)),
1785                html_checkbox("private", L("This account is private "
1786                        "(posts are not shown through the web)"),
1787                        xs_is_true(a_private)),
1788                html_checkbox("collapse_threads", L("Collapse top threads by default"),
1789                        xs_is_true(coll_thrds)),
1790                html_checkbox("approve_followers", L("Follow requests must be approved"),
1791                        xs_is_true(pending)),
1792                html_checkbox("show_contact_metrics", L("Publish follower and following metrics"),
1793                        xs_is_true(show_foll)),
1794                xs_html_tag("p",
1795                    xs_html_text(L("Current location:")),
1796                    xs_html_sctag("br", NULL),
1797                    xs_html_sctag("input",
1798                        xs_html_attr("type", "text"),
1799                        xs_html_attr("name", "latitude"),
1800                        xs_html_attr("value", latitude),
1801                        xs_html_attr("placeholder", "latitude")),
1802                    xs_html_text(" "),
1803                    xs_html_sctag("input",
1804                        xs_html_attr("type", "text"),
1805                        xs_html_attr("name", "longitude"),
1806                        xs_html_attr("value", longitude),
1807                        xs_html_attr("placeholder", "longitude"))),
1808                xs_html_tag("p",
1809                    xs_html_text(L("Profile metadata (key=value pairs in each line):")),
1810                    xs_html_sctag("br", NULL),
1811                    xs_html_tag("textarea",
1812                        xs_html_attr("name", "metadata"),
1813                        xs_html_attr("cols", "40"),
1814                        xs_html_attr("rows", "4"),
1815                        xs_html_attr("placeholder", "Blog=https:/"
1816                            "/example.com/my-blog\nGPG Key=1FA54\n..."),
1817                    xs_html_text(metadata))),
1818
1819                xs_html_tag("p",
1820                    xs_html_text(L("Web interface language:")),
1821                    xs_html_sctag("br", NULL),
1822                    lang_select),
1823
1824                xs_html_tag("p",
1825                    xs_html_text(L("Time zone:")),
1826                    xs_html_sctag("br", NULL),
1827                    tz_select),
1828
1829                xs_html_tag("p",
1830                    xs_html_text(L("Languages you usually post in:")),
1831                    xs_html_sctag("br", NULL),
1832                    xs_html_sctag("input",
1833                        xs_html_attr("type", "text"),
1834                        xs_html_attr("name", "post_langs"),
1835                        xs_html_attr("value", post_langs),
1836                        xs_html_attr("placeholder", L("en fr es de_AT")))),
1837
1838                xs_html_tag("p",
1839                    xs_html_text(L("New password:")),
1840                    xs_html_sctag("br", NULL),
1841                    xs_html_sctag("input",
1842                        xs_html_attr("type", "password"),
1843                        xs_html_attr("name", "passwd1"),
1844                        xs_html_attr("value", ""))),
1845                xs_html_tag("p",
1846                    xs_html_text(L("Repeat new password:")),
1847                    xs_html_sctag("br", NULL),
1848                    xs_html_sctag("input",
1849                        xs_html_attr("type", "password"),
1850                        xs_html_attr("name", "passwd2"),
1851                        xs_html_attr("value", ""))),
1852
1853                xs_html_sctag("input",
1854                    xs_html_attr("type", "submit"),
1855                    xs_html_attr("class", "button"),
1856                    xs_html_attr("value", L("Update user info"))),
1857
1858                xs_html_tag("p", NULL)))));
1859
1860    xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor);
1861    xs *followed_hashtags = xs_join(xs_dict_get_def(user->config,
1862                        "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
1863
1864    xs_html_add(top_controls,
1865        xs_html_tag("details",
1866        xs_html_tag("summary",
1867            xs_html_text(L("Followed hashtags..."))),
1868        xs_html_tag("p",
1869            xs_html_text(L("One hashtag per line"))),
1870        xs_html_tag("div",
1871            xs_html_attr("class", "snac-followed-hashtags"),
1872            xs_html_tag("form",
1873                xs_html_attr("autocomplete", "off"),
1874                xs_html_attr("method", "post"),
1875                xs_html_attr("action", followed_hashtags_action),
1876                xs_html_attr("enctype", "multipart/form-data"),
1877
1878                xs_html_tag("textarea",
1879                    xs_html_attr("name", "followed_hashtags"),
1880                    xs_html_attr("cols", "40"),
1881                    xs_html_attr("rows", "4"),
1882                    xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic\nhttps:/"
1883                        "/mastodon.social/tags/dogs"),
1884                    xs_html_text(followed_hashtags)),
1885
1886                xs_html_tag("br", NULL),
1887
1888                xs_html_sctag("input",
1889                    xs_html_attr("type", "submit"),
1890                    xs_html_attr("class", "button"),
1891                    xs_html_attr("value", L("Update hashtags")))))));
1892
1893    xs *blocked_hashtags_action = xs_fmt("%s/admin/blocked-hashtags", user->actor);
1894    xs *blocked_hashtags = xs_join(xs_dict_get_def(user->config,
1895                        "blocked_hashtags", xs_stock(XSTYPE_LIST)), "\n");
1896
1897    xs_html_add(top_controls,
1898        xs_html_tag("details",
1899        xs_html_tag("summary",
1900            xs_html_text(L("Blocked hashtags..."))),
1901        xs_html_tag("p",
1902            xs_html_text(L("One hashtag per line"))),
1903        xs_html_tag("div",
1904            xs_html_attr("class", "snac-blocked-hashtags"),
1905            xs_html_tag("form",
1906                xs_html_attr("autocomplete", "off"),
1907                xs_html_attr("method", "post"),
1908                xs_html_attr("action", blocked_hashtags_action),
1909                xs_html_attr("enctype", "multipart/form-data"),
1910
1911                xs_html_tag("textarea",
1912                    xs_html_attr("name", "blocked_hashtags"),
1913                    xs_html_attr("cols", "40"),
1914                    xs_html_attr("rows", "4"),
1915                    xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"),
1916                    xs_html_text(blocked_hashtags)),
1917
1918                xs_html_tag("br", NULL),
1919
1920                xs_html_sctag("input",
1921                    xs_html_attr("type", "submit"),
1922                    xs_html_attr("class", "button"),
1923                    xs_html_attr("value", L("Update hashtags")))))));
1924
1925    xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor);
1926    xs *muted_words = xs_join(xs_dict_get_def(user->config,
1927                        "muted_words", xs_stock(XSTYPE_LIST)), "\n");
1928
1929    xs_html_add(top_controls,
1930        xs_html_tag("details",
1931        xs_html_tag("summary",
1932            xs_html_text(L("Muted words..."))),
1933        xs_html_tag("p",
1934            xs_html_text(L("One word per line, partial matches count"))),
1935        xs_html_tag("div",
1936            xs_html_attr("class", "snac-muted-words"),
1937            xs_html_tag("form",
1938                xs_html_attr("autocomplete", "off"),
1939                xs_html_attr("method", "post"),
1940                xs_html_attr("action", muted_words_action),
1941                xs_html_attr("enctype", "multipart/form-data"),
1942
1943                xs_html_tag("textarea",
1944                    xs_html_attr("name", "muted_words"),
1945                    xs_html_attr("cols", "40"),
1946                    xs_html_attr("rows", "4"),
1947                    xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"),
1948                    xs_html_text(muted_words)),
1949
1950                xs_html_tag("br", NULL),
1951
1952                xs_html_sctag("input",
1953                    xs_html_attr("type", "submit"),
1954                    xs_html_attr("class", "button"),
1955                    xs_html_attr("value", L("Update muted words")))))));
1956
1957    return top_controls;
1958}
1959
1960
1961static xs_html *html_button(const char *clss, const char *label, const char *hint)
1962{
1963    xs *c = xs_fmt("snac-btn-%s", clss);
1964
1965    /* use an NULL tag to separate non-css-classed buttons from one another */
1966    return xs_html_container(
1967        xs_html_sctag("input",
1968            xs_html_attr("type",    "submit"),
1969            xs_html_attr("name",    "action"),
1970            xs_html_attr("class",   c),
1971            xs_html_attr("value",   label),
1972            xs_html_attr("title",   hint)),
1973        xs_html_text("\n"));
1974}
1975
1976
1977xs_str *build_mentions(snac *user, const xs_dict *msg)
1978/* returns a string with the mentions in msg */
1979{
1980    xs_str *s = xs_str_new(NULL);
1981    const char *list = xs_dict_get(msg, "tag");
1982    const char *v;
1983    int c = 0;
1984
1985    while (xs_list_next(list, &v, &c)) {
1986        const char *type = xs_dict_get(v, "type");
1987        const char *href = xs_dict_get(v, "href");
1988        const char *name = xs_dict_get(v, "name");
1989
1990        if (type && strcmp(type, "Mention") == 0 &&
1991            href && strcmp(href, user->actor) != 0 && name) {
1992            xs *s1 = NULL;
1993
1994            if (name[0] != '@') {
1995                s1 = xs_fmt("@%s", name);
1996                name = s1;
1997            }
1998
1999            xs *l = xs_split(name, "@");
2000
2001            /* is it a name without a host? */
2002            if (xs_list_len(l) < 3) {
2003                /* split the href and pick the host name LIKE AN ANIMAL */
2004                /* would be better to query the webfinger but *won't do that* here */
2005                xs *l2 = xs_split(href, "/");
2006
2007                if (xs_list_len(l2) >= 3) {
2008                    xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
2009
2010                    if (xs_str_in(s, s1) == -1)
2011                        s = xs_str_cat(s, s1);
2012                }
2013            }
2014            else {
2015                if (xs_str_in(s, name) == -1) {
2016                    s = xs_str_cat(s, name);
2017                    s = xs_str_cat(s, " ");
2018                }
2019            }
2020        }
2021    }
2022
2023    if (*s) {
2024        xs *s1 = s;
2025        s = xs_fmt("\n\n\nCC: %s", s1);
2026    }
2027
2028    return s;
2029}
2030
2031
2032xs_html *html_entry_controls(snac *user, const char *actor,
2033                            const xs_dict *msg, const char *md5)
2034{
2035    const char *id    = xs_dict_get(msg, "id");
2036    const char *group = xs_dict_get(msg, "audience");
2037
2038    xs *likes   = object_likes(id);
2039    xs *boosts  = object_announces(id);
2040
2041    xs *action = xs_fmt("%s/admin/action", user->actor);
2042    xs *redir  = xs_fmt("%s_entry", md5);
2043
2044    xs_html *form;
2045    xs_html *controls = xs_html_tag("div",
2046        xs_html_attr("class", "snac-controls"),
2047        form = xs_html_tag("form",
2048            xs_html_attr("autocomplete", "off"),
2049            xs_html_attr("method",       "post"),
2050            xs_html_attr("action",       action),
2051            xs_html_sctag("input",
2052                xs_html_attr("type",     "hidden"),
2053                xs_html_attr("name",     "id"),
2054                xs_html_attr("value",    id)),
2055            xs_html_sctag("input",
2056                xs_html_attr("type",     "hidden"),
2057                xs_html_attr("name",     "actor"),
2058                xs_html_attr("value",    actor)),
2059            xs_html_sctag("input",
2060                xs_html_attr("type",     "hidden"),
2061                xs_html_attr("name",     "group"),
2062                xs_html_attr("value",    xs_is_null(group) ? "" : group)),
2063            xs_html_sctag("input",
2064                xs_html_attr("type",     "hidden"),
2065                xs_html_attr("name",     "redir"),
2066                xs_html_attr("value",    redir))));
2067
2068    if (!is_msg_mine(user, id)) {
2069        if (xs_list_in(likes, user->md5) == -1) {
2070            /* not already liked; add button */
2071            xs_html_add(form,
2072                html_button("like", L("Like"), L("Say you like this post")));
2073        }
2074        else {
2075            /* not like it anymore */
2076            xs_html_add(form,
2077                html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
2078        }
2079    }
2080    else {
2081        if (is_pinned(user, id))
2082            xs_html_add(form,
2083                html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
2084        else
2085            xs_html_add(form,
2086                html_button("pin", L("Pin"), L("Pin this post to the top of your timeline")));
2087    }
2088
2089    if (is_msg_public(msg)) {
2090        if (xs_list_in(boosts, user->md5) == -1) {
2091            /* not already boosted; add button */
2092            xs_html_add(form,
2093                html_button("boost", L("Boost"), L("Announce this post to your followers")));
2094        }
2095        else {
2096            /* already boosted; add button to regret */
2097            xs_html_add(form,
2098                html_button("unboost", L("Unboost"), L("I regret I boosted this")));
2099        }
2100    }
2101
2102    if (is_bookmarked(user, id))
2103            xs_html_add(form,
2104                html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
2105        else
2106            xs_html_add(form,
2107                html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
2108
2109    if (strcmp(actor, user->actor) != 0) {
2110        /* controls for other actors than this one */
2111        if (following_check(user, actor)) {
2112            xs_html_add(form,
2113                html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
2114        }
2115        else {
2116            xs_html_add(form,
2117                html_button("follow", L("Follow"), L("Start following this user's activity")));
2118        }
2119
2120        if (!xs_is_null(group)) {
2121            if (following_check(user, group)) {
2122                xs_html_add(form,
2123                    html_button("unfollow", L("Unfollow Group"),
2124                        L("Stop following this group or channel")));
2125            }
2126            else {
2127                xs_html_add(form,
2128                    html_button("follow", L("Follow Group"),
2129                        L("Start following this group or channel")));
2130            }
2131        }
2132
2133        xs_html_add(form,
2134            html_button("mute", L("MUTE"),
2135                L("Block any activity from this user forever")));
2136    }
2137
2138    if (!xs_is_true(xs_dict_get(srv_config, "hide_delete_post_button")))
2139        xs_html_add(form,
2140            html_button("delete", L("Delete"), L("Delete this post")));
2141
2142    xs_html_add(form,
2143        html_button("hide",   L("Hide"), L("Hide this post and its children")));
2144
2145    const char *prev_src = xs_dict_get(msg, "sourceContent");
2146
2147    if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/
2148        /* post can be edited */
2149        xs *div_id  = xs_fmt("%s_edit", md5);
2150        xs *form_id = xs_fmt("%s_edit_form", md5);
2151        xs *redir   = xs_fmt("%s_entry", md5);
2152
2153        xs *att_files = xs_list_new();
2154        xs *att_alt_texts = xs_list_new();
2155
2156        const xs_list *att_list = xs_dict_get(msg, "attachment");
2157
2158        if (xs_is_list(att_list)) {
2159            const xs_dict *d;
2160
2161            xs_list_foreach(att_list, d) {
2162                const char *att_file = xs_dict_get(d, "url");
2163                const char *att_alt_text = xs_dict_get(d, "name");
2164
2165                if (xs_is_string(att_file) && xs_is_string(att_alt_text)) {
2166                    att_files = xs_list_append(att_files, att_file);
2167                    att_alt_texts = xs_list_append(att_alt_texts, att_alt_text);
2168                }
2169            }
2170        }
2171
2172        const char *note_lang = NULL;
2173        const xs_dict *cmap = xs_dict_get(msg, "contentMap");
2174        if (xs_is_dict(cmap)) {
2175            const char *dummy;
2176            int c = 0;
2177
2178            xs_dict_next(cmap, &note_lang, &dummy, &c);
2179        }
2180
2181        const int edit_scope = get_msg_visibility(msg);
2182
2183        xs_html_add(controls, xs_html_tag("div",
2184            xs_html_tag("p", NULL),
2185            html_note(user, L("Edit..."),
2186                div_id, form_id,
2187                "", prev_src,
2188                id, NULL,
2189                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
2190                edit_scope, redir,
2191                NULL, 0, att_files, att_alt_texts, is_draft(user, id),
2192                xs_dict_get(msg, "published"), note_lang)),
2193            xs_html_tag("p", NULL));
2194    }
2195
2196    if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/
2197        /* the post textarea */
2198        xs *div_id  = xs_fmt("%s_reply", md5);
2199        xs *form_id = xs_fmt("%s_reply_form", md5);
2200        xs *e_react = emoji_reacted(user, id);
2201
2202        xs_html_add(controls, xs_html_tag("div",
2203            xs_html_tag("p", NULL),
2204            html_emoji(
2205                user, L("Emoji react..."),
2206                div_id, form_id,
2207                ":neocat:", id,
2208                e_react)),
2209            xs_html_tag("p", NULL));
2210    }
2211
2212    { /** reply **/
2213        /* the post textarea */
2214        xs *ct      = build_mentions(user, msg);
2215        xs *div_id  = xs_fmt("%s_reply", md5);
2216        xs *form_id = xs_fmt("%s_reply_form", md5);
2217        xs *redir   = xs_fmt("%s_entry", md5);
2218
2219        const int scope = get_msg_visibility(msg);
2220
2221        xs_html_add(controls, xs_html_tag("div",
2222            xs_html_tag("p", NULL),
2223            html_note(user, L("Reply..."),
2224                div_id, form_id,
2225                "", ct,
2226                NULL, NULL,
2227                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
2228                scope, redir,
2229                id, 0, NULL, NULL, 0, NULL, NULL)),
2230            xs_html_tag("p", NULL));
2231    }
2232
2233    return controls;
2234}
2235
2236
2237static const xs_str* words_in_content(const xs_list *words, const xs_val *content)
2238/* returns a word that matches any of the words in content */
2239{
2240    if (!xs_is_list(words) || !xs_is_string(content)) {
2241        return NULL;
2242    }
2243    xs *c = xs_split(content, " ");
2244    xs *sc = xs_list_sort(c, NULL);
2245
2246    const xs_str *wv;
2247    const xs_str *cv;
2248    xs_list_foreach(words, wv) {
2249        xs_list_foreach(sc, cv) {
2250            xs_tolower_i((xs_str*)cv);
2251            if(xs_str_in(cv, wv) != -1)
2252                return wv;
2253        }
2254    }
2255
2256    return NULL;
2257}
2258
2259
2260xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2261                   int level, const char *md5, int hide_children)
2262{
2263    const char *id    = xs_dict_get(msg, "id");
2264    const char *type  = xs_dict_get(msg, "type");
2265    const char *actor;
2266    const char *v;
2267    int has_title = 0;
2268    int collapse_threads = 0;
2269    const char *proxy = NULL;
2270
2271    if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2272        proxy = user->actor;
2273
2274    /* do not show non-public messages in the public timeline */
2275    if ((read_only || !user) && !is_msg_public(msg))
2276        return NULL;
2277
2278    if (id && is_instance_blocked(id))
2279        return NULL;
2280
2281    if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
2282        collapse_threads = 1;
2283
2284    /* hidden? do nothing more for this conversation */
2285    if (user && is_hidden(user, id)) {
2286        xs *s1 = xs_fmt("%s_entry", md5);
2287
2288        /* return just an dummy anchor, to keep position after hitting 'Hide' */
2289        return xs_html_tag("div",
2290            xs_html_tag("a",
2291                xs_html_attr("name", s1)));
2292    }
2293
2294    /* avoid too deep nesting, as it may be a loop */
2295    if (level >= MAX_CONVERSATION_LEVELS)
2296        return xs_html_tag("mark",
2297            xs_html_text(L("Truncated (too deep)")));
2298
2299    const char *lang = NULL;
2300    const xs_dict *cmap = xs_dict_get(msg, "contentMap");
2301    if (xs_is_dict(cmap)) {
2302        const char *dummy;
2303        int c = 0;
2304
2305        xs_dict_next(cmap, &lang, &dummy, &c);
2306    }
2307
2308    if (strcmp(type, "Follow") == 0) {
2309        return xs_html_tag("div",
2310            xs_html_attr("class", "snac-post"),
2311            xs_html_tag("div",
2312                xs_html_attr("class", "snac-post-header"),
2313                xs_html_tag("div",
2314                    xs_html_attr("class", "snac-origin"),
2315                    xs_html_text(L("follows you"))),
2316                html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL, lang)));
2317    }
2318    else
2319    if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
2320        /* skip oddities */
2321        snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
2322        return NULL;
2323    }
2324
2325    /* ignore notes with "name", as they are votes to Questions */
2326    if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
2327        return NULL;
2328
2329    /* get the attributedTo */
2330    if ((actor = get_atto(msg)) == NULL)
2331        return NULL;
2332
2333    /* ignore muted morons immediately */
2334    if (user && is_muted(user, actor)) {
2335        xs *s1 = xs_fmt("%s_entry", md5);
2336
2337        /* return just an dummy anchor, to keep position after hitting 'MUTE' */
2338        return xs_html_tag("div",
2339            xs_html_tag("a",
2340                xs_html_attr("name", s1)));
2341    }
2342
2343    /* don't show followers-only notes from not followed users */
2344    if (user && get_msg_visibility(msg) == SCOPE_FOLLOWERS &&
2345        strcmp(user->actor, actor) != 0 && following_check(user, actor) == 0) {
2346        return NULL;
2347    }
2348
2349    if ((user == NULL || strcmp(actor, user->actor) != 0)
2350        && !valid_status(actor_get(actor, NULL))) {
2351
2352        if (user)
2353            enqueue_actor_refresh(user, actor, 0);
2354
2355        return NULL;
2356    }
2357
2358    /** html_entry top tag **/
2359    xs_html *entry_top = xs_html_tag("div", NULL);
2360
2361    {
2362        xs *s1 = xs_fmt("%s_entry", md5);
2363        xs_html_add(entry_top,
2364            xs_html_tag("a",
2365                xs_html_attr("name", s1)));
2366    }
2367
2368    xs_html *entry = xs_html_tag("div",
2369        xs_html_attr("class", level == 0 ? "snac-post" : "snac-child"));
2370
2371    xs_html_add(entry_top,
2372        entry);
2373
2374    /** post header **/
2375
2376    xs_html *score;
2377    xs_html *post_header = xs_html_tag("div",
2378        xs_html_attr("class", "snac-post-header"),
2379        score = xs_html_tag("div",
2380            xs_html_attr("class", "snac-score")));
2381
2382    xs_html_add(entry,
2383        post_header);
2384
2385    if (user && is_pinned(user, id)) {
2386        /* add a pin emoji */
2387        xs_html_add(score,
2388            xs_html_tag("span",
2389                xs_html_attr("title", L("Pinned")),
2390                xs_html_raw(" &#128204; ")));
2391    }
2392
2393    if (user && !read_only && is_bookmarked(user, id)) {
2394        /* add a bookmark emoji */
2395        xs_html_add(score,
2396            xs_html_tag("span",
2397                xs_html_attr("title", L("Bookmarked")),
2398                xs_html_raw(" &#128278; ")));
2399    }
2400
2401    if (strcmp(type, "Question") == 0) {
2402        /* add the ballot box emoji */
2403        xs_html_add(score,
2404            xs_html_tag("span",
2405                xs_html_attr("title", L("Poll")),
2406                xs_html_raw(" &#128499; ")));
2407
2408        if (user && was_question_voted(user, id)) {
2409            /* add a check to show this poll was voted */
2410            xs_html_add(score,
2411                xs_html_tag("span",
2412                    xs_html_attr("title", L("Voted")),
2413                    xs_html_raw(" &#10003; ")));
2414        }
2415    }
2416
2417    if (strcmp(type, "Event") == 0) {
2418        /* add the calendar emoji */
2419        xs_html_add(score,
2420            xs_html_tag("span",
2421                xs_html_attr("title", L("Event")),
2422                xs_html_raw(" &#128197; ")));
2423    }
2424
2425    /* if it's a user from this same instance, add the score */
2426    if (xs_startswith(id, srv_baseurl)) {
2427        int n_likes  = object_likes_len(id);
2428        int n_boosts = object_announces_len(id);
2429
2430        /* alternate emojis: %d &#128077; %d &#128257; */
2431        xs *s1 = xs_fmt("%d &#9733; %d &#8634;\n", n_likes, n_boosts);
2432
2433        xs_html_add(score,
2434            xs_html_raw(s1));
2435    }
2436
2437    xs *boosts = object_announces(id);
2438
2439    if (xs_list_len(boosts)) {
2440        /* if somebody boosted this, show as origin */
2441        const char *p = xs_list_get(boosts, -1);
2442        xs *actor_r = NULL;
2443
2444        if (user && xs_list_in(boosts, user->md5) != -1) {
2445            /* we boosted this */
2446            xs_html_add(post_header,
2447                xs_html_tag("div",
2448                    xs_html_attr("class", "snac-origin"),
2449                    xs_html_tag("a",
2450                        xs_html_attr("href", user->actor),
2451                        xs_html_text(xs_dict_get(user->config, "name"))),
2452                        xs_html_text(" "),
2453                        xs_html_text(L("boosted"))));
2454        }
2455        else
2456        if (valid_status(object_get_by_md5(p, &actor_r))) {
2457            xs *name = actor_name(actor_r, proxy);
2458
2459            if (!xs_is_null(name)) {
2460                xs *href = NULL;
2461                const char *id = xs_dict_get(actor_r, "id");
2462                int fwers = 0;
2463                int fwing = 0;
2464
2465                if (user != NULL) {
2466                    fwers = follower_check(user, id);
2467                    fwing = following_check(user, id);
2468                }
2469
2470                if (!read_only && (fwers || fwing))
2471                    href = xs_fmt("%s/people/%s", user->actor, p);
2472                else
2473                    href = xs_dup(id);
2474
2475                xs_html_add(post_header,
2476                    xs_html_tag("div",
2477                        xs_html_attr("class", "snac-origin"),
2478                        xs_html_tag("a",
2479                            xs_html_attr("href", href),
2480                            xs_html_raw(name)), /* already sanitized */
2481                            xs_html_text(" "),
2482                            xs_html_text(L("boosted"))));
2483            }
2484        }
2485    }
2486
2487    if (user && strcmp(type, "Note") == 0) {
2488        /* is the parent not here? */
2489        const char *parent = get_in_reply_to(msg);
2490
2491        if (!xs_is_null(parent) && *parent) {
2492            if (!timeline_here(user, parent)) {
2493                xs_html_add(post_header,
2494                    xs_html_tag("div",
2495                        xs_html_attr("class", "snac-origin"),
2496                        xs_html_text(L("in reply to")),
2497                        xs_html_text(" "),
2498                        xs_html_tag("a",
2499                            xs_html_attr("href", parent),
2500                            xs_html_text("»"))));
2501            }
2502        }
2503    }
2504
2505    xs_html_add(post_header,
2506        html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5, lang));
2507
2508    /** post content **/
2509
2510    xs_html *snac_content_wrap = xs_html_tag("div",
2511        xs_html_attr("class", "e-content snac-content"));
2512
2513    if (xs_is_string(lang))
2514        xs_html_add(snac_content_wrap,
2515            xs_html_attr("lang", lang));
2516
2517    xs_html_add(entry,
2518        snac_content_wrap);
2519
2520    if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
2521        xs_html_add(snac_content_wrap,
2522            xs_html_tag("h3",
2523                xs_html_attr("class", "snac-entry-title"),
2524                xs_html_text(v)));
2525
2526        has_title = 1;
2527    }
2528
2529    xs_html *snac_content = NULL;
2530
2531    v = xs_dict_get(msg, "summary");
2532
2533    /* if it has summary or marked as sensitive - add CW */
2534    /* come clients don't set "sensitive" flag properly */
2535    if ((!xs_is_null(v) && *v) || xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
2536        if (xs_is_null(v) || *v == '\0')
2537            v = "...";
2538
2539        const char *cw = "";
2540
2541        if (user) {
2542            /* only show it when not in the public timeline and the config setting is "open" */
2543            cw = xs_dict_get(user->config, "cw");
2544            if (xs_is_null(cw) || read_only)
2545                cw = "";
2546        }
2547
2548        snac_content = xs_html_tag("details",
2549            xs_html_attr(cw, NULL),
2550            xs_html_tag("summary",
2551                xs_html_text(v),
2552                xs_html_text(L(" [SENSITIVE CONTENT]"))));
2553    }
2554    else
2555    if (user &&
2556        /* muted_words is all lowercase and sorted for performance */
2557        (v = words_in_content(xs_dict_get(user->config, "muted_words"),
2558                              xs_dict_get(msg, "content"))) != NULL) {
2559        snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id));
2560        snac_content = xs_html_tag("details",
2561            xs_html_tag("summary",
2562                xs_html_text(L("Muted: ")),
2563                xs_html_text(v)));
2564    }
2565    else {
2566        snac_content = xs_html_tag("div", NULL);
2567    }
2568
2569    xs_html_add(snac_content_wrap,
2570        snac_content);
2571
2572    /* add all emoji reacts */
2573    int is_emoji = 0;
2574    if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2575        int c = 0;
2576        const xs_dict *k;
2577        xs *ls = xs_list_new();
2578        xs *sfrl = xs_dict_new();
2579        xs *rl = object_get_emoji_reacts(id);
2580
2581        while (xs_list_next(rl, &v, &c)) {
2582            xs *m = NULL;
2583            if (valid_status(object_get_by_md5(v, &m))) {
2584                const char *content = xs_dict_get(m, "content");
2585                const char *actor = xs_dict_get(m, "actor");
2586                const xs_list *contentl = xs_dict_get(sfrl, content);
2587
2588                if ((user && is_muted(user, actor)) || is_instance_blocked(actor))
2589                    continue;
2590
2591                xs *actors = xs_list_new();
2592                actors = xs_list_append(actors, actor);
2593                char me = actor && user && strcmp(actor, user->actor) == 0;
2594                int count = 1;
2595
2596                if (contentl) {
2597                    count = atoi(xs_list_get(contentl, 0)) + 1;
2598                    const xs_list *actorsc = xs_list_get(contentl, 1);
2599                    if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
2600                        me = 1;
2601
2602                    if (xs_list_in(actorsc, actor) != -1) {
2603                        xs_free(actors);
2604                        actors = xs_dup(actorsc);
2605                    }
2606                    else
2607                        actors = xs_list_cat(actors, actorsc);
2608                }
2609
2610                xs *fl = xs_list_new();
2611                xs *c1 = xs_fmt("%d", count);
2612                xs *c2 = xs_fmt("%d", me);
2613                fl = xs_list_append(fl, c1, actors, c2);
2614                sfrl = xs_dict_append(sfrl, content, fl);
2615            }
2616        }
2617
2618        c = 0;
2619
2620        while (xs_list_next(rl, &k, &c)) {
2621            xs *m = NULL;
2622            if (valid_status(object_get_by_md5(k, &m))) {
2623                const xs_dict *tag = xs_dict_get(m, "tag");
2624                const xs_dict *ide = xs_dict_get(m, "id");
2625
2626                const char *content = xs_dict_get(m, "content");
2627                const char *shortname;
2628                shortname = xs_dict_get(m, "content");
2629
2630                const xs_list *items = xs_dict_get(sfrl, content);
2631
2632                if (!xs_is_null(items)) {
2633                    const char *nb = xs_list_get(items, 0);
2634                    const xs_list *actors = xs_list_get(items, 1);
2635                    const char me = *xs_list_get(items, 2) == '1';
2636
2637                    is_emoji = 1;
2638
2639                    xs *al = xs_join(actors, ",\n\t");
2640                    xs *act = atoi(nb) > 1 ?
2641                        xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
2642                        xs_dup(xs_dict_get(m, "actor"));
2643
2644                    xs *class = xs_list_new();
2645                    class = xs_list_append(class, "snac-reaction");
2646
2647                    xs_html *ret = NULL;
2648                    if (tag && shortname) {
2649                        xs *cl = xs_list_new();
2650                        cl = xs_list_append(cl, "snac-reaction-image");
2651                        xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
2652
2653                        emoji = xs_strip_chars_i(emoji, ":");
2654
2655                        if (me)
2656                            class = xs_list_append(class, "snac-reacted");
2657
2658                        xs *l1 = xs_join(class, " ");
2659                        ret = xs_html_tag("button",
2660                                xs_html_attr("type", "submit"),
2661                                xs_html_attr("name", "action"),
2662                                xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
2663                                xs_html_raw(emoji),
2664                                xs_html_tag("span",
2665                                    xs_html_raw(nb),
2666                                    xs_html_attr("style", "padding-left: 5px;")),
2667                                xs_html_attr("title", act),
2668                                xs_html_attr("class", l1));
2669
2670                        if (!(ide && xs_startswith(ide, srv_baseurl)))
2671                            xs_html_add(ret, xs_html_attr("disabled", "true"));
2672                    }
2673                    else if (shortname) {
2674                        xs *sn = xs_dup(shortname);
2675                        const char *sna = sn;
2676                        unsigned int utf = xs_utf8_dec((const char **)&sna);
2677
2678                        if (xs_is_emoji(utf)) {
2679                            const char *style = "font-size: large;";
2680                            if (me)
2681                                class = xs_list_append(class, "snac-reacted");
2682                            xs *l1 = xs_join(class, " ");
2683                            xs *s1 = xs_fmt("&#%d", utf);
2684                            ret = xs_html_tag("button",
2685                                    xs_html_attr("type", "submit"),
2686                                    xs_html_attr("name", "action"),
2687                                    xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
2688                                    xs_html_raw(s1),
2689                                    xs_html_tag("span",
2690                                        xs_html_raw(nb),
2691                                        xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
2692                                    xs_html_attr("title", act),
2693                                    xs_html_attr("class", l1),
2694                                    xs_html_attr("style", style));
2695                        }
2696                    }
2697                    if (ret) {
2698                        xs *s1;
2699                        if (user) {
2700                            xs *action = xs_fmt("%s/admin/action", user->actor);
2701                            xs *form_id = xs_fmt("%s_reply_form", md5);
2702
2703                            xs_html *form =
2704                                xs_html_tag("form",
2705                                xs_html_attr("autocomplete", "off"),
2706                                xs_html_attr("method",       "post"),
2707                                xs_html_attr("action",       action),
2708                                xs_html_attr("enctype",      "multipart/form-data"),
2709                                xs_html_attr("style",      "display: inline-flex;"
2710                                    "vertical-align: middle;"),
2711                                xs_html_attr("id",           form_id),
2712                                xs_html_sctag("input",
2713                                    xs_html_attr("type",  "hidden"),
2714                                    xs_html_attr("name",  "id"),
2715                                    xs_html_attr("value", id)),
2716                                xs_html_sctag("input",
2717                                    xs_html_attr("type",  "hidden"),
2718                                    xs_html_attr("name",  "eid"),
2719                                    xs_html_attr("value", shortname)),
2720                                ret);
2721                            s1 = xs_html_render(form);
2722                        }
2723                        else
2724                            s1 = xs_html_render(ret);
2725
2726                        ls = xs_list_append(ls, s1);
2727                        sfrl = xs_dict_del(sfrl, content);
2728                    }
2729                }
2730            }
2731        }
2732
2733        c = 0;
2734
2735        xs_html *emoji_div;
2736        if (xs_list_len(ls) > 0) {
2737                emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
2738                        xs_html_attr("class", "snac-reaction-div"));
2739
2740            while (ls != NULL && xs_list_next(ls, &k, &c))
2741                xs_html_add(emoji_div, xs_html_raw(k));
2742
2743            xs_html_add(snac_content_wrap, emoji_div);
2744        }
2745
2746    }
2747
2748    {
2749        /** build the content string **/
2750        const char *content = xs_dict_get(msg, "content");
2751
2752        if (xs_type(content) != XSTYPE_STRING) {
2753            if (!xs_is_null(content))
2754                srv_archive_error("unexpected_content_xstype",
2755                    "content field type", xs_stock(XSTYPE_DICT), msg);
2756
2757            content = "";
2758        }
2759
2760        /* skip ugly line breaks at the beginning */
2761        while (xs_startswith(content, "<br>"))
2762            content += 4;
2763
2764        xs *c = sanitize(content);
2765
2766        /* do some tweaks to the content */
2767        c = xs_replace_i(c, "\r", "");
2768
2769        while (xs_endswith(c, "<br><br>"))
2770            c = xs_crop_i(c, 0, -4);
2771
2772        c = xs_replace_i(c, "<br><br>", "<p>");
2773
2774        if (is_emoji == 0)
2775            c = xs_str_cat(c, "<p>");
2776
2777        /* replace the :shortnames: */
2778        c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
2779
2780        /* Peertube videos content is in markdown */
2781        const char *mtype = xs_dict_get(msg, "mediaType");
2782        if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
2783            /* a full conversion could be better */
2784            c = xs_replace_i(c, "\r", "");
2785            c = xs_replace_i(c, "\n", "<br>");
2786        }
2787
2788        /* c contains sanitized HTML */
2789        xs_html_add(snac_content,
2790            xs_html_raw(c));
2791
2792        /* quoted post */
2793        const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
2794
2795        if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
2796            xs *quoted_post = NULL;
2797
2798            if (valid_status(object_get(quoted_id, &quoted_post))) {
2799                xs_html_add(snac_content,
2800                    xs_html_tag("blockquote",
2801                        xs_html_attr("class", "snac-quoted-post"),
2802                        html_entry(user, quoted_post, 1, 1, NULL, 1)));
2803            }
2804            else
2805            if (user)
2806                enqueue_object_request(user, quoted_id, 0);
2807        }
2808    }
2809
2810    if (strcmp(type, "Question") == 0) { /** question content **/
2811        const xs_list *oo = xs_dict_get(msg, "oneOf");
2812        const xs_list *ao = xs_dict_get(msg, "anyOf");
2813        const xs_list *p;
2814        const xs_dict *v;
2815        int closed = 0;
2816        const char *f_closed = NULL;
2817
2818        xs_html *poll = xs_html_tag("div", NULL);
2819
2820        if (read_only)
2821            closed = 1; /* non-identified page; show as closed */
2822        else
2823        if (user && is_msg_mine(user, id))
2824            closed = 1; /* we questioned; closed for us */
2825        else
2826        if (user && was_question_voted(user, id))
2827            closed = 1; /* we already voted; closed for us */
2828
2829        if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
2830            /* it has a closed date... but is it in the past? */
2831            time_t t0 = time(NULL);
2832            time_t t1 = xs_parse_iso_date(f_closed, 0);
2833
2834            if (t1 < t0)
2835                closed = 2;
2836        }
2837
2838        /* get the appropriate list of options */
2839        p = oo != NULL ? oo : ao;
2840
2841        if (closed || user == NULL) {
2842            /* closed poll */
2843            xs_html *poll_result = xs_html_tag("table",
2844                xs_html_attr("class", "snac-poll-result"));
2845            int c = 0;
2846
2847            while (xs_list_next(p, &v, &c)) {
2848                const char *name       = xs_dict_get(v, "name");
2849                const xs_dict *replies = xs_dict_get(v, "replies");
2850
2851                if (xs_is_string(name) && xs_is_dict(replies)) {
2852                    const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
2853
2854                    if (xs_is_string(ti))
2855                        xs_html_add(poll_result,
2856                            xs_html_tag("tr",
2857                                xs_html_tag("td",
2858                                    xs_html_text(name),
2859                                    xs_html_text(":")),
2860                                xs_html_tag("td",
2861                                    xs_html_text(ti))));
2862                }
2863            }
2864
2865            xs_html_add(poll,
2866                poll_result);
2867        }
2868        else {
2869            /* poll still active */
2870            xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
2871            xs_html *form;
2872            xs_html *poll_form = xs_html_tag("div",
2873                xs_html_attr("class", "snac-poll-form"),
2874                form = xs_html_tag("form",
2875                    xs_html_attr("autocomplete", "off"),
2876                    xs_html_attr("method", "post"),
2877                    xs_html_attr("action", vote_action),
2878                    xs_html_sctag("input",
2879                        xs_html_attr("type", "hidden"),
2880                        xs_html_attr("name", "actor"),
2881                        xs_html_attr("value", actor)),
2882                    xs_html_sctag("input",
2883                        xs_html_attr("type", "hidden"),
2884                        xs_html_attr("name", "irt"),
2885                        xs_html_attr("value", id))));
2886
2887            int c = 0;
2888            while (xs_list_next(p, &v, &c)) {
2889                const char *name = xs_dict_get(v, "name");
2890                const xs_dict *replies = xs_dict_get(v, "replies");
2891
2892                if (name) {
2893                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
2894
2895                    xs_html *btn = xs_html_sctag("input",
2896                            xs_html_attr("id", name),
2897                            xs_html_attr("value", name),
2898                            xs_html_attr("name", "question"));
2899
2900                    if (!xs_is_null(oo)) {
2901                        xs_html_add(btn,
2902                            xs_html_attr("type", "radio"),
2903                            xs_html_attr("required", "required"));
2904                    }
2905                    else
2906                        xs_html_add(btn,
2907                            xs_html_attr("type", "checkbox"));
2908
2909                    xs_html_add(form,
2910                        btn,
2911                        xs_html_text(" "),
2912                        xs_html_tag("span",
2913                            xs_html_attr("title", ti),
2914                            xs_html_text(name)),
2915                        xs_html_sctag("br", NULL));
2916                }
2917            }
2918
2919            xs_html_add(form,
2920                xs_html_tag("p", NULL),
2921                xs_html_sctag("input",
2922                    xs_html_attr("type", "submit"),
2923                    xs_html_attr("class", "button"),
2924                    xs_html_attr("value", L("Vote"))));
2925
2926            xs_html_add(poll,
2927                poll_form);
2928        }
2929
2930        /* if it's *really* closed, say it */
2931        if (closed == 2) {
2932            xs_html_add(poll,
2933                xs_html_tag("p",
2934                    xs_html_text(L("Closed"))));
2935        }
2936        else {
2937            /* show when the poll closes */
2938            const char *end_time = xs_dict_get(msg, "endTime");
2939
2940            /* Pleroma does not have an endTime field;
2941               it has a closed time in the future */
2942            if (xs_is_null(end_time))
2943                end_time = xs_dict_get(msg, "closed");
2944
2945            if (!xs_is_null(end_time)) {
2946                time_t t0 = time(NULL);
2947                time_t t1 = xs_parse_iso_date(end_time, 0);
2948
2949                if (t1 > 0 && t1 > t0) {
2950                    time_t diff_time = t1 - t0;
2951                    xs *tf = xs_str_time_diff(diff_time);
2952                    char *p = tf;
2953
2954                    /* skip leading zeros */
2955                    for (; *p == '0' || *p == ':'; p++);
2956
2957                    xs_html_add(poll,
2958                        xs_html_tag("p",
2959                            xs_html_text(L("Closes in")),
2960                            xs_html_text(" "),
2961                            xs_html_text(p)));
2962                }
2963            }
2964        }
2965
2966        xs_html_add(snac_content,
2967            poll);
2968    }
2969
2970    /** attachments **/
2971    xs *attach = get_attachments(msg);
2972
2973    {
2974        /* make custom css for attachments easier */
2975        xs_html *content_attachments = xs_html_tag("div",
2976            xs_html_attr("class", "snac-content-attachments"));
2977
2978        xs_html_add(snac_content,
2979            content_attachments);
2980
2981        const char *content = xs_dict_get(msg, "content");
2982
2983        int c = 0;
2984        const xs_dict *a;
2985        while (xs_list_next(attach, &a, &c)) {
2986            const char *type = xs_dict_get(a, "type");
2987            const char *o_href = xs_dict_get(a, "href");
2988            const char *name = xs_dict_get(a, "name");
2989
2990            if (!xs_is_string(type) || !xs_is_string(o_href))
2991                continue;
2992
2993            /* if this URL is already in the post content, skip */
2994            if (content && xs_str_in(content, o_href) != -1)
2995                continue;
2996
2997            if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
2998                continue;
2999
3000            /* do this attachment include an icon? */
3001            const xs_dict *icon = xs_dict_get(a, "icon");
3002            if (xs_type(icon) == XSTYPE_DICT) {
3003                const char *icon_mtype = xs_dict_get(icon, "mediaType");
3004                const char *icon_url   = xs_dict_get(icon, "url");
3005
3006                if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
3007                    xs_html_add(content_attachments,
3008                        xs_html_tag("a",
3009                            xs_html_attr("href", icon_url),
3010                            xs_html_attr("target", "_blank"),
3011                            xs_html_sctag("img",
3012                                xs_html_attr("loading", "lazy"),
3013                                xs_html_attr("src", icon_url))));
3014                }
3015            }
3016
3017            xs *href = make_url(o_href, proxy, 0);
3018
3019            if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
3020                xs_html_add(content_attachments,
3021                    xs_html_tag("a",
3022                        xs_html_attr("href", href),
3023                        xs_html_attr("target", "_blank"),
3024                        xs_html_sctag("img",
3025                            xs_html_attr("loading", "lazy"),
3026                            xs_html_attr("src", href),
3027                            xs_html_attr("alt", name),
3028                            xs_html_attr("title", name))));
3029            }
3030            else
3031            if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
3032                xs_html_add(content_attachments,
3033                    xs_html_tag("video",
3034                        xs_html_attr("preload", "none"),
3035                        xs_html_attr("style", "width: 100%"),
3036                        xs_html_attr("class", "snac-embedded-video"),
3037                        xs_html_attr("controls", NULL),
3038                        xs_html_attr("src", href),
3039                        xs_html_text(L("Video")),
3040                        xs_html_text(": "),
3041                        xs_html_tag("a",
3042                            xs_html_attr("href", href),
3043                            xs_html_text(name))));
3044            }
3045            else
3046            if (xs_startswith(type, "audio/")) {
3047                xs_html_add(content_attachments,
3048                    xs_html_tag("audio",
3049                        xs_html_attr("preload", "none"),
3050                        xs_html_attr("style", "width: 100%"),
3051                        xs_html_attr("class", "snac-embedded-audio"),
3052                        xs_html_attr("controls", NULL),
3053                        xs_html_attr("src", href),
3054                        xs_html_text(L("Audio")),
3055                        xs_html_text(": "),
3056                        xs_html_tag("a",
3057                            xs_html_attr("href", href),
3058                            xs_html_text(name))));
3059            }
3060            else
3061            if (strcmp(type, "Link") == 0) {
3062                xs_html_add(content_attachments,
3063                    xs_html_tag("p",
3064                        xs_html_tag("a",
3065                            xs_html_attr("href", o_href),
3066                            xs_html_text(href))));
3067
3068                /* do not generate an Alt... */
3069                name = NULL;
3070            }
3071            else {
3072                xs *d_href = xs_dup(o_href);
3073                if (strlen(d_href) > 64) {
3074                    d_href[64] = '\0';
3075                    d_href = xs_str_cat(d_href, "...");
3076                }
3077
3078                xs_html_add(content_attachments,
3079                    xs_html_tag("p",
3080                        xs_html_tag("a",
3081                            xs_html_attr("href", o_href),
3082                            xs_html_text(L("Attachment")),
3083                            xs_html_text(": "),
3084                            xs_html_text(d_href))));
3085
3086                /* do not generate an Alt... */
3087                name = NULL;
3088            }
3089
3090            if (name != NULL && *name) {
3091                xs_html_add(content_attachments,
3092                    xs_html_tag("p",
3093                        xs_html_attr("class", "snac-alt-text"),
3094                        xs_html_tag("details",
3095                            xs_html_tag("summary",
3096                                xs_html_text(L("Alt..."))),
3097                            xs_html_text(name))));
3098            }
3099        }
3100    }
3101
3102    /* has this message an audience (i.e., comes from a channel or community)? */
3103    const char *audience = xs_dict_get(msg, "audience");
3104    if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
3105        xs_html *au_tag = xs_html_tag("p",
3106            xs_html_text("("),
3107            xs_html_tag("a",
3108                xs_html_attr("href", audience),
3109                xs_html_attr("title", L("Source channel or community")),
3110                xs_html_text(audience)),
3111            xs_html_text(")"));
3112
3113        xs_html_add(snac_content_wrap,
3114            au_tag);
3115    }
3116
3117    /* does it have a location? */
3118    const xs_dict *location = xs_dict_get(msg, "location");
3119    if (xs_type(location) == XSTYPE_DICT) {
3120        const xs_number *latitude = xs_dict_get(location, "latitude");
3121        const xs_number *longitude = xs_dict_get(location, "longitude");
3122        const char *name = xs_dict_get(location, "name");
3123        const char *address = xs_dict_get(location, "address");
3124        xs *label_list = xs_list_new();
3125
3126        if (xs_type(name) == XSTYPE_STRING)
3127            label_list = xs_list_append(label_list, name);
3128        if (xs_type(address) == XSTYPE_STRING)
3129            label_list = xs_list_append(label_list, address);
3130
3131        if (xs_list_len(label_list)) {
3132            const char *url = xs_dict_get(location, "url");
3133            xs *label = xs_join(label_list, ", ");
3134
3135            if (xs_type(url) == XSTYPE_STRING) {
3136                xs_html_add(snac_content_wrap,
3137                    xs_html_tag("p",
3138                        xs_html_text(L("Location: ")),
3139                        xs_html_tag("a",
3140                            xs_html_attr("href", url),
3141                            xs_html_attr("target", "_blank"),
3142                            xs_html_text(label))));
3143            }
3144            else
3145            if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
3146                xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
3147                    xs_number_str(latitude), xs_number_str(longitude));
3148
3149                xs_html_add(snac_content_wrap,
3150                    xs_html_tag("p",
3151                        xs_html_text(L("Location: ")),
3152                        xs_html_tag("a",
3153                            xs_html_attr("href", url),
3154                            xs_html_attr("target", "_blank"),
3155                            xs_html_text(label))));
3156            }
3157            else
3158                xs_html_add(snac_content_wrap,
3159                    xs_html_tag("p",
3160                        xs_html_text(L("Location: ")),
3161                        xs_html_text(label)));
3162        }
3163    }
3164
3165    if (strcmp(type, "Event") == 0) { /** Event start and end times **/
3166        const char *s_time = xs_dict_get(msg, "startTime");
3167
3168        if (xs_is_string(s_time) && strlen(s_time) > 20) {
3169            const char *e_time = xs_dict_get(msg, "endTime");
3170            const char *tz     = xs_dict_get(msg, "timezone");
3171
3172            xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
3173            xs *e = NULL;
3174
3175            if (xs_is_string(e_time) && strlen(e_time) > 20)
3176                e = xs_replace_i(xs_dup(e_time), "T", " ");
3177
3178            /* if the event has a timezone, crop the offsets */
3179            if (xs_is_string(tz)) {
3180                s = xs_crop_i(s, 0, 19);
3181
3182                if (e)
3183                    e = xs_crop_i(e, 0, 19);
3184            }
3185            else
3186                tz = "";
3187
3188            /* if start and end share the same day, crop it from the end */
3189            if (e && memcmp(s, e, 11) == 0)
3190                e = xs_crop_i(e, 11, 0);
3191
3192            if (e)
3193                s = xs_str_cat(s, " / ", e);
3194
3195            if (*tz)
3196                s = xs_str_cat(s, " (", tz, ")");
3197
3198            /* replace ugly decimals */
3199            s = xs_replace_i(s, ".000", "");
3200
3201            xs_html_add(snac_content_wrap,
3202                xs_html_tag("p",
3203                    xs_html_text(L("Time: ")),
3204                    xs_html_text(s)));
3205        }
3206    }
3207
3208    /* show all hashtags that has not been shown previously in the content */
3209    const xs_list *tags = xs_dict_get(msg, "tag");
3210    const char *o_content = xs_dict_get_def(msg, "content", "");
3211
3212    if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
3213        xs *content = xs_utf8_to_lower(o_content);
3214        const xs_dict *tag;
3215
3216        xs_html *add_hashtags = xs_html_tag("ul",
3217            xs_html_attr("class", "snac-more-hashtags"));
3218
3219        xs_list_foreach(tags, tag) {
3220            const char *type = xs_dict_get(tag, "type");
3221
3222            if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
3223                const char *o_href = xs_dict_get(tag, "href");
3224                const char *name   = xs_dict_get(tag, "name");
3225
3226                if (xs_is_string(o_href) && xs_is_string(name)) {
3227                    xs *href = xs_utf8_to_lower(o_href);
3228
3229                    if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
3230                        /* not in the content: add here */
3231                        xs_html_add(add_hashtags,
3232                            xs_html_tag("li",
3233                                xs_html_tag("a",
3234                                    xs_html_attr("href", href),
3235                                    xs_html_text(name),
3236                                    xs_html_text(" "))));
3237                    }
3238                }
3239            }
3240        }
3241
3242        xs_html_add(snac_content_wrap,
3243            add_hashtags);
3244    }
3245
3246    /** controls **/
3247
3248    if (!read_only && user) {
3249        xs_html_add(entry,
3250            html_entry_controls(user, actor, msg, md5));
3251    }
3252
3253    /** children **/
3254    if (!hide_children) {
3255        xs *children = object_children(id);
3256        int left     = xs_list_len(children);
3257
3258        if (left) {
3259            xs_html *ch_details = xs_html_tag("details",
3260                xs_html_attr(collapse_threads ? "" : "open", NULL),
3261                xs_html_tag("summary",
3262                    xs_html_text("...")));
3263
3264            xs_html_add(entry,
3265                ch_details);
3266
3267            xs_html *fch_container = xs_html_tag("div",
3268                xs_html_attr("class", "snac-thread-cont"));
3269
3270            xs_html_add(ch_details,
3271                fch_container);
3272
3273            xs_html *ch_container = xs_html_tag("div",
3274                xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
3275
3276            xs_html_add(ch_details,
3277                ch_container);
3278
3279            xs_html *ch_older = NULL;
3280            if (left > 3) {
3281                xs_html_add(ch_container,
3282                    ch_older = xs_html_tag("details",
3283                        xs_html_tag("summary",
3284                            xs_html_text(L("Older...")))));
3285            }
3286
3287            int ctxt = 0;
3288            const char *cmd5;
3289            int cnt = 0;
3290            int o_cnt = 0;
3291            int f_cnt = 0;
3292
3293            /* get the first child */
3294            xs_list_next(children, &cmd5, &ctxt);
3295            xs *f_chd = NULL;
3296
3297            if (user)
3298                timeline_get_by_md5(user, cmd5, &f_chd);
3299            else
3300                object_get_by_md5(cmd5, &f_chd);
3301
3302            if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
3303                const char *p_author = get_atto(msg);
3304                const char *author   = get_atto(f_chd);
3305
3306                /* is the first child from the same author? */
3307                if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
3308                    /* then, don't add it to the children container,
3309                       so that it appears unindented just before the parent
3310                       like a fucking Twitter-like thread */
3311                    xs_html_add(fch_container,
3312                        html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children));
3313
3314                    cnt++;
3315                    f_cnt++;
3316                    left--;
3317                }
3318                else
3319                    ctxt = 0; /* restart from the beginning */
3320            }
3321
3322            while (xs_list_next(children, &cmd5, &ctxt)) {
3323                xs *chd = NULL;
3324
3325                if (user)
3326                    timeline_get_by_md5(user, cmd5, &chd);
3327                else
3328                    object_get_by_md5(cmd5, &chd);
3329
3330                if (chd != NULL) {
3331                    if (xs_is_null(xs_dict_get(chd, "name"))) {
3332                        xs_html *che = html_entry(user, chd, read_only,
3333                            level + 1, cmd5, hide_children);
3334
3335                        if (che != NULL) {
3336                            if (left > 3) {
3337                                xs_html_add(ch_older,
3338                                    che);
3339
3340                                o_cnt++;
3341                            }
3342                            else
3343                                xs_html_add(ch_container,
3344                                    che);
3345
3346                            cnt++;
3347                        }
3348                    }
3349
3350                    left--;
3351                }
3352                else
3353                    srv_debug(2, xs_fmt("cannot read child %s", cmd5));
3354            }
3355
3356            /* if no children were finally added, hide the details */
3357            if (cnt == 0)
3358                xs_html_add(ch_details,
3359                    xs_html_attr("style", "display: none"));
3360
3361            if (o_cnt == 0 && ch_older)
3362                xs_html_add(ch_older,
3363                    xs_html_attr("style", "display: none"));
3364
3365            if (f_cnt == 0)
3366                xs_html_add(fch_container,
3367                    xs_html_attr("style", "display: none"));
3368        }
3369    }
3370
3371    /* add an invisible hr, to help differentiate between posts in text browsers */
3372    xs_html_add(entry_top,
3373        xs_html_sctag("hr",
3374            xs_html_attr("hidden", NULL)));
3375
3376    return entry_top;
3377}
3378
3379
3380xs_html *html_footer(const snac *user)
3381{
3382    return xs_html_tag("div",
3383        xs_html_attr("class", "snac-footer"),
3384        xs_html_tag("a",
3385            xs_html_attr("href", srv_baseurl),
3386            xs_html_text(L("about this site"))),
3387        xs_html_text(" - "),
3388        xs_html_text(L("powered by ")),
3389        xs_html_tag("a",
3390            xs_html_attr("href", WHAT_IS_SNAC_URL),
3391            xs_html_tag("abbr",
3392                xs_html_attr("title", "Social Networks Are Crap"),
3393                xs_html_text(USER_AGENT))));
3394}
3395
3396
3397xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3398                      int skip, int show, int show_more,
3399                      const char *title, const char *page,
3400                      int utl, const char *error, int terse)
3401/* returns the HTML for the timeline */
3402{
3403    xs_list *p = (xs_list *)list;
3404    const char *v;
3405    double t = ftime();
3406    int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
3407
3408    xs *desc = NULL;
3409    xs *alternate = NULL;
3410
3411    if (xs_list_len(list) == 1) {
3412        /* only one element? pick the description from the source */
3413        const char *id = xs_list_get(list, 0);
3414        xs *d = NULL;
3415        object_get_by_md5(id, &d);
3416        const char *sc = xs_dict_get(d, "sourceContent");
3417        if (d && sc != NULL)
3418            desc = xs_dup(sc);
3419
3420        alternate = xs_dup(xs_dict_get(d, "id"));
3421    }
3422
3423    xs_html *head;
3424    xs_html *body;
3425
3426    if (user) {
3427        head = html_user_head(user, desc, alternate);
3428
3429        if (terse)
3430            body = xs_html_tag("body", NULL);
3431        else
3432            body = html_user_body(user, read_only);
3433    }
3434    else {
3435        head = html_instance_head();
3436        body = html_instance_body();
3437    }
3438
3439    xs_html *html = xs_html_tag("html",
3440        head,
3441        body);
3442
3443    if (user && !read_only)
3444        xs_html_add(body,
3445            html_top_controls(user));
3446
3447    if (error != NULL) {
3448        xs_html_add(body,
3449            xs_html_tag("dialog",
3450                xs_html_attr("open", NULL),
3451                xs_html_tag("p",
3452                    xs_html_text(error)),
3453                xs_html_tag("form",
3454                    xs_html_attr("method", "dialog"),
3455                    xs_html_sctag("input",
3456                        xs_html_attr("type", "submit"),
3457                        xs_html_attr("value", L("Dismiss"))))));
3458    }
3459
3460    /* show links to the available lists */
3461    if (user && !read_only) {
3462        xs_html *lol = xs_html_tag("ul",
3463            xs_html_attr("class", "snac-list-of-lists"));
3464        xs_html_add(body, lol);
3465
3466        xs *lists = list_maint(user, NULL, 0); /* get list of lists */
3467
3468        int ct = 0;
3469        const char *v;
3470
3471        while (xs_list_next(lists, &v, &ct)) {
3472            const char *lname = xs_list_get(v, 1);
3473            xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
3474            xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
3475
3476            xs_html_add(lol,
3477                xs_html_tag("li",
3478                    xs_html_tag("a",
3479                        xs_html_attr("href", url),
3480                        xs_html_attr("class", "snac-list-link"),
3481                        xs_html_attr("title", ttl),
3482                        xs_html_text(lname))));
3483        }
3484
3485        {
3486            /* show the list of pinned posts */
3487            xs *url = xs_fmt("%s/pinned", user->actor);
3488            xs_html_add(lol,
3489                xs_html_tag("li",
3490                    xs_html_tag("a",
3491                        xs_html_attr("href", url),
3492                        xs_html_attr("class", "snac-list-link"),
3493                        xs_html_attr("title", L("Pinned posts")),
3494                        xs_html_text(L("pinned")))));
3495        }
3496
3497        {
3498            /* show the list of bookmarked posts */
3499            xs *url = xs_fmt("%s/bookmarks", user->actor);
3500            xs_html_add(lol,
3501                xs_html_tag("li",
3502                    xs_html_tag("a",
3503                        xs_html_attr("href", url),
3504                        xs_html_attr("class", "snac-list-link"),
3505                        xs_html_attr("title", L("Bookmarked posts")),
3506                        xs_html_text(L("bookmarks")))));
3507        }
3508
3509        {
3510            /* show the list of drafts */
3511            xs *url = xs_fmt("%s/drafts", user->actor);
3512            xs_html_add(lol,
3513                xs_html_tag("li",
3514                    xs_html_tag("a",
3515                        xs_html_attr("href", url),
3516                        xs_html_attr("class", "snac-list-link"),
3517                        xs_html_attr("title", L("Post drafts")),
3518                        xs_html_text(L("drafts")))));
3519        }
3520
3521        {
3522            /* show the list of scheduled posts */
3523            xs *url = xs_fmt("%s/sched", user->actor);
3524            xs_html_add(lol,
3525                xs_html_tag("li",
3526                    xs_html_tag("a",
3527                        xs_html_attr("href", url),
3528                        xs_html_attr("class", "snac-list-link"),
3529                        xs_html_attr("title", L("Scheduled posts")),
3530                        xs_html_text(L("scheduled posts")))));
3531        }
3532
3533        /* the list of followed hashtags */
3534        const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
3535
3536        if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
3537            xs_html *loht = xs_html_tag("ul",
3538                xs_html_attr("class", "snac-list-of-lists"));
3539            xs_html_add(body, loht);
3540
3541            const char *ht;
3542
3543            xs_list_foreach(followed_hashtags, ht) {
3544                xs *url = NULL;
3545
3546                if (!xs_startswith(ht, "https:/""/"))
3547                    url = xs_fmt("%s/admin?q=%s", user->actor, ht);
3548                else
3549                    url = xs_dup(ht);
3550
3551                url = xs_replace_i(url, "#", "%23");
3552
3553                xs_html_add(loht,
3554                    xs_html_tag("li",
3555                        xs_html_tag("a",
3556                            xs_html_attr("href", url),
3557                            xs_html_attr("class", "snac-list-link"),
3558                            xs_html_text(ht))));
3559            }
3560        }
3561    }
3562
3563    xs_html_add(body,
3564        xs_html_tag("a",
3565            xs_html_attr("name", "snac-posts")));
3566
3567    xs_html *posts = xs_html_tag("div",
3568        xs_html_attr("class", "snac-posts"));
3569
3570    if (title) {
3571        xs_html_add(posts,
3572            xs_html_tag("h2",
3573                xs_html_attr("class", "snac-header"),
3574                xs_html_text(title)));
3575    }
3576
3577    xs_html_add(body, posts);
3578
3579    int mark_shown = 0;
3580
3581    while (xs_list_iter(&p, &v)) {
3582        xs *msg = NULL;
3583        int status;
3584
3585        /* "already seen" mark? */
3586        if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
3587            if (skip == 0 && !mark_shown) {
3588                xs *s = xs_fmt("%s/admin", user->actor);
3589
3590                xs_html_add(posts,
3591                    xs_html_tag("div",
3592                        xs_html_attr("class", "snac-no-more-unseen-posts"),
3593                        xs_html_text(L("No more unseen posts")),
3594                        xs_html_text(" - "),
3595                        xs_html_tag("a",
3596                            xs_html_attr("href", s),
3597                            xs_html_text(L("Back to top")))));
3598            }
3599
3600            mark_shown = 1;
3601
3602            continue;
3603        }
3604
3605        if (utl && user && !is_pinned_by_md5(user, v))
3606            status = timeline_get_by_md5(user, v, &msg);
3607        else
3608            status = object_get_by_md5(v, &msg);
3609
3610        if (!valid_status(status))
3611            continue;
3612
3613        /* if it's an instance page, discard messages from private users */
3614        if (user == NULL && is_msg_from_private_user(msg))
3615            continue;
3616
3617        const int scope = get_msg_visibility(msg);
3618        if (user != NULL && scope != SCOPE_PUBLIC) {
3619            /* is this message a non-public reply? */
3620            const char *irt = get_in_reply_to(msg);
3621
3622            /* is it a reply to something not in the storage? */
3623            if (!xs_is_null(irt) && !object_here(irt)) {
3624                /* is it for me? */
3625                const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
3626                const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
3627
3628                if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
3629                    snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
3630                    continue;
3631                }
3632            }
3633        }
3634        /* hide non-public posts from /instance view */
3635        if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC)
3636            continue;
3637        /* hide non-public posts viewed from outside */
3638        if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED))
3639            continue;
3640
3641        xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
3642
3643        if (entry != NULL)
3644            xs_html_add(posts,
3645                entry);
3646    }
3647
3648    if (list && user && read_only) {
3649        /** history **/
3650        if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
3651            xs_html *ul = xs_html_tag("ul", NULL);
3652
3653            xs_html *history = xs_html_tag("div",
3654                xs_html_attr("class", "snac-history"),
3655                xs_html_tag("p",
3656                    xs_html_attr("class", "snac-history-title"),
3657                    xs_html_text(L("History"))),
3658                    ul);
3659
3660            xs *list = history_list(user);
3661            xs_list *p = list;
3662            const char *v;
3663
3664            while (xs_list_iter(&p, &v)) {
3665                xs *fn  = xs_replace(v, ".html", "");
3666                xs *url = xs_fmt("%s/h/%s", user->actor, v);
3667
3668                xs_html_add(ul,
3669                    xs_html_tag("li",
3670                        xs_html_tag("a",
3671                            xs_html_attr("href", url),
3672                            xs_html_text(fn))));
3673            }
3674
3675            xs_html_add(body,
3676                history);
3677        }
3678    }
3679
3680    {
3681        xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
3682        xs_html_add(body,
3683            xs_html_raw(s1));
3684    }
3685
3686    if (show_more) {
3687        xs *m  = NULL;
3688        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3689
3690        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3691
3692        if (page != NULL)
3693            url = xs_str_cat(url, page);
3694
3695        if (xs_str_in(url, "?") != -1)
3696            m = xs_fmt("%s&%s", url, ss);
3697        else
3698            m = xs_fmt("%s?%s", url, ss);
3699
3700        xs_html *more_links = xs_html_tag("p",
3701            xs_html_tag("a",
3702                xs_html_attr("href", url),
3703                xs_html_attr("name", "snac-more"),
3704                xs_html_text(L("Back to top"))),
3705            xs_html_text(" - "),
3706            xs_html_tag("a",
3707                xs_html_attr("href", m),
3708                xs_html_attr("name", "snac-more"),
3709                xs_html_text(L("More..."))));
3710
3711        xs_html_add(body,
3712            more_links);
3713    }
3714
3715    xs_html_add(body,
3716        html_footer(user));
3717
3718    return xs_html_render_s(html, "<!DOCTYPE html>\n");
3719}
3720
3721
3722xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int count)
3723{
3724    xs_html *snac_posts;
3725    xs *header_cnt;
3726    if (count > -1)
3727        header_cnt = xs_fmt("%s - %d\n", header, count);
3728    else
3729        header_cnt = xs_fmt("%s\n", header);
3730
3731    xs_html *people = xs_html_tag("div",
3732        xs_html_tag("h2",
3733            xs_html_attr("class", "snac-header"),
3734            xs_html_text(header_cnt)),
3735        snac_posts = xs_html_tag("details",
3736                xs_html_attr("open", NULL),
3737                xs_html_tag("summary",
3738                    xs_html_text("..."))));
3739
3740    xs *redir = xs_fmt("%s/people", user->actor);
3741
3742    const char *actor_id;
3743
3744    xs_list_foreach(list, actor_id) {
3745        xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
3746        xs *actor = NULL;
3747
3748        if (valid_status(actor_get(actor_id, &actor))) {
3749            xs_html *snac_post = xs_html_tag("div",
3750                xs_html_attr("class", "snac-post"),
3751                xs_html_tag("a",
3752                    xs_html_attr("name", md5)),
3753                xs_html_tag("div",
3754                    xs_html_attr("class", "snac-post-header"),
3755                    html_actor_icon(user, actor, xs_dict_get(actor, "published"),
3756                                    NULL, NULL, -1, 1, proxy, NULL, NULL)));
3757
3758            /* content (user bio) */
3759            const char *c = xs_dict_get(actor, "summary");
3760            const xs_val *tag = xs_dict_get(actor, "tag");
3761
3762            if (!xs_is_null(c)) {
3763                xs *sc = sanitize(c);
3764                sc = replace_shortnames(sc, tag, 2, proxy);
3765
3766                xs_html *snac_content = xs_html_tag("div",
3767                    xs_html_attr("class", "snac-content"));
3768
3769                if (xs_startswith(sc, "<p>"))
3770                    xs_html_add(snac_content,
3771                        xs_html_raw(sc)); /* already sanitized */
3772                else
3773                    xs_html_add(snac_content,
3774                        xs_html_tag("p",
3775                            xs_html_raw(sc))); /* already sanitized */
3776
3777                xs_html_add(snac_post, snac_content);
3778            }
3779
3780            /* add user metadata */
3781            xs_html *snac_metadata = xs_html_tag("div",
3782                xs_html_attr("class", "snac-metadata"));
3783
3784            int count = 0;
3785            const xs_val *address = xs_dict_get(actor, "vcard:Address");
3786            if (xs_is_string(address)) {
3787                xs_html_add(snac_metadata,
3788                    xs_html_tag("span",
3789                        xs_html_attr("class", "snac-property-name"),
3790                        xs_html_raw("&#x1F4CD; Location")),
3791                    xs_html_text(":"),
3792                    xs_html_raw("&nbsp;"),
3793                    xs_html_tag("span",
3794                        xs_html_attr("class", "snac-property-value p-adr"),
3795                        xs_html_text(address)),
3796                    xs_html_sctag("br", NULL));
3797
3798                count++;
3799            }
3800
3801            const xs_val *birthday = xs_dict_get(actor, "vcard:bday");
3802            if (xs_is_string(birthday)) {
3803                xs_html_add(snac_metadata,
3804                    xs_html_tag("span",
3805                        xs_html_attr("class", "snac-property-name"),
3806                        xs_html_raw("&#x1F382; Birthday")),
3807                    xs_html_text(":"),
3808                    xs_html_raw("&nbsp;"),
3809                    xs_html_tag("time",
3810                        xs_html_attr("class", "snac-property-value dt-bday"),
3811                        xs_html_text(birthday)),
3812                    xs_html_sctag("br", NULL));
3813
3814                count++;
3815            }
3816
3817            const xs_list *attachment = xs_dict_get(actor, "attachment");
3818            if (count > 0 && xs_list_len(attachment) > 0) {
3819                xs_html_add(snac_metadata,
3820                    xs_html_sctag("hr",
3821                        xs_html_attr("class", "snac-property-divider")));
3822            }
3823
3824            const xs_val *v;
3825            xs_list_foreach(attachment, v) {
3826                const char *type  = xs_dict_get(v, "type");
3827                const char *name  = xs_dict_get(v, "name");
3828                const char *value = xs_dict_get(v, "value");
3829
3830                if (!xs_is_null(type) && !xs_is_null(name) &&
3831                    !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
3832                    /* both the name and the value can contain emoji */
3833                    xs *nam = sanitize(name);
3834                    nam = replace_shortnames(nam, tag, 1, proxy);
3835
3836                    /* todo: sometimes the value is transmitted as markdown and not html ._. */
3837                    xs *val = sanitize(value);
3838                    val = replace_shortnames(val, tag, 1, proxy);
3839
3840                    /* delete <p> tags, because some software sends them */
3841                    val = xs_replace_i(val, "<p>", "");
3842                    val = xs_replace_i(val, "</p>", "");
3843
3844                    xs_html_add(snac_metadata,
3845                        xs_html_tag("span",
3846                            xs_html_attr("class", "snac-property-name"),
3847                            xs_html_raw(nam)),
3848                        xs_html_text(":"),
3849                        xs_html_raw("&nbsp;"),
3850                        xs_html_tag("span",
3851                            xs_html_attr("class", "snac-property-value"),
3852                            xs_html_raw(val)),
3853                        xs_html_sctag("br", NULL));
3854
3855                    count++;
3856                }
3857            }
3858
3859            if (count > 0) {
3860                xs_html_add(snac_post, snac_metadata);
3861            }
3862            else {
3863                /* free the html, by rendering it... */
3864                xs_free(xs_html_render(snac_metadata));
3865            }
3866
3867            /* buttons */
3868            xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
3869
3870            xs_html *snac_controls = xs_html_tag("div",
3871                xs_html_attr("class",   "snac-controls"));
3872
3873            xs_html *form = xs_html_tag("form",
3874                xs_html_attr("autocomplete",    "off"),
3875                xs_html_attr("method",          "post"),
3876                xs_html_attr("action",          btn_form_action),
3877                xs_html_sctag("input",
3878                    xs_html_attr("type",    "hidden"),
3879                    xs_html_attr("name",    "actor"),
3880                    xs_html_attr("value",   actor_id)),
3881                xs_html_sctag("input",
3882                    xs_html_attr("type",    "hidden"),
3883                    xs_html_attr("name",    "hard-redir"),
3884                    xs_html_attr("value",   redir)),
3885                xs_html_sctag("input",
3886                    xs_html_attr("type",    "hidden"),
3887                    xs_html_attr("name",    "actor-form"),
3888                    xs_html_attr("value",   "yes")));
3889
3890            xs_html_add(snac_controls, form);
3891
3892            if (following_check(user, actor_id)) {
3893                xs_html_add(form,
3894                    html_button("unfollow", L("Unfollow"),
3895                                L("Stop following this user's activity")));
3896
3897                if (is_limited(user, actor_id))
3898                    xs_html_add(form,
3899                        html_button("unlimit", L("Unlimit"),
3900                                L("Allow announces (boosts) from this user")));
3901                else
3902                    xs_html_add(form,
3903                        html_button("limit", L("Limit"),
3904                                L("Block announces (boosts) from this user")));
3905            }
3906            else {
3907                xs_html_add(form,
3908                    html_button("follow", L("Follow"),
3909                                L("Start following this user's activity")));
3910
3911                if (follower_check(user, actor_id))
3912                    xs_html_add(form,
3913                        html_button("delete", L("Delete"), L("Delete this user")));
3914            }
3915
3916            if (pending_check(user, actor_id)) {
3917                xs_html_add(form,
3918                    html_button("approve", L("Approve"),
3919                                L("Approve this follow request")));
3920
3921                xs_html_add(form,
3922                    html_button("discard", L("Discard"), L("Discard this follow request")));
3923            }
3924
3925            if (is_muted(user, actor_id))
3926                xs_html_add(form,
3927                    html_button("unmute", L("Unmute"),
3928                                L("Stop blocking activities from this user")));
3929            else
3930                xs_html_add(form,
3931                    html_button("mute", L("MUTE"),
3932                                L("Block any activity from this user")));
3933
3934            /* the post textarea */
3935            xs *dm_div_id  = xs_fmt("%s_%s_dm", md5, t);
3936            xs *dm_form_id = xs_fmt("%s_reply_form", md5);
3937
3938            xs_html_add(snac_controls,
3939                xs_html_tag("p", NULL),
3940                html_note(user, L("Direct Message..."),
3941                    dm_div_id, dm_form_id,
3942                    "", "",
3943                    NULL, actor_id,
3944                    xs_stock(XSTYPE_FALSE), "",
3945                    SCOPE_MENTIONED, NULL,
3946                    NULL, 0, NULL, NULL, 0, NULL, NULL),
3947                xs_html_tag("p", NULL));
3948
3949            xs_html_add(snac_post, snac_controls);
3950
3951            xs_html_add(snac_posts, snac_post);
3952        }
3953    }
3954
3955    return people;
3956}
3957
3958
3959xs_str *html_people(snac *user)
3960{
3961    const char *proxy = NULL;
3962
3963    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3964        proxy = user->actor;
3965
3966    xs *wing = following_list(user);
3967    xs *wers = follower_list(user);
3968    xs *pending = pending_list(user);
3969
3970    xs_html *lists = xs_html_tag("div",
3971        xs_html_attr("class", "snac-posts"));
3972
3973    if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
3974        xs_html_add(lists,
3975            html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, xs_list_len(pending)));
3976    }
3977
3978    xs_html_add(lists,
3979        html_people_list(user, wing, L("People you follow"), "i", proxy, following_list_len(user)),
3980        html_people_list(user, wers, L("People that follow you"), "e", proxy, xs_list_len(wers)));
3981
3982    xs_html *html = xs_html_tag("html",
3983        html_user_head(user, NULL, NULL),
3984        xs_html_add(html_user_body(user, 0),
3985            lists,
3986            html_footer(user)));
3987
3988    return xs_html_render_s(html, "<!DOCTYPE html>\n");
3989}
3990
3991/* Filter list to display only posts by actor.  We'll probably show
3992   fewer than show posts.  Should we try harder to find some?  */
3993xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3994                        int skip, int show, int show_more, const char *page)
3995{
3996    const char *proxy = NULL;
3997    xs_list *p = (xs_list *)list;
3998    const char *v;
3999
4000    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4001        proxy = user->actor;
4002
4003    xs_html *body = html_user_body(user, 0);
4004
4005    xs_html *lists = xs_html_tag("div",
4006        xs_html_attr("class", "snac-posts"));
4007
4008    xs *foll = xs_list_append(xs_list_new(), actor);
4009
4010    xs_html_add(lists,
4011        html_people_list(user, foll, L("Contact's posts"), "p", proxy, -1));
4012
4013    xs_html_add(body, lists);
4014
4015    while (xs_list_iter(&p, &v)) {
4016        xs *msg = NULL;
4017        int status;
4018
4019        status = timeline_get_by_md5(user, v, &msg);
4020
4021        if (!valid_status(status))
4022            continue;
4023
4024        const char *id = xs_dict_get(msg, "id");
4025        const char *by = get_atto(msg);
4026        xs *actor_md5 = NULL;
4027        xs_list *boosts = NULL;
4028        xs_list *likes = NULL;
4029        xs_list *reacts = NULL;
4030        /* Besides actor's posts, also show actor's boosts, and also
4031           posts by user with likes or reacts by actor.  I.e., any
4032           actor's actions that user could have seen in the timeline
4033           or in notifications.  */
4034        if (!(by && strcmp(actor, by) == 0) &&
4035            xs_list_in((boosts = object_announces(id)),
4036                       (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
4037            (!(by && strcmp(user->actor, by) == 0) ||
4038             (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
4039              xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
4040            continue;
4041
4042        xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
4043
4044        if (entry != NULL)
4045            xs_html_add(lists,
4046                entry);
4047    }
4048
4049    if (show_more) {
4050        xs *m  = NULL;
4051        xs *m10  = NULL;
4052        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
4053
4054        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
4055
4056        if (page != NULL)
4057            url = xs_str_cat(url, page);
4058
4059        if (xs_str_in(url, "?") != -1)
4060            m = xs_fmt("%s&%s", url, ss);
4061        else
4062            m = xs_fmt("%s?%s", url, ss);
4063
4064        m10 = xs_fmt("%s0", m);
4065
4066        xs_html *more_links = xs_html_tag("p",
4067            xs_html_tag("a",
4068                xs_html_attr("href", url),
4069                xs_html_attr("name", "snac-more"),
4070                xs_html_text(L("Back to top"))),
4071            xs_html_text(" - "),
4072            xs_html_tag("a",
4073                xs_html_attr("href", m),
4074                xs_html_attr("name", "snac-more"),
4075                xs_html_text(L("More..."))),
4076            xs_html_text(" - "),
4077            xs_html_tag("a",
4078                xs_html_attr("href", m10),
4079                xs_html_attr("name", "snac-more"),
4080                xs_html_text(L("More (x 10)..."))));
4081
4082        xs_html_add(body,
4083            more_links);
4084    }
4085
4086    xs_html *html = xs_html_tag("html",
4087        html_user_head(user, NULL, NULL),
4088        xs_html_add(body,
4089            html_footer(user)));
4090
4091    return xs_html_render_s(html, "<!DOCTYPE html>\n");
4092}
4093
4094void notify_filter(snac *user, const xs_dict *p_vars)
4095/* sets filter for notifications */
4096{
4097    const char *v;
4098    int likes_on  = (v = xs_dict_get(p_vars, "likes_on")) ? strcmp(v, "on") == 0 : 0;
4099    int reacts_on = (v = xs_dict_get(p_vars, "reacts_on")) ? strcmp(v, "on") == 0 : 0;
4100    int ments_on  = (v = xs_dict_get(p_vars, "mentions_on")) ? strcmp(v, "on") == 0 : 0;
4101    int ann_on    = (v = xs_dict_get(p_vars, "announces_on")) ? strcmp(v, "on") == 0 : 0;
4102    int foll_on   = (v = xs_dict_get(p_vars, "follows_on")) ? strcmp(v, "on") == 0 : 0;
4103    int unfoll_on = (v = xs_dict_get(p_vars, "unfollows_on")) ? strcmp(v, "on") == 0 : 0;
4104    int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0;
4105    int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0;
4106    int polls_on  = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0;
4107    xs_dict *filter = xs_dict_new();
4108    filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4109    filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4110    filter = xs_dict_set(filter, "mentions", xs_stock(ments_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4111    filter = xs_dict_set(filter, "announces", xs_stock(ann_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4112    filter = xs_dict_set(filter, "follows", xs_stock(foll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4113    filter = xs_dict_set(filter, "unfollows", xs_stock(unfoll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4114    filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4115    filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4116    filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4117    user->config = xs_dict_set(user->config, "notify_filter", filter);
4118}
4119
4120xs_str *html_notifications(snac *user, int skip, int show)
4121{
4122    const char *proxy = NULL;
4123
4124    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4125        proxy = user->actor;
4126
4127    xs *n_list_unfilt = notify_list(user, skip, show);
4128    xs *n_time = notify_check_time(user, 0);
4129
4130    xs_html *body = html_user_body(user, 0);
4131    const xs_dict *n_filter = xs_dict_get(user->config, "notify_filter");
4132    if (!n_filter) {
4133        user->config = xs_dict_set(user->config, "notify_filter", xs_dict_new());
4134        n_filter = xs_dict_get(user->config, "notify_filter");
4135    }
4136    xs *n_list = notify_filter_list(user, n_list_unfilt);
4137    /* all filters are true by default */
4138    const xs_val *n_def = xs_stock( XSTYPE_TRUE );
4139    int n_likes_on  = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
4140    int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
4141    int n_ments_on  = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
4142    int n_ann_on    = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
4143    int n_fol_on    = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
4144    int n_unfol_on  = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
4145    int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
4146    int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
4147    int n_polls_on  = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
4148
4149    xs_html *html = xs_html_tag("html",
4150        html_user_head(user, NULL, NULL),
4151        body);
4152
4153    xs *filter_notifs_action = xs_fmt("%s/admin/filter-notifications", user->actor);
4154    xs_html *notifs_form = xs_html_tag("form",
4155        xs_html_attr("autocomplete", "off"),
4156        xs_html_attr("method",       "post"),
4157        xs_html_attr("action",       filter_notifs_action),
4158        xs_html_attr("enctype",      "multipart/form-data"),
4159        xs_html_attr("id",           "filter"),
4160        xs_html_sctag("input",
4161            xs_html_attr("type",    "hidden"),
4162            xs_html_attr("name",    "hard-redir"),
4163            xs_html_attr("value",   xs_fmt("%s/notifications", user->actor))),
4164        html_checkbox("likes_on", L("Likes"), n_likes_on),
4165        html_checkbox("reacts_on", L("Emoji reacts"), n_reacts_on),
4166        html_checkbox("mentions_on", L("Mentions"), n_ments_on),
4167        html_checkbox("announces_on", L("Announces"), n_ann_on),
4168        html_checkbox("follows_on", L("Follows"), n_fol_on),
4169        html_checkbox("unfollows_on", L("Unfollows"), n_unfol_on),
4170        html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on),
4171        html_checkbox("blocks_on", L("Blocks"), n_blocks_on),
4172        html_checkbox("polls_on", L("Polls"), n_polls_on),
4173        xs_html_sctag("input",
4174            xs_html_attr("type",     "submit"),
4175            xs_html_attr("class",    "button"),
4176            xs_html_attr("value",    L("Save"))));
4177
4178    xs_html_add(body,
4179        xs_html_tag("p",
4180            xs_html_tag("div",
4181                xs_html_attr("class", "snac-notify-filter"),
4182                xs_html_tag("details",
4183                    xs_html_tag("summary",
4184                        xs_html_text(L("Notifications filter..."))),
4185                    notifs_form))));
4186
4187    xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
4188
4189    xs_html_add(body,
4190        xs_html_tag("form",
4191            xs_html_attr("autocomplete", "off"),
4192            xs_html_attr("method",       "post"),
4193            xs_html_attr("action",       clear_all_action),
4194            xs_html_attr("id",           "clear"),
4195            xs_html_sctag("input",
4196                xs_html_attr("type",     "submit"),
4197                xs_html_attr("class",    "snac-btn-like"),
4198                xs_html_attr("value",    L("Clear all")))));
4199
4200    xs_html *noti_new = NULL;
4201    xs_html *noti_seen = NULL;
4202
4203    xs_html *posts = xs_html_tag("div",
4204        xs_html_attr("class", "snac-posts"));
4205    xs_html_add(body, posts);
4206
4207    xs_set rep;
4208    xs_set_init(&rep);
4209
4210    /* dict to store previous notification labels */
4211    xs *admiration_labels = xs_dict_new();
4212
4213    const xs_str *v;
4214
4215    xs_list_foreach(n_list, v) {
4216        xs *noti = notify_get(user, v);
4217
4218        if (noti == NULL)
4219            continue;
4220
4221        xs *obj = NULL;
4222        const char *type  = xs_dict_get(noti, "type");
4223        const char *utype = xs_dict_get(noti, "utype");
4224        const char *id    = xs_dict_get(noti, "objid");
4225        const char *date  = xs_dict_get(noti, "date");
4226        const char *id2   = xs_dict_get_path(noti, "msg.id");
4227        xs *wrk = NULL;
4228
4229        if (xs_is_null(id))
4230            continue;
4231
4232        if (is_hidden(user, id))
4233            continue;
4234
4235        if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
4236            continue;
4237
4238        if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
4239            continue;
4240
4241        object_get(id, &obj);
4242
4243        const char *msg_id = NULL;
4244
4245        if (xs_is_dict(obj))
4246            msg_id = xs_dict_get(obj, "id");
4247
4248        const char *actor_id = xs_dict_get(noti, "actor");
4249        xs *actor = NULL;
4250        xs *a_name = NULL;
4251
4252        if (valid_status(actor_get(actor_id, &actor)))
4253            a_name = actor_name(actor, proxy);
4254        else {
4255            a_name = xs_dup(actor_id);
4256
4257            /* actor not here: request it */
4258            enqueue_actor_refresh(user, actor_id, 0);
4259        }
4260
4261        const char *html_url = xs_dict_get_def(actor, "url", actor_id);
4262
4263        if (xs_is_list(html_url))
4264            html_url = xs_list_get(html_url, 0);
4265
4266        if (!xs_is_string(html_url))
4267            html_url = actor_id;
4268
4269        xs *label_sanitized = sanitize(type);
4270        const char *label = label_sanitized;
4271
4272        if (strcmp(type, "Create") == 0)
4273            label = L("Mention");
4274        else
4275        if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
4276            label = L("Finished poll");
4277        else
4278        if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
4279            label = L("Unfollow");
4280        else
4281        if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
4282            const char *content = xs_dict_get_path(noti, "msg.content");
4283
4284            xs *cd = xs_dup(content);
4285            const char *sna = cd;
4286            const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
4287            unsigned int utf = xs_utf8_dec((const char **)&sna);
4288
4289            int isEmoji = 0;
4290            if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
4291                isEmoji = 1;
4292
4293            if (xs_type(content) == XSTYPE_STRING) {
4294                if (!isEmoji && !n_likes_on)
4295                    continue;
4296                xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
4297                wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
4298                label = wrk;
4299            }
4300        }
4301        else
4302        if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
4303            label = L("Follow Request");
4304
4305        xs *s_date = html_date_label(user, date);
4306
4307        xs_html *this_html_label = xs_html_container(
4308                xs_html_tag("b",
4309                    xs_html_raw(label),
4310                    xs_html_text(" by "),
4311                    xs_html_tag("a",
4312                        xs_html_attr("href", html_url),
4313                        xs_html_raw(a_name))), /* a_name is already sanitized */
4314                xs_html_text(" "),
4315                xs_html_tag("time",
4316                    xs_html_attr("class", "dt-published snac-pubdate"),
4317                    xs_html_attr("title", date),
4318                    xs_html_text(s_date)));
4319
4320        xs_html *html_label = NULL;
4321
4322        if (xs_is_string(msg_id)) {
4323            const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
4324
4325            if (xs_type(prev_label) == XSTYPE_DATA) {
4326                /* there is a previous list of admiration labels! */
4327                xs_data_get(&html_label, prev_label);
4328
4329                xs_html_add(html_label,
4330                    xs_html_sctag("br", NULL),
4331                    this_html_label);
4332
4333                continue;
4334            }
4335        }
4336
4337        xs_html *entry = NULL;
4338
4339        html_label = xs_html_tag("p",
4340            this_html_label);
4341
4342        /* store in the admiration labels dict */
4343        xs *pl = xs_data_new(&html_label, sizeof(html_label));
4344
4345        if (xs_is_string(msg_id))
4346            admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
4347
4348        entry = xs_html_tag("div",
4349            xs_html_attr("class", "snac-post-with-desc"),
4350            html_label);
4351
4352        if (strcmp(type, "Block") == 0) {
4353            if (actor)
4354                xs_html_add(entry,
4355                    xs_html_tag("div",
4356                        xs_html_attr("class", "snac-post"),
4357                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4358        }
4359        else
4360        if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
4361            if (actor) {
4362                xs *action = xs_fmt("%s/admin/action", user->actor);
4363                xs_html *button = NULL;
4364
4365                if (following_check(user, actor_id))
4366                    button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"));
4367                else
4368                    button = html_button("follow", L("Follow"), L("Start following this user's activity"));
4369
4370                xs_html_add(entry,
4371                    xs_html_tag("div",
4372                        xs_html_attr("class", "snac-post"),
4373                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL),
4374                        xs_html_tag("form",
4375                            xs_html_attr("method", "post"),
4376                            xs_html_attr("action", action),
4377                            xs_html_sctag("input",
4378                                xs_html_attr("type", "hidden"),
4379                                xs_html_attr("name", "actor"),
4380                                xs_html_attr("value", actor_id)),
4381                            button,
4382                            xs_html_sctag("br", NULL))));
4383            }
4384        }
4385        else
4386        if (strcmp(type, "Move") == 0) {
4387            const xs_dict *o_msg = xs_dict_get(noti, "msg");
4388            const char *target;
4389
4390            if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
4391                xs *old_actor = NULL;
4392
4393                if (valid_status(actor_get(target, &old_actor))) {
4394                    xs_html_add(entry,
4395                        xs_html_tag("div",
4396                            xs_html_attr("class", "snac-post"),
4397                            html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4398                }
4399            }
4400        }
4401        else
4402        if (obj != NULL) {
4403            xs *md5 = xs_md5_hex(id, strlen(id));
4404            xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
4405
4406            xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
4407
4408            if (h != NULL) {
4409                xs_html_add(entry,
4410                    xs_html_tag("p",
4411                        xs_html_tag("a",
4412                            xs_html_attr("href", ctxt),
4413                            xs_html_text(L("Context")))),
4414                    h);
4415            }
4416            else
4417                xs_html_add(entry,
4418                    xs_html_tag("p",
4419                        xs_html_text(L("Location: ")),
4420                        xs_html_tag("a",
4421                            xs_html_attr("href", id),
4422                            xs_html_text(id))));
4423        }
4424
4425        if (strcmp(v, n_time) > 0) {
4426            /* unseen notification */
4427            if (noti_new == NULL) {
4428                noti_new = xs_html_tag("div",
4429                    xs_html_tag("h2",
4430                        xs_html_attr("class", "snac-header"),
4431                        xs_html_text(L("New"))));
4432
4433                xs_html_add(posts,
4434                    noti_new);
4435            }
4436
4437            xs_html_add(noti_new,
4438                entry);
4439        }
4440        else {
4441            /* already seen notification */
4442            if (noti_seen == NULL) {
4443                noti_seen = xs_html_tag("div",
4444                    xs_html_tag("h2",
4445                        xs_html_attr("class", "snac-header"),
4446                        xs_html_text(L("Already seen"))));
4447
4448                xs_html_add(posts,
4449                    noti_seen);
4450            }
4451
4452            xs_html_add(noti_seen,
4453                entry);
4454        }
4455    }
4456
4457    if (noti_new == NULL && noti_seen == NULL)
4458        xs_html_add(body,
4459            xs_html_tag("h2",
4460                xs_html_attr("class", "snac-header"),
4461                xs_html_text(L("None"))));
4462
4463    /* add the navigation footer */
4464    xs *next_p = notify_list(user, skip + show, 1);
4465    if (xs_list_len(next_p)) {
4466        xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
4467            user->actor, skip + show, show);
4468
4469        xs_html_add(body,
4470            xs_html_tag("p",
4471                xs_html_tag("a",
4472                    xs_html_attr("href", url),
4473                    xs_html_text(L("More...")))));
4474    }
4475
4476    xs_set_free(&rep);
4477
4478    xs_html_add(body,
4479        html_footer(user));
4480
4481    /* set the check time to now */
4482    xs *dummy = notify_check_time(user, 1);
4483    dummy = xs_free(dummy);
4484
4485    timeline_touch(user);
4486
4487    return xs_html_render_s(html, "<!DOCTYPE html>\n");
4488}
4489
4490
4491void set_user_lang(snac *user)
4492/* sets the language dict according to user configuration */
4493{
4494    user->lang = NULL;
4495    const char *lang = xs_dict_get(user->config, "lang");
4496
4497    if (xs_is_string(lang))
4498        user->lang = xs_dict_get(srv_langs, lang);
4499}
4500
4501
4502int html_get_handler(const xs_dict *req, const char *q_path,
4503                     char **body, int *b_size, char **ctype,
4504                     xs_str **etag, xs_str **last_modified)
4505{
4506    const char *accept = xs_dict_get(req, "accept");
4507    int status = HTTP_STATUS_NOT_FOUND;
4508    const snac *user = NULL;
4509    snac snac;
4510    xs *uid = NULL;
4511    const char *p_path;
4512    int cache = 1;
4513    int save = 1;
4514    int proxy = 0;
4515    int terse = 0;
4516    const char *v;
4517
4518    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
4519
4520    xs *l = xs_split_n(q_path, "/", 2);
4521    v = xs_list_get(l, 1);
4522
4523    if (xs_is_null(v)) {
4524        srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
4525        return HTTP_STATUS_NOT_FOUND;
4526    }
4527
4528    if (!xs_is_null(xs_dict_get(q_vars, "terse")))
4529        terse = 1;
4530
4531    if (strcmp(v, "share-bridge") == 0) {
4532        /* temporary redirect for a post */
4533        const char *login = xs_dict_get(q_vars, "login");
4534        const char *content = xs_dict_get(q_vars, "content");
4535
4536        if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
4537            xs *b64 = xs_base64_enc(content, strlen(content));
4538
4539            srv_log(xs_fmt("share-bridge for user '%s'", login));
4540
4541            *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
4542            return HTTP_STATUS_SEE_OTHER;
4543        }
4544        else
4545            return HTTP_STATUS_NOT_FOUND;
4546    }
4547    else
4548    if (strcmp(v, "auth-int-bridge") == 0) {
4549        const char *login  = xs_dict_get(q_vars, "login");
4550        const char *id     = xs_dict_get(q_vars, "id");
4551        const char *action = xs_dict_get(q_vars, "action");
4552
4553        if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
4554            *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
4555                srv_baseurl, login, action, id);
4556
4557            return HTTP_STATUS_SEE_OTHER;
4558        }
4559        else
4560            return HTTP_STATUS_NOT_FOUND;
4561    }
4562
4563    uid = xs_dup(v);
4564
4565    /* rss extension? */
4566    if (xs_endswith(uid, ".rss")) {
4567        uid = xs_crop_i(uid, 0, -4);
4568        p_path = ".rss";
4569    }
4570    else
4571        p_path = xs_list_get(l, 2);
4572
4573    if (!uid || !user_open(&snac, uid)) {
4574        /* invalid user */
4575        status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
4576        srv_debug(1, xs_fmt("html_get_handler bad user %s %d", uid, status));
4577        return status;
4578    }
4579
4580    user = &snac; /* for L() */
4581    set_user_lang(&snac);
4582
4583    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4584        proxy = 1;
4585
4586    /* return the RSS if requested by Accept header */
4587    if (accept != NULL) {
4588        if (xs_str_in(accept, "text/xml") != -1 ||
4589            xs_str_in(accept, "application/rss+xml") != -1)
4590            p_path = ".rss";
4591    }
4592
4593    /* check if server config variable 'disable_cache' is set */
4594    if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
4595        cache = 0;
4596
4597    int skip = 0;
4598    const char *max_show_default = "50";
4599    int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
4600                                 max_show_default));
4601    int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
4602                                 xs_dict_get_def(srv_config, "max_timeline_entries",
4603                                 max_show_default)));
4604    int show = def_show;
4605
4606    if ((v = xs_dict_get(q_vars, "skip")) != NULL)
4607        skip = atoi(v), cache = 0, save = 0;
4608    if ((v = xs_dict_get(q_vars, "show")) != NULL)
4609        show = atoi(v), cache = 0, save = 0;
4610    if ((v = xs_dict_get(q_vars, "da")) != NULL) {
4611        /* user dismissed an announcement */
4612        if (login(&snac, req)) {
4613            double ts = atof(v);
4614            xs *timestamp = xs_number_new(ts);
4615            srv_log(xs_fmt("user dismissed announcements until %d", ts));
4616            snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
4617            user_persist(&snac, 0);
4618        }
4619    }
4620
4621    /* get a possible error message */
4622    const char *error = xs_dict_get(q_vars, "error");
4623    if (error != NULL)
4624        cache = 0;
4625
4626    /* a show of 0 has no sense */
4627    if (show == 0)
4628        show = def_show;
4629    if (show > max_show)
4630        show = max_show;
4631
4632    if (p_path == NULL) { /** public timeline **/
4633        xs *h = xs_str_localtime(0, "%Y-%m.html");
4634
4635        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
4636            /** empty public timeline for private users **/
4637            *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse);
4638            *b_size = strlen(*body);
4639            status  = HTTP_STATUS_OK;
4640        }
4641        else
4642        if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
4643            snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
4644
4645            status = history_get(&snac, h, body, b_size,
4646                        xs_dict_get(req, "if-none-match"), etag);
4647        }
4648        else {
4649            xs *list = NULL;
4650            int more = 0;
4651
4652            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
4653                list = timeline_simple_list(&snac, "public", skip, show, &more);
4654            else
4655                list = timeline_list(&snac, "public", skip, show, &more);
4656
4657            xs *pins = pinned_list(&snac);
4658            pins = xs_list_cat(pins, list);
4659
4660            *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse);
4661
4662            *b_size = strlen(*body);
4663            status  = HTTP_STATUS_OK;
4664
4665            if (save)
4666                history_add(&snac, h, *body, *b_size, etag);
4667        }
4668    }
4669    else
4670    if (strcmp(p_path, "admin") == 0) { /** private timeline **/
4671        if (!login(&snac, req)) {
4672            *body  = xs_dup(uid);
4673            status = HTTP_STATUS_UNAUTHORIZED;
4674        }
4675        else {
4676            const char *q = NULL;
4677            xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
4678            xs *url_acct = NULL;
4679
4680            if (xs_is_string(cq)) {
4681                cq = xs_strip_i(cq);
4682                q = cq;
4683            }
4684
4685            /* searching for an URL? */
4686            if (q && xs_match(q, "https://*|http://*")) {
4687                /* may by an actor; try a webfinger */
4688                xs *actor_obj = NULL;
4689
4690                if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) {
4691                    /* it's an actor; do the dirty trick of changing q to the account name */
4692                    q = url_acct;
4693                }
4694                else {
4695                    /* bring it to the user's timeline */
4696                    xs *object = NULL;
4697                    int status;
4698
4699                    status = activitypub_request(&snac, q, &object);
4700                    snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
4701
4702                    if (valid_status(status)) {
4703                        /* got it; also request the actor */
4704                        const char *attr_to = get_atto(object);
4705
4706                        if (!xs_is_null(attr_to)) {
4707                            status = actor_request(&snac, attr_to, &actor_obj);
4708
4709                            if (valid_status(status)) {
4710                                /* reset the query string to be the real id */
4711                                url_acct = xs_dup(xs_dict_get(object, "id"));
4712                                q = url_acct;
4713
4714                                /* add the post to the timeline */
4715                                if (!timeline_here(&snac, q))
4716                                    timeline_add(&snac, q, object);
4717                            }
4718                        }
4719                        else {
4720                            /* retry webfinger, this time with the 'official' id */
4721                            const char *id = xs_dict_get(object, "id");
4722
4723                            if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) &&
4724                                xs_is_string(url_acct))
4725                                q = url_acct;
4726                        }
4727                    }
4728                }
4729
4730                /* fall through */
4731            }
4732
4733            if (q && *q) {
4734                if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
4735                    /** search account **/
4736                    xs *actor = NULL;
4737                    xs *acct = NULL;
4738                    xs *l = xs_list_new();
4739                    xs_html *page = NULL;
4740
4741                    if (valid_status(webfinger_request(q, &actor, &acct))) {
4742                        xs *actor_obj = NULL;
4743
4744                        if (valid_status(actor_request(&snac, actor, &actor_obj))) {
4745                            actor_add(actor, actor_obj);
4746
4747                            /* create a people list with only one element */
4748                            l = xs_list_append(l, actor);
4749
4750                            xs *title = xs_fmt(L("Search results for account %s"), q);
4751
4752                            page = html_people_list(&snac, l, title, "wf", NULL, 1);
4753                        }
4754                    }
4755
4756                    if (page == NULL) {
4757                        xs *title = xs_fmt(L("Account %s not found"), q);
4758
4759                        page = xs_html_tag("div",
4760                            xs_html_tag("h2",
4761                                xs_html_attr("class", "snac-header"),
4762                                xs_html_text(title)));
4763                    }
4764
4765                    xs_html *html = xs_html_tag("html",
4766                        html_user_head(&snac, NULL, NULL),
4767                        xs_html_add(html_user_body(&snac, 0),
4768                        page,
4769                        html_footer(user)));
4770
4771                    *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
4772                    *b_size = strlen(*body);
4773                    status = HTTP_STATUS_OK;
4774                }
4775                else
4776                if (*q == '#') {
4777                    /** search by tag **/
4778                    xs *tl = tag_search(q, skip, show + 1);
4779                    int more = 0;
4780                    if (xs_list_len(tl) >= show + 1) {
4781                        /* drop the last one */
4782                        tl = xs_list_del(tl, -1);
4783                        more = 1;
4784                    }
4785
4786                    xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
4787                    xs *title = xs_fmt(xs_list_len(tl) ?
4788                        L("Search results for tag %s") : L("Nothing found for tag %s"), q);
4789
4790                    *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse);
4791                    *b_size = strlen(*body);
4792                    status  = HTTP_STATUS_OK;
4793                }
4794                else {
4795                    /** search by content **/
4796                    int to = 0;
4797                    int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
4798                    xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
4799                    xs *title = NULL;
4800                    xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
4801                    int tl_len = xs_list_len(tl);
4802
4803                    if (to)
4804                        title = xs_fmt(L("Search results for '%s' (may be more)"), q);
4805                    else
4806                    if (tl_len)
4807                        title = xs_fmt(L("Search results for '%s'"), q);
4808                    else
4809                    if (skip)
4810                        title = xs_fmt(L("No more matches for '%s'"), q);
4811                    else
4812                        title = xs_fmt(L("Nothing found for '%s'"), q);
4813
4814                    *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
4815                                            title, page, 0, error, terse);
4816                    *b_size = strlen(*body);
4817                    status  = HTTP_STATUS_OK;
4818                }
4819            }
4820            else {
4821                /** the private timeline **/
4822                double t = history_mtime(&snac, "timeline.html_");
4823
4824                /* if enabled by admin, return a cached page if its timestamp is:
4825                   a) newer than the timeline timestamp
4826                   b) newer than the start time of the server
4827                */
4828                if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
4829                    snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
4830
4831                    status = history_get(&snac, "timeline.html_", body, b_size,
4832                                xs_dict_get(req, "if-none-match"), etag);
4833                }
4834                else {
4835                    int more = 0;
4836
4837                    snac_debug(&snac, 1, xs_fmt("building timeline"));
4838
4839                    xs *list = timeline_list(&snac, "private", skip, show, &more);
4840
4841                    *body = html_timeline(&snac, list, 0, skip, show,
4842                            more, NULL, "/admin", 1, error, terse);
4843
4844                    *b_size = strlen(*body);
4845                    status  = HTTP_STATUS_OK;
4846
4847                    if (save)
4848                        history_add(&snac, "timeline.html_", *body, *b_size, etag);
4849
4850                    timeline_add_mark(&snac);
4851                }
4852            }
4853        }
4854    }
4855    else
4856    if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
4857        if (!login(&snac, req)) {
4858            *body  = xs_dup(uid);
4859            status = HTTP_STATUS_UNAUTHORIZED;
4860        }
4861        else {
4862            xs *l = xs_split(p_path, "/");
4863            const char *md5 = xs_list_get(l, -1);
4864
4865            if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) {
4866                xs *list0 = xs_list_append(xs_list_new(), md5);
4867                xs *list  = timeline_top_level(&snac, list0);
4868
4869                *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse);
4870                *b_size = strlen(*body);
4871                status  = HTTP_STATUS_OK;
4872            }
4873        }
4874    }
4875    else
4876    if (strcmp(p_path, "people") == 0) { /** the list of people **/
4877        if (!login(&snac, req)) {
4878            *body  = xs_dup(uid);
4879            status = HTTP_STATUS_UNAUTHORIZED;
4880        }
4881        else {
4882            *body   = html_people(&snac);
4883            *b_size = strlen(*body);
4884            status  = HTTP_STATUS_OK;
4885        }
4886    }
4887    else
4888    if (xs_startswith(p_path, "people/")) { /** a single actor **/
4889        if (!login(&snac, req)) {
4890            *body  = xs_dup(uid);
4891            status = HTTP_STATUS_UNAUTHORIZED;
4892        }
4893        else {
4894            xs *actor_dict = NULL;
4895            const char *actor_id = NULL;
4896            xs *actor = NULL;
4897            xs_list *page_lst = xs_split_n(p_path, "?", 2);
4898            xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
4899            xs_list *l = xs_split_n(page, "/", 3);
4900            const char *actor_md5 = xs_list_get(l, 2);
4901
4902            if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
4903               (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
4904               valid_status(actor_get(actor_id, &actor))) {
4905                int more = 0;
4906                xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
4907
4908                *body   = html_people_one(&snac, actor_id, list, skip, show, more, page);
4909                *b_size = strlen(*body);
4910                status  = HTTP_STATUS_OK;
4911            }
4912            else {
4913                *body = xs_dup(uid);
4914                status = HTTP_STATUS_NOT_FOUND;
4915            }
4916        }
4917    }
4918    else
4919    if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
4920        if (!login(&snac, req)) {
4921            *body  = xs_dup(uid);
4922            status = HTTP_STATUS_UNAUTHORIZED;
4923        }
4924        else {
4925            *body   = html_notifications(&snac, skip, show);
4926            *b_size = strlen(*body);
4927            status  = HTTP_STATUS_OK;
4928        }
4929    }
4930    else
4931    if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
4932        if (!login(&snac, req)) {
4933            *body  = xs_dup(uid);
4934            status = HTTP_STATUS_UNAUTHORIZED;
4935        }
4936        else {
4937            xs *list = timeline_instance_list(skip, show);
4938            xs *next = timeline_instance_list(skip + show, 1);
4939
4940            *body = html_timeline(&snac, list, 0, skip, show,
4941                xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse);
4942            *b_size = strlen(*body);
4943            status  = HTTP_STATUS_OK;
4944        }
4945    }
4946    else
4947    if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
4948        if (!login(&snac, req)) {
4949            *body  = xs_dup(uid);
4950            status = HTTP_STATUS_UNAUTHORIZED;
4951        }
4952        else {
4953            xs *list = pinned_list(&snac);
4954
4955            *body = html_timeline(&snac, list, 0, skip, show,
4956                0, L("Pinned posts"), "", 0, error, terse);
4957            *b_size = strlen(*body);
4958            status  = HTTP_STATUS_OK;
4959        }
4960    }
4961    else
4962    if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
4963        if (!login(&snac, req)) {
4964            *body  = xs_dup(uid);
4965            status = HTTP_STATUS_UNAUTHORIZED;
4966        }
4967        else {
4968            xs *list = bookmark_list(&snac);
4969
4970            *body = html_timeline(&snac, list, 0, skip, show,
4971                0, L("Bookmarked posts"), "", 0, error, terse);
4972            *b_size = strlen(*body);
4973            status  = HTTP_STATUS_OK;
4974        }
4975    }
4976    else
4977    if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
4978        if (!login(&snac, req)) {
4979            *body  = xs_dup(uid);
4980            status = HTTP_STATUS_UNAUTHORIZED;
4981        }
4982        else {
4983            xs *list = draft_list(&snac);
4984
4985            *body = html_timeline(&snac, list, 0, skip, show,
4986                0, L("Post drafts"), "", 0, error, terse);
4987            *b_size = strlen(*body);
4988            status  = HTTP_STATUS_OK;
4989        }
4990    }
4991    else
4992    if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
4993        if (!login(&snac, req)) {
4994            *body  = xs_dup(uid);
4995            status = HTTP_STATUS_UNAUTHORIZED;
4996        }
4997        else {
4998            xs *list = scheduled_list(&snac);
4999
5000            *body = html_timeline(&snac, list, 0, skip, show,
5001                0, L("Scheduled posts"), "", 0, error, terse);
5002            *b_size = strlen(*body);
5003            status  = HTTP_STATUS_OK;
5004        }
5005    }
5006    else
5007    if (xs_startswith(p_path, "list/")) { /** list timelines **/
5008        if (!login(&snac, req)) {
5009            *body  = xs_dup(uid);
5010            status = HTTP_STATUS_UNAUTHORIZED;
5011        }
5012        else {
5013            xs *l = xs_split(p_path, "/");
5014            const char *lid = xs_list_get(l, -1);
5015
5016            xs *list = list_timeline(&snac, lid, skip, show);
5017            xs *next = list_timeline(&snac, lid, skip + show, 1);
5018
5019            if (list != NULL) {
5020                xs *ttl = timeline_top_level(&snac, list);
5021
5022                xs *base = xs_fmt("/list/%s", lid);
5023                xs *name = list_maint(&snac, lid, 3);
5024                xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
5025
5026                *body = html_timeline(&snac, ttl, 0, skip, show,
5027                    xs_list_len(next), title, base, 1, error, terse);
5028                *b_size = strlen(*body);
5029                status  = HTTP_STATUS_OK;
5030            }
5031        }
5032    }
5033    else
5034    if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
5035        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5036            return HTTP_STATUS_FORBIDDEN;
5037
5038        xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
5039        xs *msg = NULL;
5040
5041        if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) {
5042            xs *md5  = xs_md5_hex(id, strlen(id));
5043            xs *list = xs_list_new();
5044
5045            list = xs_list_append(list, md5);
5046
5047            *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse);
5048            *b_size = strlen(*body);
5049            status  = HTTP_STATUS_OK;
5050        }
5051    }
5052    else
5053    if (xs_startswith(p_path, "s/")) { /** a static file **/
5054        xs *l    = xs_split(p_path, "/");
5055        const char *id = xs_list_get(l, 1);
5056        int sz;
5057
5058        if (id && *id) {
5059            status = static_get(&snac, id, body, &sz,
5060                        xs_dict_get(req, "if-none-match"), etag);
5061
5062            if (valid_status(status)) {
5063                *b_size = sz;
5064                *ctype  = (char *)xs_mime_by_ext(id);
5065            }
5066        }
5067    }
5068    else
5069    if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
5070        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5071            return HTTP_STATUS_FORBIDDEN;
5072
5073        if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
5074            return HTTP_STATUS_FORBIDDEN;
5075
5076        xs *l = xs_split(p_path, "/");
5077        const char *id = xs_list_get(l, 1);
5078
5079        if (id && *id) {
5080            if (xs_endswith(id, "timeline.html_")) {
5081                /* Don't let them in */
5082                *b_size = 0;
5083                status = HTTP_STATUS_NOT_FOUND;
5084            }
5085            else
5086                status = history_get(&snac, id, body, b_size,
5087                            xs_dict_get(req, "if-none-match"), etag);
5088        }
5089    }
5090    else
5091    if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
5092        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5093            return HTTP_STATUS_FORBIDDEN;
5094
5095        int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
5096
5097        xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
5098        xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
5099
5100        xs *rss_title = xs_fmt("%s (@%s@%s)",
5101            xs_dict_get(snac.config, "name"),
5102            snac.uid,
5103            xs_dict_get(srv_config, "host"));
5104        xs *rss_link = xs_fmt("%s.rss", snac.actor);
5105
5106        *body   = rss_from_timeline(&snac, elems, rss_title, rss_link, bio);
5107        *b_size = strlen(*body);
5108        *ctype  = "application/rss+xml; charset=utf-8";
5109        status  = HTTP_STATUS_OK;
5110
5111        snac_debug(&snac, 1, xs_fmt("serving RSS"));
5112    }
5113    else
5114    if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
5115        xs *proxy_prefix = NULL;
5116
5117        if (xs_startswith(p_path, "x/")) {
5118            /* proxy usage authorized by http basic auth */
5119            if (login(&snac, req))
5120                proxy_prefix = xs_str_new("x/");
5121            else {
5122                *body  = xs_dup(uid);
5123                status = HTTP_STATUS_UNAUTHORIZED;
5124            }
5125        }
5126        else {
5127            /* proxy usage authorized by proxy_token */
5128            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
5129            xs *tk = xs_md5_hex(tks, strlen(tks));
5130            xs *p = xs_fmt("y/%s/", tk);
5131
5132            if (xs_startswith(p_path, p))
5133                proxy_prefix = xs_dup(p);
5134        }
5135
5136        if (proxy_prefix) {
5137            /* pick the raw path (including optional ? arguments) */
5138            const char *raw_path = xs_dict_get(req, "raw_path");
5139
5140            /* skip to where the proxy/ string starts */
5141            raw_path += xs_str_in(raw_path, proxy_prefix);
5142
5143            xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
5144            xs *hdrs = xs_dict_new();
5145
5146            hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
5147
5148            const char *ims = xs_dict_get(req, "if-modified-since");
5149            const char *inm = xs_dict_get(req, "if-none-match");
5150
5151            if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
5152            if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
5153
5154            xs *rsp = xs_http_request("GET", url, hdrs,
5155                        NULL, 0, &status, body, b_size, 0);
5156
5157            if (valid_status(status)) {
5158                const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
5159                const char *lm = xs_dict_get(rsp, "last-modified");
5160                const char *et = xs_dict_get(rsp, "etag");
5161
5162                if (lm) *last_modified = xs_dup(lm);
5163                if (et) *etag = xs_dup(et);
5164
5165                /* find the content-type in the static mime types,
5166                   and return that value instead of ct, which will
5167                   be destroyed when out of scope */
5168                for (int n = 0; xs_mime_types[n]; n += 2) {
5169                    if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
5170                        *ctype = (char *)xs_mime_types[n + 1];
5171                        break;
5172                    }
5173                }
5174            }
5175
5176            snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
5177        }
5178    }
5179    else
5180    if (strcmp(p_path, "share") == 0) { /** direct post **/
5181        if (!login(&snac, req)) {
5182            *body  = xs_dup(uid);
5183            status = HTTP_STATUS_UNAUTHORIZED;
5184        }
5185        else {
5186            const char *b64 = xs_dict_get(q_vars, "content");
5187            int sz;
5188            xs *content = xs_base64_dec(b64, &sz);
5189            xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL);
5190            xs *c_msg = msg_create(&snac, msg);
5191
5192            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
5193
5194            enqueue_message(&snac, c_msg);
5195
5196            snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
5197
5198            *body   = xs_fmt("%s/admin", snac.actor);
5199            *b_size = strlen(*body);
5200            status  = HTTP_STATUS_SEE_OTHER;
5201        }
5202    }
5203    else
5204    if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
5205        if (!login(&snac, req)) {
5206            *body  = xs_dup(uid);
5207            status = HTTP_STATUS_UNAUTHORIZED;
5208        }
5209        else {
5210            status = HTTP_STATUS_NOT_FOUND;
5211
5212            const char *id     = xs_dict_get(q_vars, "id");
5213            const char *action = xs_dict_get(q_vars, "action");
5214
5215            if (xs_is_string(id) && xs_is_string(action)) {
5216                if (strcmp(action, "Follow") == 0) {
5217                    xs *msg = msg_follow(&snac, id);
5218
5219                    if (msg != NULL) {
5220                        const char *actor = xs_dict_get(msg, "object");
5221
5222                        following_add(&snac, actor, msg);
5223
5224                        enqueue_output_by_actor(&snac, msg, actor, 0);
5225
5226                        status = HTTP_STATUS_SEE_OTHER;
5227                    }
5228                }
5229                else
5230                if (xs_match(action, "Like|Boost|Announce")) {
5231                    /* bring the post */
5232                    xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
5233
5234                    if (msg != NULL) {
5235                        timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
5236                        enqueue_message(&snac, msg);
5237
5238                        status = HTTP_STATUS_SEE_OTHER;
5239                    }
5240                }
5241            }
5242
5243            if (status == HTTP_STATUS_SEE_OTHER) {
5244                *body   = xs_fmt("%s/admin", snac.actor);
5245                *b_size = strlen(*body);
5246            }
5247        }
5248    }
5249    else
5250        status = HTTP_STATUS_NOT_FOUND;
5251
5252    user_free(&snac);
5253
5254    if (valid_status(status) && *ctype == NULL) {
5255        *ctype = "text/html; charset=utf-8";
5256    }
5257
5258    return status;
5259}
5260
5261
5262int html_post_handler(const xs_dict *req, const char *q_path,
5263                      char *payload, int p_size,
5264                      char **body, int *b_size, char **ctype)
5265{
5266    (void)p_size;
5267    (void)ctype;
5268
5269    int status = 0;
5270    const snac *user = NULL;
5271    snac snac;
5272    const char *uid;
5273    const char *p_path;
5274    const xs_dict *p_vars;
5275
5276    xs *l = xs_split_n(q_path, "/", 2);
5277
5278    uid = xs_list_get(l, 1);
5279    if (!uid || !user_open(&snac, uid)) {
5280        /* invalid user */
5281        srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
5282        return HTTP_STATUS_NOT_FOUND;
5283    }
5284
5285    p_path = xs_list_get(l, 2);
5286
5287    /* all posts must be authenticated */
5288    if (!login(&snac, req)) {
5289        user_free(&snac);
5290        *body  = xs_dup(uid);
5291        return HTTP_STATUS_UNAUTHORIZED;
5292    }
5293
5294    user = &snac; /* for L() */
5295    set_user_lang(&snac);
5296
5297    p_vars = xs_dict_get(req, "p_vars");
5298
5299    if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
5300        snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
5301
5302        /* post note */
5303        const char *content      = xs_dict_get(p_vars, "content");
5304        const char *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
5305        const char *to           = xs_dict_get(p_vars, "to");
5306        const char *sensitive    = xs_dict_get(p_vars, "sensitive");
5307        const char *summary      = xs_dict_get(p_vars, "summary");
5308        const char *edit_id      = xs_dict_get(p_vars, "edit_id");
5309        const char *post_date    = xs_dict_get_def(p_vars, "post_date", "");
5310        const char *post_time    = xs_dict_get_def(p_vars, "post_time", "");
5311        const char *post_lang    = xs_dict_get(p_vars, "post_lang");
5312        const char *visibility   = xs_dict_get(p_vars, "visibility");
5313        int scope = SCOPE_PUBLIC;  /* default to public */
5314        if (!xs_is_null(visibility)) {
5315            if (strcmp(visibility, "unlisted") == 0)
5316                scope = SCOPE_UNLISTED;
5317            else
5318            if (strcmp(visibility, "followers") == 0)
5319                scope = SCOPE_FOLLOWERS;
5320            else
5321            if (strcmp(visibility, "mentioned") == 0)
5322                scope = SCOPE_MENTIONED;
5323        }
5324        int store_as_draft   = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
5325        xs *attach_list      = xs_list_new();
5326
5327        /* iterate the attachments */
5328        int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
5329
5330        for (int att_n = 0; att_n < max_attachments; att_n++) {
5331            xs *url_lbl = xs_fmt("attach_url_%d", att_n);
5332            xs *att_lbl = xs_fmt("attach_%d", att_n);
5333            xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
5334
5335            const char *attach_url     = xs_dict_get(p_vars, url_lbl);
5336            const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
5337            const char *alt_text       = xs_dict_get_def(p_vars, alt_lbl, "");
5338
5339            if (xs_is_string(attach_url) && *attach_url != '\0') {
5340                xs *l = xs_list_new();
5341
5342                l = xs_list_append(l, attach_url);
5343                l = xs_list_append(l, alt_text);
5344
5345                attach_list = xs_list_append(attach_list, l);
5346            }
5347            else
5348            if (xs_is_list(attach_file)) {
5349                const char *fn = xs_list_get(attach_file, 0);
5350
5351                if (xs_is_string(fn) && *fn != '\0') {
5352                    char rnd[32];
5353                    xs_rnd_buf(rnd, sizeof(rnd));
5354
5355                    const char *ext = strrchr(fn, '.');
5356                    xs *hash  = xs_md5_hex(rnd, sizeof(rnd));
5357                    xs *id    = xs_fmt("post-%s%s", hash, ext ? ext : "");
5358                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
5359                    int fo    = xs_number_get(xs_list_get(attach_file, 1));
5360                    int fs    = xs_number_get(xs_list_get(attach_file, 2));
5361
5362                    /* store */
5363                    static_put(&snac, id, payload + fo, fs);
5364
5365                    xs *l = xs_list_new();
5366
5367                    l = xs_list_append(l, url);
5368                    l = xs_list_append(l, alt_text);
5369
5370                    attach_list = xs_list_append(attach_list, l);
5371                }
5372            }
5373        }
5374
5375        if (content != NULL) {
5376            xs *msg       = NULL;
5377            xs *c_msg     = NULL;
5378            xs *content_2 = xs_replace(content, "\r", "");
5379            xs *poll_opts = NULL;
5380
5381            /* is there a valid set of poll options? */
5382            const char *v = xs_dict_get(p_vars, "poll_options");
5383            if (!xs_is_null(v) && *v) {
5384                xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
5385
5386                poll_opts = xs_split(v2, "\n");
5387            }
5388
5389            if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
5390                /* get the rest of poll configuration */
5391                const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
5392                const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
5393                int multiple = 0;
5394
5395                int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
5396
5397                if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
5398                    multiple = 1;
5399
5400                msg = msg_question(&snac, content_2, attach_list,
5401                                   poll_opts, multiple, end_secs);
5402
5403                enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
5404            }
5405            else
5406                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
5407
5408            if (sensitive != NULL) {
5409                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
5410                msg = xs_dict_set(msg, "summary",   xs_is_null(summary) ? "..." : summary);
5411            }
5412
5413            if (xs_is_string(post_date) && *post_date) {
5414                xs *post_pubdate = xs_fmt("%sT%s", post_date,
5415                    xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
5416
5417                time_t t = xs_parse_iso_date(post_pubdate, 0);
5418
5419                if (t != 0) {
5420                    t -= xs_tz_offset(snac.tz);
5421
5422                    xs *iso_date = xs_str_iso_date(t);
5423                    msg = xs_dict_set(msg, "published", iso_date);
5424
5425                    snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
5426                }
5427                else
5428                    snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
5429            }
5430
5431            /* is the published date from the future? */
5432            int future_post = 0;
5433            xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
5434
5435            if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
5436                future_post = 1;
5437
5438            if (xs_is_null(edit_id)) {
5439                /* new message */
5440                const char *id = xs_dict_get(msg, "id");
5441
5442                if (store_as_draft) {
5443                    draft_add(&snac, id, msg);
5444                }
5445                else
5446                if (future_post) {
5447                    schedule_add(&snac, id, msg);
5448                }
5449                else {
5450                    c_msg = msg_create(&snac, msg);
5451                    timeline_add(&snac, id, msg);
5452                }
5453            }
5454            else {
5455                /* an edition of a previous message */
5456                xs *p_msg = NULL;
5457
5458                if (valid_status(object_get(edit_id, &p_msg))) {
5459                    /* copy relevant fields from previous version */
5460                    char *fields[] = { "id", "context", "url",
5461                                       "cc", "attributedTo",
5462                                       "to", "inReplyTo", NULL };
5463                    int n;
5464
5465                    for (n = 0; fields[n]; n++) {
5466                        const char *v = xs_dict_get(p_msg, fields[n]);
5467                        msg = xs_dict_set(msg, fields[n], v);
5468                    }
5469
5470                    if (store_as_draft) {
5471                        draft_add(&snac, edit_id, msg);
5472                    }
5473                    else
5474                    if (is_draft(&snac, edit_id)) {
5475                        /* message was previously a draft; it's a create activity */
5476
5477                        /* if the date is from the past, overwrite it with right_now */
5478                        if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
5479                            snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
5480                            msg = xs_dict_set(msg, "published", right_now);
5481                        }
5482
5483                        /* overwrite object */
5484                        object_add_ow(edit_id, msg);
5485
5486                        if (future_post) {
5487                            schedule_add(&snac, edit_id, msg);
5488                        }
5489                        else {
5490                            c_msg = msg_create(&snac, msg);
5491                            timeline_add(&snac, edit_id, msg);
5492                        }
5493
5494                        draft_del(&snac, edit_id);
5495                    }
5496                    else
5497                    if (is_scheduled(&snac, edit_id)) {
5498                        /* editing an scheduled post; just update it */
5499                        schedule_add(&snac, edit_id, msg);
5500                    }
5501                    else {
5502                        /* ignore the (possibly changed) published date */
5503                        msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
5504
5505                        /* set the updated field */
5506                        xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
5507                        msg = xs_dict_set(msg, "updated", updated);
5508
5509                        /* overwrite object, not updating the indexes */
5510                        object_add_ow(edit_id, msg);
5511
5512                        /* index tags */
5513                        tag_index(edit_id, msg);
5514
5515                        /* update message */
5516                        c_msg = msg_update(&snac, msg);
5517                    }
5518                }
5519                else
5520                    snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
5521            }
5522
5523            if (c_msg != NULL) {
5524                enqueue_message(&snac, c_msg);
5525                enqueue_webmention(msg);
5526            }
5527
5528            history_del(&snac, "timeline.html_");
5529        }
5530
5531        status = HTTP_STATUS_SEE_OTHER;
5532    }
5533    else
5534    if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
5535        /* action on an entry */
5536        const char *id     = xs_dict_get(p_vars, "id");
5537        const char *actor  = xs_dict_get(p_vars, "actor");
5538        const char *action = xs_dict_get(p_vars, "action");
5539        const char *group  = xs_dict_get(p_vars, "group");
5540
5541        if (action == NULL)
5542            return HTTP_STATUS_NOT_FOUND;
5543
5544        snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
5545
5546        status = HTTP_STATUS_SEE_OTHER;
5547
5548        if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
5549            const char *eid = xs_dict_get(p_vars, "eid");
5550
5551            if (eid != NULL) {
5552                xs *n_msg = msg_emoji_unreact(&snac, id, eid);
5553
5554                if (n_msg != NULL)
5555                    enqueue_message(&snac, n_msg);
5556            }
5557        }
5558        else
5559        if (strcmp(action, L("EmojiReact")) == 0) { /** **/
5560            xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
5561
5562            eid = xs_strip_chars_i(eid, ":");
5563
5564            const xs_dict *ret = msg_emoji_init(&snac, id, eid);
5565            /* fails if either invalid or already reacted */
5566            if (!ret)
5567                ret = msg_emoji_unreact(&snac, id, eid);
5568            if (!ret)
5569                status = HTTP_STATUS_NOT_FOUND;
5570        }
5571        else
5572        if (strcmp(action, L("Like")) == 0) { /** **/
5573            xs *msg = msg_admiration(&snac, id, "Like");
5574
5575            if (msg != NULL) {
5576                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
5577                enqueue_message(&snac, msg);
5578            }
5579        }
5580        else
5581        if (strcmp(action, L("Boost")) == 0) { /** **/
5582            xs *msg = msg_admiration(&snac, id, "Announce");
5583
5584            if (msg != NULL) {
5585                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
5586                enqueue_message(&snac, msg);
5587            }
5588        }
5589        else
5590        if (strcmp(action, L("Unlike")) == 0) { /** **/
5591            xs *msg = msg_repulsion(&snac, id, "Like");
5592
5593            if (msg != NULL) {
5594                enqueue_message(&snac, msg);
5595            }
5596        }
5597        else
5598        if (strcmp(action, L("Unboost")) == 0) { /** **/
5599            xs *msg = msg_repulsion(&snac, id, "Announce");
5600
5601            if (msg != NULL) {
5602                enqueue_message(&snac, msg);
5603            }
5604        }
5605        else
5606        if (strcmp(action, L("MUTE")) == 0) { /** **/
5607            mute(&snac, actor);
5608        }
5609        else
5610        if (strcmp(action, L("Unmute")) == 0) { /** **/
5611            unmute(&snac, actor);
5612        }
5613        else
5614        if (strcmp(action, L("Hide")) == 0) { /** **/
5615            if (is_draft(&snac, id))
5616                draft_del(&snac, id);
5617            else
5618            if (is_scheduled(&snac, id))
5619                schedule_del(&snac, id);
5620            else
5621                hide(&snac, id);
5622        }
5623        else
5624        if (strcmp(action, L("Limit")) == 0) { /** **/
5625            limit(&snac, actor);
5626        }
5627        else
5628        if (strcmp(action, L("Unlimit")) == 0) { /** **/
5629            unlimit(&snac, actor);
5630        }
5631        else
5632        if (strcmp(action, L("Follow")) == 0) { /** **/
5633            xs *msg = msg_follow(&snac, actor);
5634
5635            if (msg != NULL) {
5636                /* reload the actor from the message, in may be different */
5637                actor = xs_dict_get(msg, "object");
5638
5639                following_add(&snac, actor, msg);
5640
5641                enqueue_output_by_actor(&snac, msg, actor, 0);
5642            }
5643        }
5644        else
5645        if (strcmp(action, L("Unfollow")) == 0) { /** **/
5646            /* get the following object */
5647            xs *object = NULL;
5648
5649            if (valid_status(following_get(&snac, actor, &object))) {
5650                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5651
5652                following_del(&snac, actor);
5653
5654                enqueue_output_by_actor(&snac, msg, actor, 0);
5655
5656                snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
5657            }
5658            else
5659                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5660        }
5661        else
5662        if (strcmp(action, L("Follow Group")) == 0) { /** **/
5663            xs *msg = msg_follow(&snac, group);
5664
5665            if (msg != NULL) {
5666                /* reload the group from the message, in may be different */
5667                group = xs_dict_get(msg, "object");
5668
5669                following_add(&snac, group, msg);
5670
5671                enqueue_output_by_actor(&snac, msg, group, 0);
5672            }
5673        }
5674        else
5675        if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
5676            /* get the following object */
5677            xs *object = NULL;
5678
5679            if (valid_status(following_get(&snac, group, &object))) {
5680                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5681
5682                following_del(&snac, group);
5683
5684                enqueue_output_by_actor(&snac, msg, group, 0);
5685
5686                snac_log(&snac, xs_fmt("unfollowed group %s", group));
5687            }
5688            else
5689                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5690        }
5691        else
5692        if (strcmp(action, L("Delete")) == 0) { /** **/
5693            const char *actor_form = xs_dict_get(p_vars, "actor-form");
5694            if (actor_form != NULL) {
5695                /* delete follower */
5696                if (valid_status(follower_del(&snac, actor)))
5697                    snac_log(&snac, xs_fmt("deleted follower %s", actor));
5698                else
5699                    snac_log(&snac, xs_fmt("error deleting follower %s", actor));
5700            }
5701            else {
5702                /* delete an entry */
5703                if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
5704                    /* it's a post by us: generate a delete */
5705                    xs *msg = msg_delete(&snac, id);
5706
5707                    enqueue_message(&snac, msg);
5708
5709                    snac_log(&snac, xs_fmt("posted tombstone for %s", id));
5710                }
5711
5712                timeline_del(&snac, id);
5713
5714                draft_del(&snac, id);
5715
5716                schedule_del(&snac, id);
5717
5718                snac_log(&snac, xs_fmt("deleted entry %s", id));
5719            }
5720        }
5721        else
5722        if (strcmp(action, L("Pin")) == 0) { /** **/
5723            pin(&snac, id);
5724            timeline_touch(&snac);
5725        }
5726        else
5727        if (strcmp(action, L("Unpin")) == 0) { /** **/
5728            unpin(&snac, id);
5729            timeline_touch(&snac);
5730        }
5731        else
5732        if (strcmp(action, L("Bookmark")) == 0) { /** **/
5733            bookmark(&snac, id);
5734            timeline_touch(&snac);
5735        }
5736        else
5737        if (strcmp(action, L("Unbookmark")) == 0) { /** **/
5738            unbookmark(&snac, id);
5739            timeline_touch(&snac);
5740        }
5741        else
5742        if (strcmp(action, L("Approve")) == 0) { /** **/
5743            xs *fwreq = pending_get(&snac, actor);
5744
5745            if (fwreq != NULL) {
5746                xs *reply = msg_accept(&snac, fwreq, actor);
5747
5748                enqueue_message(&snac, reply);
5749
5750                if (xs_is_null(xs_dict_get(fwreq, "published"))) {
5751                    /* add a date if it doesn't include one (Mastodon) */
5752                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
5753                    fwreq = xs_dict_set(fwreq, "published", date);
5754                }
5755
5756                timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
5757
5758                follower_add(&snac, actor);
5759
5760                pending_del(&snac, actor);
5761
5762                snac_log(&snac, xs_fmt("new follower %s", actor));
5763            }
5764        }
5765        else
5766        if (strcmp(action, L("Discard")) == 0) { /** **/
5767            pending_del(&snac, actor);
5768        }
5769        else
5770            status = HTTP_STATUS_NOT_FOUND;
5771
5772        /* delete the cached timeline */
5773        if (status == HTTP_STATUS_SEE_OTHER)
5774            history_del(&snac, "timeline.html_");
5775    }
5776    else
5777    if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
5778        /* change of user data */
5779        const char *v;
5780        const char *p1, *p2;
5781
5782        if ((v = xs_dict_get(p_vars, "name")) != NULL)
5783            snac.config = xs_dict_set(snac.config, "name", v);
5784        if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
5785            snac.config = xs_dict_set(snac.config, "avatar", v);
5786        if ((v = xs_dict_get(p_vars, "bio")) != NULL)
5787            snac.config = xs_dict_set(snac.config, "bio", v);
5788        if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
5789            strcmp(v, "on") == 0) {
5790            snac.config = xs_dict_set(snac.config, "cw", "open");
5791        } else { /* if the checkbox is not set, the parameter is missing */
5792            snac.config = xs_dict_set(snac.config, "cw", "");
5793        }
5794        if ((v = xs_dict_get(p_vars, "email")) != NULL)
5795            snac.config = xs_dict_set(snac.config, "email", v);
5796        if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
5797            snac.config = xs_dict_set(snac.config, "telegram_bot", v);
5798        if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
5799            snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
5800        if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
5801            snac.config = xs_dict_set(snac.config, "ntfy_server", v);
5802        if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
5803            snac.config = xs_dict_set(snac.config, "ntfy_token", v);
5804        if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
5805            xs *days    = xs_number_new(atof(v));
5806            snac.config = xs_dict_set(snac.config, "purge_days", days);
5807        }
5808        if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
5809            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
5810        else
5811            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
5812        if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
5813            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
5814        else
5815            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
5816        if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
5817            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
5818        else
5819            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
5820        if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
5821            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
5822        else
5823            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
5824        if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
5825            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
5826        else
5827            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
5828        if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
5829            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
5830        else
5831            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
5832        if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
5833            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
5834        else
5835            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
5836        if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
5837            snac.config = xs_dict_set(snac.config, "lang", v);
5838        if ((v = xs_dict_get(p_vars, "tz")) != NULL)
5839            snac.config = xs_dict_set(snac.config, "tz", v);
5840        if ((v = xs_dict_get(p_vars, "post_langs")) != NULL)
5841            snac.config = xs_dict_set(snac.config, "post_langs", v);
5842
5843        snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
5844        snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
5845
5846        snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", ""));
5847
5848        if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
5849            snac.config = xs_dict_set(snac.config, "metadata", v);
5850
5851        /* uploads */
5852        const char *uploads[] = { "avatar", "header", NULL };
5853        int n;
5854        for (n = 0; uploads[n]; n++) {
5855            xs *var_name = xs_fmt("%s_file", uploads[n]);
5856
5857            const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
5858            if (xs_type(uploaded_file) == XSTYPE_LIST) {
5859                const char *fn = xs_list_get(uploaded_file, 0);
5860
5861                if (fn && *fn) {
5862                    const char *mimetype = xs_mime_by_ext(fn);
5863
5864                    if (xs_startswith(mimetype, "image/")) {
5865                        const char *ext = strrchr(fn, '.');
5866                        xs *hash        = xs_md5_hex(fn, strlen(fn));
5867                        xs *id          = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
5868                        xs *url         = xs_fmt("%s/s/%s", snac.actor, id);
5869                        int fo          = xs_number_get(xs_list_get(uploaded_file, 1));
5870                        int fs          = xs_number_get(xs_list_get(uploaded_file, 2));
5871
5872                        /* store */
5873                        static_put(&snac, id, payload + fo, fs);
5874
5875                        snac.config = xs_dict_set(snac.config, uploads[n], url);
5876                    }
5877                }
5878            }
5879        }
5880
5881        /* delete images by removing url from user.json */
5882        for (n = 0; uploads[n]; n++) {
5883            xs *var_name = xs_fmt("%s_delete", uploads[n]);
5884            const char *delete_var = xs_dict_get(p_vars, var_name);
5885
5886            if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
5887                    snac.config = xs_dict_set(snac.config, uploads[n], "");
5888            }
5889        }
5890
5891        /* password change? */
5892        if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
5893            (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
5894            *p1 && strcmp(p1, p2) == 0) {
5895            xs *pw = hash_password(snac.uid, p1, NULL);
5896            snac.config = xs_dict_set(snac.config, "passwd", pw);
5897        }
5898
5899        user_persist(&snac, 1);
5900
5901        status = HTTP_STATUS_SEE_OTHER;
5902    }
5903    else
5904    if (p_path && strcmp(p_path, "admin/filter-notifications") == 0) { /** **/
5905        notify_filter(&snac, p_vars);
5906        user_persist(&snac, 0);
5907        status = HTTP_STATUS_SEE_OTHER;
5908    }
5909    else
5910    if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
5911        notify_clear(&snac);
5912        timeline_touch(&snac);
5913
5914        status = HTTP_STATUS_SEE_OTHER;
5915    }
5916    else
5917    if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
5918        const char *irt   = xs_dict_get(p_vars, "irt");
5919        const char *opt   = xs_dict_get(p_vars, "question");
5920        const char *actor = xs_dict_get(p_vars, "actor");
5921
5922        xs *ls = NULL;
5923
5924        /* multiple choices? */
5925        if (xs_type(opt) == XSTYPE_LIST)
5926            ls = xs_dup(opt);
5927        else
5928        if (xs_type(opt) == XSTYPE_STRING) {
5929            ls = xs_list_new();
5930            ls = xs_list_append(ls, opt);
5931        }
5932
5933        const xs_str *v;
5934        int c = 0;
5935
5936        while (xs_list_next(ls, &v, &c)) {
5937            xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL);
5938
5939            /* set the option */
5940            msg = xs_dict_append(msg, "name", v);
5941
5942            /* delete the content */
5943            msg = xs_dict_del(msg, "content");
5944
5945            xs *c_msg = msg_create(&snac, msg);
5946
5947            enqueue_message(&snac, c_msg);
5948
5949            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
5950        }
5951
5952        if (ls != NULL) {
5953            /* get the poll object */
5954            xs *poll = NULL;
5955
5956            if (valid_status(object_get(irt, &poll))) {
5957                const char *date = xs_dict_get(poll, "endTime");
5958                if (xs_is_null(date))
5959                    date = xs_dict_get(poll, "closed");
5960
5961                if (!xs_is_null(date)) {
5962                    time_t t = xs_parse_iso_date(date, 0) - time(NULL);
5963
5964                    /* request the poll when it's closed;
5965                       Pleroma does not send and update when the poll closes */
5966                    enqueue_object_request(&snac, irt, t + 2);
5967                }
5968            }
5969        }
5970
5971        status = HTTP_STATUS_SEE_OTHER;
5972    }
5973    else
5974    if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
5975        const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
5976
5977        if (xs_is_string(followed_hashtags)) {
5978            xs *new_hashtags = xs_list_new();
5979            xs *l = xs_split(followed_hashtags, "\n");
5980            const char *v;
5981
5982            xs_list_foreach(l, v) {
5983                xs *s1 = xs_strip_i(xs_dup(v));
5984                s1 = xs_replace_i(s1, " ", "");
5985
5986                if (*s1 == '\0')
5987                    continue;
5988
5989                xs *s2 = NULL;
5990
5991                if (xs_startswith(s1, "https:/"))
5992                    s2 = xs_dup(s1);
5993                else {
5994                    s2 = xs_utf8_to_lower(s1);
5995
5996                    if (*s2 != '#')
5997                        s2 = xs_str_prepend_i(s2, "#");
5998                }
5999
6000                new_hashtags = xs_list_append(new_hashtags, s2);
6001            }
6002
6003            snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
6004            user_persist(&snac, 0);
6005        }
6006
6007        status = HTTP_STATUS_SEE_OTHER;
6008    }
6009    else
6010    if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/
6011        const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags");
6012
6013        if (xs_is_string(hashtags)) {
6014            xs *new_hashtags = xs_list_new();
6015            xs *l = xs_split(hashtags, "\n");
6016            const char *v;
6017
6018            xs_list_foreach(l, v) {
6019                xs *s1 = xs_strip_i(xs_dup(v));
6020                s1 = xs_replace_i(s1, " ", "");
6021
6022                if (*s1 == '\0')
6023                    continue;
6024
6025                xs *s2 = xs_utf8_to_lower(s1);
6026                if (*s2 != '#')
6027                    s2 = xs_str_prepend_i(s2, "#");
6028
6029                new_hashtags = xs_list_append(new_hashtags, s2);
6030            }
6031
6032            snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags);
6033            user_persist(&snac, 0);
6034        }
6035
6036        status = HTTP_STATUS_SEE_OTHER;
6037    }
6038    else
6039    if (p_path && strcmp(p_path, "admin/muted-words") == 0) {
6040        const char *words = xs_dict_get(p_vars, "muted_words");
6041
6042        if (xs_is_string(words)) {
6043            xs *new_words = xs_list_new();
6044            xs *l = xs_split(words, "\n");
6045            const char *v;
6046
6047            xs_list_foreach(l, v) {
6048                xs *s1 = xs_strip_i(xs_dup(v));
6049                s1 = xs_replace_i(s1, " ", "");
6050
6051                if (*s1 == '\0')
6052                    continue;
6053
6054                xs *s2 = xs_utf8_to_lower(s1);
6055
6056                new_words = xs_list_insert_sorted(new_words, s2);
6057            }
6058
6059            snac.config = xs_dict_set(snac.config, "muted_words", new_words);
6060            user_persist(&snac, 0);
6061        }
6062
6063        status = HTTP_STATUS_SEE_OTHER;
6064    }
6065
6066    if (status == HTTP_STATUS_SEE_OTHER) {
6067        const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
6068
6069        if (xs_is_string(hard_redir))
6070            *body = xs_dup(hard_redir);
6071        else {
6072            const char *redir = xs_dict_get(p_vars, "redir");
6073
6074            if (xs_is_null(redir))
6075                redir = "top";
6076
6077            *body = xs_fmt("%s/admin#%s", snac.actor, redir);
6078        }
6079
6080        *b_size = strlen(*body);
6081    }
6082
6083    user_free(&snac);
6084
6085    return status;
6086}