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