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