2.86 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                xs *actors = xs_list_new();
2464                actors = xs_list_append(actors, actor);
2465                char me = actor && user && strcmp(actor, user->actor) == 0;
2466                int count = 1;
2467
2468                if (contentl) {
2469                    count = atoi(xs_list_get(contentl, 0)) + 1;
2470                    const xs_list *actorsc = xs_list_get(contentl, 1);
2471                    if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
2472                        me = 1;
2473
2474                    if (xs_list_in(actorsc, actor) != -1) {
2475                        xs_free(actors);
2476                        actors = xs_dup(actorsc);
2477                    }
2478                    else
2479                        actors = xs_list_cat(actors, actorsc);
2480                }
2481
2482                xs *fl = xs_list_new();
2483                xs *c1 = xs_fmt("%d", count);
2484                xs *c2 = xs_fmt("%d", me);
2485                fl = xs_list_append(fl, c1, actors, c2);
2486                sfrl = xs_dict_append(sfrl, content, fl);
2487            }
2488        }
2489
2490        c = 0;
2491
2492        while (xs_list_next(rl, &k, &c)) {
2493            xs *m = NULL;
2494            if (valid_status(object_get_by_md5(k, &m))) {
2495                const xs_dict *tag = xs_dict_get(m, "tag");
2496                const xs_dict *ide = xs_dict_get(m, "id");
2497
2498                const char *content = xs_dict_get(m, "content");
2499                const char *shortname;
2500                shortname = xs_dict_get(m, "content");
2501
2502                const xs_list *items = xs_dict_get(sfrl, content);
2503
2504                if (!xs_is_null(items)) {
2505                    const char *nb = xs_list_get(items, 0);
2506                    const xs_list *actors = xs_list_get(items, 1);
2507                    const char me = *xs_list_get(items, 2) == '1';
2508
2509                    is_emoji = 1;
2510
2511                    xs *al = xs_join(actors, ",\n\t");
2512                    xs *act = atoi(nb) > 1 ?
2513                        xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
2514                        xs_dup(xs_dict_get(m, "actor"));
2515
2516                    xs *class = xs_list_new();
2517                    class = xs_list_append(class, "snac-reaction");
2518
2519                    xs_html *ret = NULL;
2520                    if (tag && shortname) {
2521                        xs *cl = xs_list_new();
2522                        cl = xs_list_append(cl, "snac-reaction-image");
2523                        xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
2524
2525                        emoji = xs_strip_chars_i(emoji, ":");
2526
2527                        if (me)
2528                            class = xs_list_append(class, "snac-reacted");
2529
2530                        xs *l1 = xs_join(class, " ");
2531                        ret = xs_html_tag("button",
2532                                xs_html_attr("type", "submit"),
2533                                xs_html_attr("name", "action"),
2534                                xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
2535                                xs_html_raw(emoji),
2536                                xs_html_tag("span",
2537                                    xs_html_raw(nb),
2538                                    xs_html_attr("style", "padding-left: 5px;")),
2539                                xs_html_attr("title", act),
2540                                xs_html_attr("class", l1));
2541
2542                        if (!(ide && xs_startswith(ide, srv_baseurl)))
2543                            xs_html_add(ret, xs_html_attr("disabled", "true"));
2544                    }
2545                    else if (shortname) {
2546                        xs *sn = xs_dup(shortname);
2547                        const char *sna = sn;
2548                        unsigned int utf = xs_utf8_dec((const char **)&sna);
2549
2550                        if (xs_is_emoji(utf)) {
2551                            const char *style = "font-size: large;";
2552                            if (me)
2553                                class = xs_list_append(class, "snac-reacted");
2554                            xs *l1 = xs_join(class, " ");
2555                            xs *s1 = xs_fmt("&#%d", utf);
2556                            ret = xs_html_tag("button",
2557                                    xs_html_attr("type", "submit"),
2558                                    xs_html_attr("name", "action"),
2559                                    xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
2560                                    xs_html_raw(s1),
2561                                    xs_html_tag("span",
2562                                        xs_html_raw(nb),
2563                                        xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
2564                                    xs_html_attr("title", act),
2565                                    xs_html_attr("class", l1),
2566                                    xs_html_attr("style", style));
2567                        }
2568                    }
2569                    if (ret) {
2570                        xs *s1;
2571                        if (user) {
2572                            xs *action = xs_fmt("%s/admin/action", user->actor);
2573                            xs *form_id = xs_fmt("%s_reply_form", md5);
2574
2575                            xs_html *form =
2576                                xs_html_tag("form",
2577                                xs_html_attr("autocomplete", "off"),
2578                                xs_html_attr("method",       "post"),
2579                                xs_html_attr("action",       action),
2580                                xs_html_attr("enctype",      "multipart/form-data"),
2581                                xs_html_attr("style",      "display: inline-flex;"
2582                                    "vertical-align: middle;"),
2583                                xs_html_attr("id",           form_id),
2584                                xs_html_sctag("input",
2585                                    xs_html_attr("type",  "hidden"),
2586                                    xs_html_attr("name",  "id"),
2587                                    xs_html_attr("value", id)),
2588                                xs_html_sctag("input",
2589                                    xs_html_attr("type",  "hidden"),
2590                                    xs_html_attr("name",  "eid"),
2591                                    xs_html_attr("value", shortname)),
2592                                ret);
2593                            s1 = xs_html_render(form);
2594                        }
2595                        else
2596                            s1 = xs_html_render(ret);
2597
2598                        ls = xs_list_append(ls, s1);
2599                        sfrl = xs_dict_del(sfrl, content);
2600                    }
2601                }
2602            }
2603        }
2604
2605        c = 0;
2606
2607        xs_html *emoji_div;
2608        if (xs_list_len(ls) > 0) {
2609                emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
2610                        xs_html_attr("class", "snac-reaction-div"));
2611
2612            while (ls != NULL && xs_list_next(ls, &k, &c))
2613                xs_html_add(emoji_div, xs_html_raw(k));
2614
2615            xs_html_add(snac_content_wrap, emoji_div);
2616        }
2617
2618    }
2619
2620    {
2621        /** build the content string **/
2622        const char *content = xs_dict_get(msg, "content");
2623
2624        if (xs_type(content) != XSTYPE_STRING) {
2625            if (!xs_is_null(content))
2626                srv_archive_error("unexpected_content_xstype",
2627                    "content field type", xs_stock(XSTYPE_DICT), msg);
2628
2629            content = "";
2630        }
2631
2632        /* skip ugly line breaks at the beginning */
2633        while (xs_startswith(content, "<br>"))
2634            content += 4;
2635
2636        xs *c = sanitize(content);
2637
2638        /* do some tweaks to the content */
2639        c = xs_replace_i(c, "\r", "");
2640
2641        while (xs_endswith(c, "<br><br>"))
2642            c = xs_crop_i(c, 0, -4);
2643
2644        c = xs_replace_i(c, "<br><br>", "<p>");
2645
2646        if (is_emoji == 0)
2647            c = xs_str_cat(c, "<p>");
2648
2649        /* replace the :shortnames: */
2650        c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
2651
2652        /* Peertube videos content is in markdown */
2653        const char *mtype = xs_dict_get(msg, "mediaType");
2654        if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
2655            /* a full conversion could be better */
2656            c = xs_replace_i(c, "\r", "");
2657            c = xs_replace_i(c, "\n", "<br>");
2658        }
2659
2660        /* c contains sanitized HTML */
2661        xs_html_add(snac_content,
2662            xs_html_raw(c));
2663
2664        /* quoted post */
2665        const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
2666
2667        if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
2668            xs *quoted_post = NULL;
2669
2670            if (valid_status(object_get(quoted_id, &quoted_post))) {
2671                xs_html_add(snac_content,
2672                    xs_html_tag("blockquote",
2673                        xs_html_attr("class", "snac-quoted-post"),
2674                        html_entry(user, quoted_post, 1, 1, NULL, 1)));
2675            }
2676            else
2677            if (user)
2678                enqueue_object_request(user, quoted_id, 0);
2679        }
2680    }
2681
2682    if (strcmp(type, "Question") == 0) { /** question content **/
2683        const xs_list *oo = xs_dict_get(msg, "oneOf");
2684        const xs_list *ao = xs_dict_get(msg, "anyOf");
2685        const xs_list *p;
2686        const xs_dict *v;
2687        int closed = 0;
2688        const char *f_closed = NULL;
2689
2690        xs_html *poll = xs_html_tag("div", NULL);
2691
2692        if (read_only)
2693            closed = 1; /* non-identified page; show as closed */
2694        else
2695        if (user && is_msg_mine(user, id))
2696            closed = 1; /* we questioned; closed for us */
2697        else
2698        if (user && was_question_voted(user, id))
2699            closed = 1; /* we already voted; closed for us */
2700
2701        if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
2702            /* it has a closed date... but is it in the past? */
2703            time_t t0 = time(NULL);
2704            time_t t1 = xs_parse_iso_date(f_closed, 0);
2705
2706            if (t1 < t0)
2707                closed = 2;
2708        }
2709
2710        /* get the appropriate list of options */
2711        p = oo != NULL ? oo : ao;
2712
2713        if (closed || user == NULL) {
2714            /* closed poll */
2715            xs_html *poll_result = xs_html_tag("table",
2716                xs_html_attr("class", "snac-poll-result"));
2717            int c = 0;
2718
2719            while (xs_list_next(p, &v, &c)) {
2720                const char *name       = xs_dict_get(v, "name");
2721                const xs_dict *replies = xs_dict_get(v, "replies");
2722
2723                if (xs_is_string(name) && xs_is_dict(replies)) {
2724                    const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
2725
2726                    if (xs_is_string(ti))
2727                        xs_html_add(poll_result,
2728                            xs_html_tag("tr",
2729                                xs_html_tag("td",
2730                                    xs_html_text(name),
2731                                    xs_html_text(":")),
2732                                xs_html_tag("td",
2733                                    xs_html_text(ti))));
2734                }
2735            }
2736
2737            xs_html_add(poll,
2738                poll_result);
2739        }
2740        else {
2741            /* poll still active */
2742            xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
2743            xs_html *form;
2744            xs_html *poll_form = xs_html_tag("div",
2745                xs_html_attr("class", "snac-poll-form"),
2746                form = xs_html_tag("form",
2747                    xs_html_attr("autocomplete", "off"),
2748                    xs_html_attr("method", "post"),
2749                    xs_html_attr("action", vote_action),
2750                    xs_html_sctag("input",
2751                        xs_html_attr("type", "hidden"),
2752                        xs_html_attr("name", "actor"),
2753                        xs_html_attr("value", actor)),
2754                    xs_html_sctag("input",
2755                        xs_html_attr("type", "hidden"),
2756                        xs_html_attr("name", "irt"),
2757                        xs_html_attr("value", id))));
2758
2759            int c = 0;
2760            while (xs_list_next(p, &v, &c)) {
2761                const char *name = xs_dict_get(v, "name");
2762                const xs_dict *replies = xs_dict_get(v, "replies");
2763
2764                if (name) {
2765                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
2766
2767                    xs_html *btn = xs_html_sctag("input",
2768                            xs_html_attr("id", name),
2769                            xs_html_attr("value", name),
2770                            xs_html_attr("name", "question"));
2771
2772                    if (!xs_is_null(oo)) {
2773                        xs_html_add(btn,
2774                            xs_html_attr("type", "radio"),
2775                            xs_html_attr("required", "required"));
2776                    }
2777                    else
2778                        xs_html_add(btn,
2779                            xs_html_attr("type", "checkbox"));
2780
2781                    xs_html_add(form,
2782                        btn,
2783                        xs_html_text(" "),
2784                        xs_html_tag("span",
2785                            xs_html_attr("title", ti),
2786                            xs_html_text(name)),
2787                        xs_html_sctag("br", NULL));
2788                }
2789            }
2790
2791            xs_html_add(form,
2792                xs_html_tag("p", NULL),
2793                xs_html_sctag("input",
2794                    xs_html_attr("type", "submit"),
2795                    xs_html_attr("class", "button"),
2796                    xs_html_attr("value", L("Vote"))));
2797
2798            xs_html_add(poll,
2799                poll_form);
2800        }
2801
2802        /* if it's *really* closed, say it */
2803        if (closed == 2) {
2804            xs_html_add(poll,
2805                xs_html_tag("p",
2806                    xs_html_text(L("Closed"))));
2807        }
2808        else {
2809            /* show when the poll closes */
2810            const char *end_time = xs_dict_get(msg, "endTime");
2811
2812            /* Pleroma does not have an endTime field;
2813               it has a closed time in the future */
2814            if (xs_is_null(end_time))
2815                end_time = xs_dict_get(msg, "closed");
2816
2817            if (!xs_is_null(end_time)) {
2818                time_t t0 = time(NULL);
2819                time_t t1 = xs_parse_iso_date(end_time, 0);
2820
2821                if (t1 > 0 && t1 > t0) {
2822                    time_t diff_time = t1 - t0;
2823                    xs *tf = xs_str_time_diff(diff_time);
2824                    char *p = tf;
2825
2826                    /* skip leading zeros */
2827                    for (; *p == '0' || *p == ':'; p++);
2828
2829                    xs_html_add(poll,
2830                        xs_html_tag("p",
2831                            xs_html_text(L("Closes in")),
2832                            xs_html_text(" "),
2833                            xs_html_text(p)));
2834                }
2835            }
2836        }
2837
2838        xs_html_add(snac_content,
2839            poll);
2840    }
2841
2842    /** attachments **/
2843    xs *attach = get_attachments(msg);
2844
2845    {
2846        /* make custom css for attachments easier */
2847        xs_html *content_attachments = xs_html_tag("div",
2848            xs_html_attr("class", "snac-content-attachments"));
2849
2850        xs_html_add(snac_content,
2851            content_attachments);
2852
2853        const char *content = xs_dict_get(msg, "content");
2854
2855        int c = 0;
2856        const xs_dict *a;
2857        while (xs_list_next(attach, &a, &c)) {
2858            const char *type = xs_dict_get(a, "type");
2859            const char *o_href = xs_dict_get(a, "href");
2860            const char *name = xs_dict_get(a, "name");
2861
2862            if (!xs_is_string(type) || !xs_is_string(o_href))
2863                continue;
2864
2865            /* if this URL is already in the post content, skip */
2866            if (content && xs_str_in(content, o_href) != -1)
2867                continue;
2868
2869            if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
2870                continue;
2871
2872            /* do this attachment include an icon? */
2873            const xs_dict *icon = xs_dict_get(a, "icon");
2874            if (xs_type(icon) == XSTYPE_DICT) {
2875                const char *icon_mtype = xs_dict_get(icon, "mediaType");
2876                const char *icon_url   = xs_dict_get(icon, "url");
2877
2878                if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
2879                    xs_html_add(content_attachments,
2880                        xs_html_tag("a",
2881                            xs_html_attr("href", icon_url),
2882                            xs_html_attr("target", "_blank"),
2883                            xs_html_sctag("img",
2884                                xs_html_attr("loading", "lazy"),
2885                                xs_html_attr("src", icon_url))));
2886                }
2887            }
2888
2889            xs *href = make_url(o_href, proxy, 0);
2890
2891            if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
2892                xs_html_add(content_attachments,
2893                    xs_html_tag("a",
2894                        xs_html_attr("href", href),
2895                        xs_html_attr("target", "_blank"),
2896                        xs_html_sctag("img",
2897                            xs_html_attr("loading", "lazy"),
2898                            xs_html_attr("src", href),
2899                            xs_html_attr("alt", name),
2900                            xs_html_attr("title", name))));
2901            }
2902            else
2903            if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
2904                xs_html_add(content_attachments,
2905                    xs_html_tag("video",
2906                        xs_html_attr("preload", "none"),
2907                        xs_html_attr("style", "width: 100%"),
2908                        xs_html_attr("class", "snac-embedded-video"),
2909                        xs_html_attr("controls", NULL),
2910                        xs_html_attr("src", href),
2911                        xs_html_text(L("Video")),
2912                        xs_html_text(": "),
2913                        xs_html_tag("a",
2914                            xs_html_attr("href", href),
2915                            xs_html_text(name))));
2916            }
2917            else
2918            if (xs_startswith(type, "audio/")) {
2919                xs_html_add(content_attachments,
2920                    xs_html_tag("audio",
2921                        xs_html_attr("preload", "none"),
2922                        xs_html_attr("style", "width: 100%"),
2923                        xs_html_attr("class", "snac-embedded-audio"),
2924                        xs_html_attr("controls", NULL),
2925                        xs_html_attr("src", href),
2926                        xs_html_text(L("Audio")),
2927                        xs_html_text(": "),
2928                        xs_html_tag("a",
2929                            xs_html_attr("href", href),
2930                            xs_html_text(name))));
2931            }
2932            else
2933            if (strcmp(type, "Link") == 0) {
2934                xs_html_add(content_attachments,
2935                    xs_html_tag("p",
2936                        xs_html_tag("a",
2937                            xs_html_attr("href", o_href),
2938                            xs_html_text(href))));
2939
2940                /* do not generate an Alt... */
2941                name = NULL;
2942            }
2943            else {
2944                xs *d_href = xs_dup(o_href);
2945                if (strlen(d_href) > 64) {
2946                    d_href[64] = '\0';
2947                    d_href = xs_str_cat(d_href, "...");
2948                }
2949
2950                xs_html_add(content_attachments,
2951                    xs_html_tag("p",
2952                        xs_html_tag("a",
2953                            xs_html_attr("href", o_href),
2954                            xs_html_text(L("Attachment")),
2955                            xs_html_text(": "),
2956                            xs_html_text(d_href))));
2957
2958                /* do not generate an Alt... */
2959                name = NULL;
2960            }
2961
2962            if (name != NULL && *name) {
2963                xs_html_add(content_attachments,
2964                    xs_html_tag("p",
2965                        xs_html_attr("class", "snac-alt-text"),
2966                        xs_html_tag("details",
2967                            xs_html_tag("summary",
2968                                xs_html_text(L("Alt..."))),
2969                            xs_html_text(name))));
2970            }
2971        }
2972    }
2973
2974    /* has this message an audience (i.e., comes from a channel or community)? */
2975    const char *audience = xs_dict_get(msg, "audience");
2976    if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
2977        xs_html *au_tag = xs_html_tag("p",
2978            xs_html_text("("),
2979            xs_html_tag("a",
2980                xs_html_attr("href", audience),
2981                xs_html_attr("title", L("Source channel or community")),
2982                xs_html_text(audience)),
2983            xs_html_text(")"));
2984
2985        xs_html_add(snac_content_wrap,
2986            au_tag);
2987    }
2988
2989    /* does it have a location? */
2990    const xs_dict *location = xs_dict_get(msg, "location");
2991    if (xs_type(location) == XSTYPE_DICT) {
2992        const xs_number *latitude = xs_dict_get(location, "latitude");
2993        const xs_number *longitude = xs_dict_get(location, "longitude");
2994        const char *name = xs_dict_get(location, "name");
2995        const char *address = xs_dict_get(location, "address");
2996        xs *label_list = xs_list_new();
2997
2998        if (xs_type(name) == XSTYPE_STRING)
2999            label_list = xs_list_append(label_list, name);
3000        if (xs_type(address) == XSTYPE_STRING)
3001            label_list = xs_list_append(label_list, address);
3002
3003        if (xs_list_len(label_list)) {
3004            const char *url = xs_dict_get(location, "url");
3005            xs *label = xs_join(label_list, ", ");
3006
3007            if (xs_type(url) == XSTYPE_STRING) {
3008                xs_html_add(snac_content_wrap,
3009                    xs_html_tag("p",
3010                        xs_html_text(L("Location: ")),
3011                        xs_html_tag("a",
3012                            xs_html_attr("href", url),
3013                            xs_html_attr("target", "_blank"),
3014                            xs_html_text(label))));
3015            }
3016            else
3017            if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
3018                xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
3019                    xs_number_str(latitude), xs_number_str(longitude));
3020
3021                xs_html_add(snac_content_wrap,
3022                    xs_html_tag("p",
3023                        xs_html_text(L("Location: ")),
3024                        xs_html_tag("a",
3025                            xs_html_attr("href", url),
3026                            xs_html_attr("target", "_blank"),
3027                            xs_html_text(label))));
3028            }
3029            else
3030                xs_html_add(snac_content_wrap,
3031                    xs_html_tag("p",
3032                        xs_html_text(L("Location: ")),
3033                        xs_html_text(label)));
3034        }
3035    }
3036
3037    if (strcmp(type, "Event") == 0) { /** Event start and end times **/
3038        const char *s_time = xs_dict_get(msg, "startTime");
3039
3040        if (xs_is_string(s_time) && strlen(s_time) > 20) {
3041            const char *e_time = xs_dict_get(msg, "endTime");
3042            const char *tz     = xs_dict_get(msg, "timezone");
3043
3044            xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
3045            xs *e = NULL;
3046
3047            if (xs_is_string(e_time) && strlen(e_time) > 20)
3048                e = xs_replace_i(xs_dup(e_time), "T", " ");
3049
3050            /* if the event has a timezone, crop the offsets */
3051            if (xs_is_string(tz)) {
3052                s = xs_crop_i(s, 0, 19);
3053
3054                if (e)
3055                    e = xs_crop_i(e, 0, 19);
3056            }
3057            else
3058                tz = "";
3059
3060            /* if start and end share the same day, crop it from the end */
3061            if (e && memcmp(s, e, 11) == 0)
3062                e = xs_crop_i(e, 11, 0);
3063
3064            if (e)
3065                s = xs_str_cat(s, " / ", e);
3066
3067            if (*tz)
3068                s = xs_str_cat(s, " (", tz, ")");
3069
3070            /* replace ugly decimals */
3071            s = xs_replace_i(s, ".000", "");
3072
3073            xs_html_add(snac_content_wrap,
3074                xs_html_tag("p",
3075                    xs_html_text(L("Time: ")),
3076                    xs_html_text(s)));
3077        }
3078    }
3079
3080    /* show all hashtags that has not been shown previously in the content */
3081    const xs_list *tags = xs_dict_get(msg, "tag");
3082    const char *o_content = xs_dict_get_def(msg, "content", "");
3083
3084    if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
3085        xs *content = xs_utf8_to_lower(o_content);
3086        const xs_dict *tag;
3087
3088        xs_html *add_hashtags = xs_html_tag("ul",
3089            xs_html_attr("class", "snac-more-hashtags"));
3090
3091        xs_list_foreach(tags, tag) {
3092            const char *type = xs_dict_get(tag, "type");
3093
3094            if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
3095                const char *o_href = xs_dict_get(tag, "href");
3096                const char *name   = xs_dict_get(tag, "name");
3097
3098                if (xs_is_string(o_href) && xs_is_string(name)) {
3099                    xs *href = xs_utf8_to_lower(o_href);
3100
3101                    if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
3102                        /* not in the content: add here */
3103                        xs_html_add(add_hashtags,
3104                            xs_html_tag("li",
3105                                xs_html_tag("a",
3106                                    xs_html_attr("href", href),
3107                                    xs_html_text(name),
3108                                    xs_html_text(" "))));
3109                    }
3110                }
3111            }
3112        }
3113
3114        xs_html_add(snac_content_wrap,
3115            add_hashtags);
3116    }
3117
3118    /** controls **/
3119
3120    if (!read_only && user) {
3121        xs_html_add(entry,
3122            html_entry_controls(user, actor, msg, md5));
3123    }
3124
3125    /** children **/
3126    if (!hide_children) {
3127        xs *children = object_children(id);
3128        int left     = xs_list_len(children);
3129
3130        if (left) {
3131            xs_html *ch_details = xs_html_tag("details",
3132                xs_html_attr(collapse_threads ? "" : "open", NULL),
3133                xs_html_tag("summary",
3134                    xs_html_text("...")));
3135
3136            xs_html_add(entry,
3137                ch_details);
3138
3139            xs_html *fch_container = xs_html_tag("div",
3140                xs_html_attr("class", "snac-thread-cont"));
3141
3142            xs_html_add(ch_details,
3143                fch_container);
3144
3145            xs_html *ch_container = xs_html_tag("div",
3146                xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
3147
3148            xs_html_add(ch_details,
3149                ch_container);
3150
3151            xs_html *ch_older = NULL;
3152            if (left > 3) {
3153                xs_html_add(ch_container,
3154                    ch_older = xs_html_tag("details",
3155                        xs_html_tag("summary",
3156                            xs_html_text(L("Older...")))));
3157            }
3158
3159            int ctxt = 0;
3160            const char *cmd5;
3161            int cnt = 0;
3162            int o_cnt = 0;
3163            int f_cnt = 0;
3164
3165            /* get the first child */
3166            xs_list_next(children, &cmd5, &ctxt);
3167            xs *f_chd = NULL;
3168
3169            if (user)
3170                timeline_get_by_md5(user, cmd5, &f_chd);
3171            else
3172                object_get_by_md5(cmd5, &f_chd);
3173
3174            if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
3175                const char *p_author = get_atto(msg);
3176                const char *author   = get_atto(f_chd);
3177
3178                /* is the first child from the same author? */
3179                if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
3180                    /* then, don't add it to the children container,
3181                       so that it appears unindented just before the parent
3182                       like a fucking Twitter-like thread */
3183                    xs_html_add(fch_container,
3184                        html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children));
3185
3186                    cnt++;
3187                    f_cnt++;
3188                    left--;
3189                }
3190                else
3191                    ctxt = 0; /* restart from the beginning */
3192            }
3193
3194            while (xs_list_next(children, &cmd5, &ctxt)) {
3195                xs *chd = NULL;
3196
3197                if (user)
3198                    timeline_get_by_md5(user, cmd5, &chd);
3199                else
3200                    object_get_by_md5(cmd5, &chd);
3201
3202                if (chd != NULL) {
3203                    if (xs_is_null(xs_dict_get(chd, "name"))) {
3204                        xs_html *che = html_entry(user, chd, read_only,
3205                            level + 1, cmd5, hide_children);
3206
3207                        if (che != NULL) {
3208                            if (left > 3) {
3209                                xs_html_add(ch_older,
3210                                    che);
3211
3212                                o_cnt++;
3213                            }
3214                            else
3215                                xs_html_add(ch_container,
3216                                    che);
3217
3218                            cnt++;
3219                        }
3220                    }
3221
3222                    left--;
3223                }
3224                else
3225                    srv_debug(2, xs_fmt("cannot read child %s", cmd5));
3226            }
3227
3228            /* if no children were finally added, hide the details */
3229            if (cnt == 0)
3230                xs_html_add(ch_details,
3231                    xs_html_attr("style", "display: none"));
3232
3233            if (o_cnt == 0 && ch_older)
3234                xs_html_add(ch_older,
3235                    xs_html_attr("style", "display: none"));
3236
3237            if (f_cnt == 0)
3238                xs_html_add(fch_container,
3239                    xs_html_attr("style", "display: none"));
3240        }
3241    }
3242
3243    /* add an invisible hr, to help differentiate between posts in text browsers */
3244    xs_html_add(entry_top,
3245        xs_html_sctag("hr",
3246            xs_html_attr("hidden", NULL)));
3247
3248    return entry_top;
3249}
3250
3251
3252xs_html *html_footer(const snac *user)
3253{
3254    return xs_html_tag("div",
3255        xs_html_attr("class", "snac-footer"),
3256        xs_html_tag("a",
3257            xs_html_attr("href", srv_baseurl),
3258            xs_html_text(L("about this site"))),
3259        xs_html_text(" - "),
3260        xs_html_text(L("powered by ")),
3261        xs_html_tag("a",
3262            xs_html_attr("href", WHAT_IS_SNAC_URL),
3263            xs_html_tag("abbr",
3264                xs_html_attr("title", "Social Networks Are Crap"),
3265                xs_html_text(USER_AGENT))));
3266}
3267
3268
3269xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3270                      int skip, int show, int show_more,
3271                      const char *title, const char *page,
3272                      int utl, const char *error, int terse)
3273/* returns the HTML for the timeline */
3274{
3275    xs_list *p = (xs_list *)list;
3276    const char *v;
3277    double t = ftime();
3278    int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
3279
3280    xs *desc = NULL;
3281    xs *alternate = NULL;
3282
3283    if (xs_list_len(list) == 1) {
3284        /* only one element? pick the description from the source */
3285        const char *id = xs_list_get(list, 0);
3286        xs *d = NULL;
3287        object_get_by_md5(id, &d);
3288        const char *sc = xs_dict_get(d, "sourceContent");
3289        if (d && sc != NULL)
3290            desc = xs_dup(sc);
3291
3292        alternate = xs_dup(xs_dict_get(d, "id"));
3293    }
3294
3295    xs_html *head;
3296    xs_html *body;
3297
3298    if (user) {
3299        head = html_user_head(user, desc, alternate);
3300
3301        if (terse)
3302            body = xs_html_tag("body", NULL);
3303        else
3304            body = html_user_body(user, read_only);
3305    }
3306    else {
3307        head = html_instance_head();
3308        body = html_instance_body();
3309    }
3310
3311    xs_html *html = xs_html_tag("html",
3312        head,
3313        body);
3314
3315    if (user && !read_only)
3316        xs_html_add(body,
3317            html_top_controls(user));
3318
3319    if (error != NULL) {
3320        xs_html_add(body,
3321            xs_html_tag("dialog",
3322                xs_html_attr("open", NULL),
3323                xs_html_tag("p",
3324                    xs_html_text(error)),
3325                xs_html_tag("form",
3326                    xs_html_attr("method", "dialog"),
3327                    xs_html_sctag("input",
3328                        xs_html_attr("type", "submit"),
3329                        xs_html_attr("value", L("Dismiss"))))));
3330    }
3331
3332    /* show links to the available lists */
3333    if (user && !read_only) {
3334        xs_html *lol = xs_html_tag("ul",
3335            xs_html_attr("class", "snac-list-of-lists"));
3336        xs_html_add(body, lol);
3337
3338        xs *lists = list_maint(user, NULL, 0); /* get list of lists */
3339
3340        int ct = 0;
3341        const char *v;
3342
3343        while (xs_list_next(lists, &v, &ct)) {
3344            const char *lname = xs_list_get(v, 1);
3345            xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
3346            xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
3347
3348            xs_html_add(lol,
3349                xs_html_tag("li",
3350                    xs_html_tag("a",
3351                        xs_html_attr("href", url),
3352                        xs_html_attr("class", "snac-list-link"),
3353                        xs_html_attr("title", ttl),
3354                        xs_html_text(lname))));
3355        }
3356
3357        {
3358            /* show the list of pinned posts */
3359            xs *url = xs_fmt("%s/pinned", user->actor);
3360            xs_html_add(lol,
3361                xs_html_tag("li",
3362                    xs_html_tag("a",
3363                        xs_html_attr("href", url),
3364                        xs_html_attr("class", "snac-list-link"),
3365                        xs_html_attr("title", L("Pinned posts")),
3366                        xs_html_text(L("pinned")))));
3367        }
3368
3369        {
3370            /* show the list of bookmarked posts */
3371            xs *url = xs_fmt("%s/bookmarks", user->actor);
3372            xs_html_add(lol,
3373                xs_html_tag("li",
3374                    xs_html_tag("a",
3375                        xs_html_attr("href", url),
3376                        xs_html_attr("class", "snac-list-link"),
3377                        xs_html_attr("title", L("Bookmarked posts")),
3378                        xs_html_text(L("bookmarks")))));
3379        }
3380
3381        {
3382            /* show the list of drafts */
3383            xs *url = xs_fmt("%s/drafts", user->actor);
3384            xs_html_add(lol,
3385                xs_html_tag("li",
3386                    xs_html_tag("a",
3387                        xs_html_attr("href", url),
3388                        xs_html_attr("class", "snac-list-link"),
3389                        xs_html_attr("title", L("Post drafts")),
3390                        xs_html_text(L("drafts")))));
3391        }
3392
3393        {
3394            /* show the list of scheduled posts */
3395            xs *url = xs_fmt("%s/sched", user->actor);
3396            xs_html_add(lol,
3397                xs_html_tag("li",
3398                    xs_html_tag("a",
3399                        xs_html_attr("href", url),
3400                        xs_html_attr("class", "snac-list-link"),
3401                        xs_html_attr("title", L("Scheduled posts")),
3402                        xs_html_text(L("scheduled posts")))));
3403        }
3404
3405        /* the list of followed hashtags */
3406        const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
3407
3408        if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
3409            xs_html *loht = xs_html_tag("ul",
3410                xs_html_attr("class", "snac-list-of-lists"));
3411            xs_html_add(body, loht);
3412
3413            const char *ht;
3414
3415            xs_list_foreach(followed_hashtags, ht) {
3416                xs *url = NULL;
3417
3418                if (!xs_startswith(ht, "https:/""/"))
3419                    url = xs_fmt("%s/admin?q=%s", user->actor, ht);
3420                else
3421                    url = xs_dup(ht);
3422
3423                url = xs_replace_i(url, "#", "%23");
3424
3425                xs_html_add(loht,
3426                    xs_html_tag("li",
3427                        xs_html_tag("a",
3428                            xs_html_attr("href", url),
3429                            xs_html_attr("class", "snac-list-link"),
3430                            xs_html_text(ht))));
3431            }
3432        }
3433    }
3434
3435    xs_html_add(body,
3436        xs_html_tag("a",
3437            xs_html_attr("name", "snac-posts")));
3438
3439    xs_html *posts = xs_html_tag("div",
3440        xs_html_attr("class", "snac-posts"));
3441
3442    if (title) {
3443        xs_html_add(posts,
3444            xs_html_tag("h2",
3445                xs_html_attr("class", "snac-header"),
3446                xs_html_text(title)));
3447    }
3448
3449    xs_html_add(body, posts);
3450
3451    int mark_shown = 0;
3452
3453    while (xs_list_iter(&p, &v)) {
3454        xs *msg = NULL;
3455        int status;
3456
3457        /* "already seen" mark? */
3458        if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
3459            if (skip == 0 && !mark_shown) {
3460                xs *s = xs_fmt("%s/admin", user->actor);
3461
3462                xs_html_add(posts,
3463                    xs_html_tag("div",
3464                        xs_html_attr("class", "snac-no-more-unseen-posts"),
3465                        xs_html_text(L("No more unseen posts")),
3466                        xs_html_text(" - "),
3467                        xs_html_tag("a",
3468                            xs_html_attr("href", s),
3469                            xs_html_text(L("Back to top")))));
3470            }
3471
3472            mark_shown = 1;
3473
3474            continue;
3475        }
3476
3477        if (utl && user && !is_pinned_by_md5(user, v))
3478            status = timeline_get_by_md5(user, v, &msg);
3479        else
3480            status = object_get_by_md5(v, &msg);
3481
3482        if (!valid_status(status))
3483            continue;
3484
3485        /* if it's an instance page, discard messages from private users */
3486        if (user == NULL && is_msg_from_private_user(msg))
3487            continue;
3488
3489        const int scope = get_msg_visibility(msg);
3490        if (user != NULL && scope != SCOPE_PUBLIC){
3491            /* is this message a non-public reply? */
3492            const char *irt = get_in_reply_to(msg);
3493
3494            /* is it a reply to something not in the storage? */
3495            if (!xs_is_null(irt) && !object_here(irt)) {
3496                /* is it for me? */
3497                const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
3498                const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
3499
3500                if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
3501                    snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
3502                    continue;
3503                }
3504            }
3505        }
3506        /* hide non-public posts from /instance view */
3507        if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){
3508            continue;
3509        }
3510        /* hide non-public posts viewed from outside */
3511        if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) {
3512            continue;
3513        }
3514
3515        xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
3516
3517        if (entry != NULL)
3518            xs_html_add(posts,
3519                entry);
3520    }
3521
3522    if (list && user && read_only) {
3523        /** history **/
3524        if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
3525            xs_html *ul = xs_html_tag("ul", NULL);
3526
3527            xs_html *history = xs_html_tag("div",
3528                xs_html_attr("class", "snac-history"),
3529                xs_html_tag("p",
3530                    xs_html_attr("class", "snac-history-title"),
3531                    xs_html_text(L("History"))),
3532                    ul);
3533
3534            xs *list = history_list(user);
3535            xs_list *p = list;
3536            const char *v;
3537
3538            while (xs_list_iter(&p, &v)) {
3539                xs *fn  = xs_replace(v, ".html", "");
3540                xs *url = xs_fmt("%s/h/%s", user->actor, v);
3541
3542                xs_html_add(ul,
3543                    xs_html_tag("li",
3544                        xs_html_tag("a",
3545                            xs_html_attr("href", url),
3546                            xs_html_text(fn))));
3547            }
3548
3549            xs_html_add(body,
3550                history);
3551        }
3552    }
3553
3554    {
3555        xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
3556        xs_html_add(body,
3557            xs_html_raw(s1));
3558    }
3559
3560    if (show_more) {
3561        xs *m  = NULL;
3562        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3563
3564        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3565
3566        if (page != NULL)
3567            url = xs_str_cat(url, page);
3568
3569        if (xs_str_in(url, "?") != -1)
3570            m = xs_fmt("%s&%s", url, ss);
3571        else
3572            m = xs_fmt("%s?%s", url, ss);
3573
3574        xs_html *more_links = xs_html_tag("p",
3575            xs_html_tag("a",
3576                xs_html_attr("href", url),
3577                xs_html_attr("name", "snac-more"),
3578                xs_html_text(L("Back to top"))),
3579            xs_html_text(" - "),
3580            xs_html_tag("a",
3581                xs_html_attr("href", m),
3582                xs_html_attr("name", "snac-more"),
3583                xs_html_text(L("More..."))));
3584
3585        xs_html_add(body,
3586            more_links);
3587    }
3588
3589    xs_html_add(body,
3590        html_footer(user));
3591
3592    return xs_html_render_s(html, "<!DOCTYPE html>\n");
3593}
3594
3595
3596xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
3597{
3598    xs_html *snac_posts;
3599    xs_html *people = xs_html_tag("div",
3600        xs_html_tag("h2",
3601            xs_html_attr("class", "snac-header"),
3602            xs_html_text(header)),
3603        snac_posts = xs_html_tag("details",
3604                xs_html_attr("open", NULL),
3605                xs_html_tag("summary",
3606                    xs_html_text("..."))));
3607
3608    xs *redir = xs_fmt("%s/people", user->actor);
3609
3610    const char *actor_id;
3611
3612    xs_list_foreach(list, actor_id) {
3613        xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
3614        xs *actor = NULL;
3615
3616        if (valid_status(actor_get(actor_id, &actor))) {
3617            xs_html *snac_post = xs_html_tag("div",
3618                xs_html_attr("class", "snac-post"),
3619                xs_html_tag("a",
3620                    xs_html_attr("name", md5)),
3621                xs_html_tag("div",
3622                    xs_html_attr("class", "snac-post-header"),
3623                    html_actor_icon(user, actor, xs_dict_get(actor, "published"),
3624                                    NULL, NULL, -1, 1, proxy, NULL, NULL)));
3625
3626            /* content (user bio) */
3627            const char *c = xs_dict_get(actor, "summary");
3628            const xs_val *tag = xs_dict_get(actor, "tag");
3629
3630            if (!xs_is_null(c)) {
3631                xs *sc = sanitize(c);
3632                sc = replace_shortnames(sc, tag, 2, proxy);
3633
3634                xs_html *snac_content = xs_html_tag("div",
3635                    xs_html_attr("class", "snac-content"));
3636
3637                if (xs_startswith(sc, "<p>"))
3638                    xs_html_add(snac_content,
3639                        xs_html_raw(sc)); /* already sanitized */
3640                else
3641                    xs_html_add(snac_content,
3642                        xs_html_tag("p",
3643                            xs_html_raw(sc))); /* already sanitized */
3644
3645                xs_html_add(snac_post, snac_content);
3646            }
3647
3648            /* add user metadata */
3649            xs_html *snac_metadata = xs_html_tag("div",
3650                xs_html_attr("class", "snac-metadata"));
3651
3652            int count = 0;
3653            const xs_val *address = xs_dict_get(actor, "vcard:Address");
3654            if (xs_is_string(address)) {
3655                xs_html_add(snac_metadata,
3656                    xs_html_tag("span",
3657                        xs_html_attr("class", "snac-property-name"),
3658                        xs_html_raw("&#x1F4CD; Location")),
3659                    xs_html_text(":"),
3660                    xs_html_raw("&nbsp;"),
3661                    xs_html_tag("span",
3662                        xs_html_attr("class", "snac-property-value p-adr"),
3663                        xs_html_text(address)),
3664                    xs_html_sctag("br", NULL));
3665
3666                count++;
3667            }
3668
3669            const xs_val *birthday = xs_dict_get(actor, "vcard:bday");
3670            if (xs_is_string(birthday)) {
3671                xs_html_add(snac_metadata,
3672                    xs_html_tag("span",
3673                        xs_html_attr("class", "snac-property-name"),
3674                        xs_html_raw("&#x1F382; Birthday")),
3675                    xs_html_text(":"),
3676                    xs_html_raw("&nbsp;"),
3677                    xs_html_tag("time",
3678                        xs_html_attr("class", "snac-property-value dt-bday"),
3679                        xs_html_text(birthday)),
3680                    xs_html_sctag("br", NULL));
3681
3682                count++;
3683            }
3684
3685            const xs_list *attachment = xs_dict_get(actor, "attachment");
3686            if (count > 0 && xs_list_len(attachment) > 0) {
3687                xs_html_add(snac_metadata,
3688                    xs_html_sctag("hr",
3689                        xs_html_attr("class", "snac-property-divider")));
3690            }
3691
3692            const xs_val *v;
3693            xs_list_foreach(attachment, v) {
3694                const char *type  = xs_dict_get(v, "type");
3695                const char *name  = xs_dict_get(v, "name");
3696                const char *value = xs_dict_get(v, "value");
3697
3698                if (!xs_is_null(type) && !xs_is_null(name) &&
3699                    !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
3700                    /* both the name and the value can contain emoji */
3701                    xs *nam = sanitize(name);
3702                    nam = replace_shortnames(nam, tag, 1, proxy);
3703
3704                    /* todo: sometimes the value is transmitted as markdown and not html ._. */
3705                    xs *val = sanitize(value);
3706                    val = replace_shortnames(val, tag, 1, proxy);
3707
3708                    /* delete <p> tags, because some software sends them */
3709                    val = xs_replace_i(val, "<p>", "");
3710                    val = xs_replace_i(val, "</p>", "");
3711
3712                    xs_html_add(snac_metadata,
3713                        xs_html_tag("span",
3714                            xs_html_attr("class", "snac-property-name"),
3715                            xs_html_raw(nam)),
3716                        xs_html_text(":"),
3717                        xs_html_raw("&nbsp;"),
3718                        xs_html_tag("span",
3719                            xs_html_attr("class", "snac-property-value"),
3720                            xs_html_raw(val)),
3721                        xs_html_sctag("br", NULL));
3722
3723                    count++;
3724                }
3725            }
3726
3727            if (count > 0) {
3728                xs_html_add(snac_post, snac_metadata);
3729            }
3730            else {
3731                /* free the html, by rendering it... */
3732                xs_free(xs_html_render(snac_metadata));
3733            }
3734
3735            /* buttons */
3736            xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
3737
3738            xs_html *snac_controls = xs_html_tag("div",
3739                xs_html_attr("class",   "snac-controls"));
3740
3741            xs_html *form = xs_html_tag("form",
3742                xs_html_attr("autocomplete",    "off"),
3743                xs_html_attr("method",          "post"),
3744                xs_html_attr("action",          btn_form_action),
3745                xs_html_sctag("input",
3746                    xs_html_attr("type",    "hidden"),
3747                    xs_html_attr("name",    "actor"),
3748                    xs_html_attr("value",   actor_id)),
3749                xs_html_sctag("input",
3750                    xs_html_attr("type",    "hidden"),
3751                    xs_html_attr("name",    "hard-redir"),
3752                    xs_html_attr("value",   redir)),
3753                xs_html_sctag("input",
3754                    xs_html_attr("type",    "hidden"),
3755                    xs_html_attr("name",    "actor-form"),
3756                    xs_html_attr("value",   "yes")));
3757
3758            xs_html_add(snac_controls, form);
3759
3760            if (following_check(user, actor_id)) {
3761                xs_html_add(form,
3762                    html_button("unfollow", L("Unfollow"),
3763                                L("Stop following this user's activity")));
3764
3765                if (is_limited(user, actor_id))
3766                    xs_html_add(form,
3767                        html_button("unlimit", L("Unlimit"),
3768                                L("Allow announces (boosts) from this user")));
3769                else
3770                    xs_html_add(form,
3771                        html_button("limit", L("Limit"),
3772                                L("Block announces (boosts) from this user")));
3773            }
3774            else {
3775                xs_html_add(form,
3776                    html_button("follow", L("Follow"),
3777                                L("Start following this user's activity")));
3778
3779                if (follower_check(user, actor_id))
3780                    xs_html_add(form,
3781                        html_button("delete", L("Delete"), L("Delete this user")));
3782            }
3783
3784            if (pending_check(user, actor_id)) {
3785                xs_html_add(form,
3786                    html_button("approve", L("Approve"),
3787                                L("Approve this follow request")));
3788
3789                xs_html_add(form,
3790                    html_button("discard", L("Discard"), L("Discard this follow request")));
3791            }
3792
3793            if (is_muted(user, actor_id))
3794                xs_html_add(form,
3795                    html_button("unmute", L("Unmute"),
3796                                L("Stop blocking activities from this user")));
3797            else
3798                xs_html_add(form,
3799                    html_button("mute", L("MUTE"),
3800                                L("Block any activity from this user")));
3801
3802            /* the post textarea */
3803            xs *dm_div_id  = xs_fmt("%s_%s_dm", md5, t);
3804            xs *dm_form_id = xs_fmt("%s_reply_form", md5);
3805
3806            xs_html_add(snac_controls,
3807                xs_html_tag("p", NULL),
3808                html_note(user, L("Direct Message..."),
3809                    dm_div_id, dm_form_id,
3810                    "", "",
3811                    NULL, actor_id,
3812                    xs_stock(XSTYPE_FALSE), "",
3813                    SCOPE_MENTIONED, NULL,
3814                    NULL, 0, NULL, NULL, 0, NULL, NULL),
3815                xs_html_tag("p", NULL));
3816
3817            xs_html_add(snac_post, snac_controls);
3818
3819            xs_html_add(snac_posts, snac_post);
3820        }
3821    }
3822
3823    return people;
3824}
3825
3826
3827xs_str *html_people(snac *user)
3828{
3829    const char *proxy = NULL;
3830
3831    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3832        proxy = user->actor;
3833
3834    xs *wing = following_list(user);
3835    xs *wers = follower_list(user);
3836    xs *pending = pending_list(user);
3837
3838    xs_html *lists = xs_html_tag("div",
3839        xs_html_attr("class", "snac-posts"));
3840
3841    if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
3842        xs_html_add(lists,
3843            html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
3844    }
3845
3846    xs_html_add(lists,
3847        html_people_list(user, wing, L("People you follow"), "i", proxy),
3848        html_people_list(user, wers, L("People that follow you"), "e", proxy));
3849
3850    xs_html *html = xs_html_tag("html",
3851        html_user_head(user, NULL, NULL),
3852        xs_html_add(html_user_body(user, 0),
3853            lists,
3854            html_footer(user)));
3855
3856    return xs_html_render_s(html, "<!DOCTYPE html>\n");
3857}
3858
3859/* Filter list to display only posts by actor.  We'll probably show
3860   fewer than show posts.  Should we try harder to find some?  */
3861xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3862                        int skip, int show, int show_more, const char *page)
3863{
3864    const char *proxy = NULL;
3865    xs_list *p = (xs_list *)list;
3866    const char *v;
3867
3868    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3869        proxy = user->actor;
3870
3871    xs_html *body = html_user_body(user, 0);
3872
3873    xs_html *lists = xs_html_tag("div",
3874        xs_html_attr("class", "snac-posts"));
3875
3876    xs *foll = xs_list_append(xs_list_new(), actor);
3877
3878    xs_html_add(lists,
3879        html_people_list(user, foll, L("Contact's posts"), "p", proxy));
3880
3881    xs_html_add(body, lists);
3882
3883    while (xs_list_iter(&p, &v)) {
3884        xs *msg = NULL;
3885        int status;
3886
3887        status = timeline_get_by_md5(user, v, &msg);
3888
3889        if (!valid_status(status))
3890            continue;
3891
3892        const char *id = xs_dict_get(msg, "id");
3893        const char *by = get_atto(msg);
3894        xs *actor_md5 = NULL;
3895        xs_list *boosts = NULL;
3896        xs_list *likes = NULL;
3897        xs_list *reacts = NULL;
3898        /* Besides actor's posts, also show actor's boosts, and also
3899           posts by user with likes or reacts by actor.  I.e., any
3900           actor's actions that user could have seen in the timeline
3901           or in notifications.  */
3902        if (!(by && strcmp(actor, by) == 0) &&
3903            xs_list_in((boosts = object_announces(id)),
3904                       (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
3905            (!(by && strcmp(user->actor, by) == 0) ||
3906             (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
3907              xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
3908            continue;
3909
3910        xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
3911
3912        if (entry != NULL)
3913            xs_html_add(lists,
3914                entry);
3915    }
3916
3917    if (show_more) {
3918        xs *m  = NULL;
3919        xs *m10  = NULL;
3920        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3921
3922        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3923
3924        if (page != NULL)
3925            url = xs_str_cat(url, page);
3926
3927        if (xs_str_in(url, "?") != -1)
3928            m = xs_fmt("%s&%s", url, ss);
3929        else
3930            m = xs_fmt("%s?%s", url, ss);
3931
3932        m10 = xs_fmt("%s0", m);
3933
3934        xs_html *more_links = xs_html_tag("p",
3935            xs_html_tag("a",
3936                xs_html_attr("href", url),
3937                xs_html_attr("name", "snac-more"),
3938                xs_html_text(L("Back to top"))),
3939            xs_html_text(" - "),
3940            xs_html_tag("a",
3941                xs_html_attr("href", m),
3942                xs_html_attr("name", "snac-more"),
3943                xs_html_text(L("More..."))),
3944            xs_html_text(" - "),
3945            xs_html_tag("a",
3946                xs_html_attr("href", m10),
3947                xs_html_attr("name", "snac-more"),
3948                xs_html_text(L("More (x 10)..."))));
3949
3950        xs_html_add(body,
3951            more_links);
3952    }
3953
3954    xs_html *html = xs_html_tag("html",
3955        html_user_head(user, NULL, NULL),
3956        xs_html_add(body,
3957            html_footer(user)));
3958
3959    return xs_html_render_s(html, "<!DOCTYPE html>\n");
3960}
3961
3962xs_str *html_notifications(snac *user, int skip, int show)
3963{
3964    const char *proxy = NULL;
3965
3966    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3967        proxy = user->actor;
3968
3969    xs *n_list = notify_list(user, skip, show);
3970    xs *n_time = notify_check_time(user, 0);
3971
3972    xs_html *body = html_user_body(user, 0);
3973
3974    xs_html *html = xs_html_tag("html",
3975        html_user_head(user, NULL, NULL),
3976        body);
3977
3978    xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
3979
3980    xs_html_add(body,
3981        xs_html_tag("form",
3982            xs_html_attr("autocomplete", "off"),
3983            xs_html_attr("method",       "post"),
3984            xs_html_attr("action",       clear_all_action),
3985            xs_html_attr("id",           "clear"),
3986            xs_html_sctag("input",
3987                xs_html_attr("type",     "submit"),
3988                xs_html_attr("class",    "snac-btn-like"),
3989                xs_html_attr("value",    L("Clear all")))));
3990
3991    xs_html *noti_new = NULL;
3992    xs_html *noti_seen = NULL;
3993
3994    xs_html *posts = xs_html_tag("div",
3995        xs_html_attr("class", "snac-posts"));
3996    xs_html_add(body, posts);
3997
3998    xs_set rep;
3999    xs_set_init(&rep);
4000
4001    /* dict to store previous notification labels */
4002    xs *admiration_labels = xs_dict_new();
4003
4004    const xs_str *v;
4005
4006    xs_list_foreach(n_list, v) {
4007        xs *noti = notify_get(user, v);
4008
4009        if (noti == NULL)
4010            continue;
4011
4012        xs *obj = NULL;
4013        const char *type  = xs_dict_get(noti, "type");
4014        const char *utype = xs_dict_get(noti, "utype");
4015        const char *id    = xs_dict_get(noti, "objid");
4016        const char *date  = xs_dict_get(noti, "date");
4017        const char *id2   = xs_dict_get_path(noti, "msg.id");
4018        xs *wrk = NULL;
4019
4020        if (xs_is_null(id))
4021            continue;
4022
4023        if (is_hidden(user, id))
4024            continue;
4025
4026        if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
4027            continue;
4028
4029        object_get(id, &obj);
4030
4031        const char *msg_id = NULL;
4032
4033        if (xs_is_dict(obj))
4034            msg_id = xs_dict_get(obj, "id");
4035
4036        const char *actor_id = xs_dict_get(noti, "actor");
4037        xs *actor = NULL;
4038        xs *a_name = NULL;
4039
4040        if (valid_status(actor_get(actor_id, &actor)))
4041            a_name = actor_name(actor, proxy);
4042        else {
4043            a_name = xs_dup(actor_id);
4044
4045            /* actor not here: request it */
4046            enqueue_actor_refresh(user, actor_id, 0);
4047        }
4048
4049        xs *label_sanitized = sanitize(type);
4050        const char *label = label_sanitized;
4051
4052        if (strcmp(type, "Create") == 0)
4053            label = L("Mention");
4054        else
4055        if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
4056            label = L("Finished poll");
4057        else
4058        if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
4059            label = L("Unfollow");
4060        else
4061        if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
4062            const char *content = xs_dict_get_path(noti, "msg.content");
4063
4064            xs *cd = xs_dup(content);
4065            const char *sna = cd;
4066            const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
4067            unsigned int utf = xs_utf8_dec((const char **)&sna);
4068
4069            int isEmoji = 0;
4070            if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
4071                isEmoji = 1;
4072
4073            if (xs_type(content) == XSTYPE_STRING) {
4074                xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
4075                wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
4076                label = wrk;
4077            }
4078        }
4079        else
4080        if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
4081            label = L("Follow Request");
4082
4083        xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
4084
4085        xs_html *this_html_label = xs_html_container(
4086                xs_html_tag("b",
4087                    xs_html_raw(label),
4088                    xs_html_text(" by "),
4089                    xs_html_tag("a",
4090                        xs_html_attr("href", actor_id),
4091                        xs_html_raw(a_name))), /* a_name is already sanitized */
4092                xs_html_text(" "),
4093                xs_html_tag("time",
4094                    xs_html_attr("class", "dt-published snac-pubdate"),
4095                    xs_html_attr("title", date),
4096                    xs_html_text(s_date)));
4097
4098        xs_html *html_label = NULL;
4099
4100        if (xs_is_string(msg_id)) {
4101            const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
4102
4103            if (xs_type(prev_label) == XSTYPE_DATA) {
4104                /* there is a previous list of admiration labels! */
4105                xs_data_get(&html_label, prev_label);
4106
4107                xs_html_add(html_label,
4108                    xs_html_sctag("br", NULL),
4109                    this_html_label);
4110
4111                continue;
4112            }
4113        }
4114
4115        xs_html *entry = NULL;
4116
4117        html_label = xs_html_tag("p",
4118            this_html_label);
4119
4120        /* store in the admiration labels dict */
4121        xs *pl = xs_data_new(&html_label, sizeof(html_label));
4122
4123        if (xs_is_string(msg_id))
4124            admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
4125
4126        entry = xs_html_tag("div",
4127            xs_html_attr("class", "snac-post-with-desc"),
4128            html_label);
4129
4130        if (strcmp(type, "Block") == 0) {
4131            if (actor)
4132                xs_html_add(entry,
4133                    xs_html_tag("div",
4134                        xs_html_attr("class", "snac-post"),
4135                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4136        }
4137        else
4138        if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
4139            if (actor) {
4140                xs *action = xs_fmt("%s/admin/action", user->actor);
4141                xs_html *button = NULL;
4142
4143                if (following_check(user, actor_id))
4144                    button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"));
4145                else
4146                    button = html_button("follow", L("Follow"), L("Start following this user's activity"));
4147
4148                xs_html_add(entry,
4149                    xs_html_tag("div",
4150                        xs_html_attr("class", "snac-post"),
4151                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL),
4152                        xs_html_tag("form",
4153                            xs_html_attr("method", "post"),
4154                            xs_html_attr("action", action),
4155                            xs_html_sctag("input",
4156                                xs_html_attr("type", "hidden"),
4157                                xs_html_attr("name", "actor"),
4158                                xs_html_attr("value", actor_id)),
4159                            button,
4160                            xs_html_sctag("br", NULL))));
4161            }
4162        }
4163        else
4164        if (strcmp(type, "Move") == 0) {
4165            const xs_dict *o_msg = xs_dict_get(noti, "msg");
4166            const char *target;
4167
4168            if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
4169                xs *old_actor = NULL;
4170
4171                if (valid_status(actor_get(target, &old_actor))) {
4172                    xs_html_add(entry,
4173                        xs_html_tag("div",
4174                            xs_html_attr("class", "snac-post"),
4175                            html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4176                }
4177            }
4178        }
4179        else
4180        if (obj != NULL) {
4181            xs *md5 = xs_md5_hex(id, strlen(id));
4182            xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
4183
4184            xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
4185
4186            if (h != NULL) {
4187                xs_html_add(entry,
4188                    xs_html_tag("p",
4189                        xs_html_tag("a",
4190                            xs_html_attr("href", ctxt),
4191                            xs_html_text(L("Context")))),
4192                    h);
4193            }
4194            else
4195                xs_html_add(entry,
4196                    xs_html_tag("p",
4197                        xs_html_text(L("Location: ")),
4198                        xs_html_tag("a",
4199                            xs_html_attr("href", id),
4200                            xs_html_text(id))));
4201        }
4202
4203        if (strcmp(v, n_time) > 0) {
4204            /* unseen notification */
4205            if (noti_new == NULL) {
4206                noti_new = xs_html_tag("div",
4207                    xs_html_tag("h2",
4208                        xs_html_attr("class", "snac-header"),
4209                        xs_html_text(L("New"))));
4210
4211                xs_html_add(posts,
4212                    noti_new);
4213            }
4214
4215            xs_html_add(noti_new,
4216                entry);
4217        }
4218        else {
4219            /* already seen notification */
4220            if (noti_seen == NULL) {
4221                noti_seen = xs_html_tag("div",
4222                    xs_html_tag("h2",
4223                        xs_html_attr("class", "snac-header"),
4224                        xs_html_text(L("Already seen"))));
4225
4226                xs_html_add(posts,
4227                    noti_seen);
4228            }
4229
4230            xs_html_add(noti_seen,
4231                entry);
4232        }
4233    }
4234
4235    if (noti_new == NULL && noti_seen == NULL)
4236        xs_html_add(body,
4237            xs_html_tag("h2",
4238                xs_html_attr("class", "snac-header"),
4239                xs_html_text(L("None"))));
4240
4241    /* add the navigation footer */
4242    xs *next_p = notify_list(user, skip + show, 1);
4243    if (xs_list_len(next_p)) {
4244        xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
4245            user->actor, skip + show, show);
4246
4247        xs_html_add(body,
4248            xs_html_tag("p",
4249                xs_html_tag("a",
4250                    xs_html_attr("href", url),
4251                    xs_html_text(L("More...")))));
4252    }
4253
4254    xs_set_free(&rep);
4255
4256    xs_html_add(body,
4257        html_footer(user));
4258
4259    /* set the check time to now */
4260    xs *dummy = notify_check_time(user, 1);
4261    dummy = xs_free(dummy);
4262
4263    timeline_touch(user);
4264
4265    return xs_html_render_s(html, "<!DOCTYPE html>\n");
4266}
4267
4268
4269void set_user_lang(snac *user)
4270/* sets the language dict according to user configuration */
4271{
4272    user->lang = NULL;
4273    const char *lang = xs_dict_get(user->config, "lang");
4274
4275    if (xs_is_string(lang))
4276        user->lang = xs_dict_get(srv_langs, lang);
4277}
4278
4279
4280int html_get_handler(const xs_dict *req, const char *q_path,
4281                     char **body, int *b_size, char **ctype,
4282                     xs_str **etag, xs_str **last_modified)
4283{
4284    const char *accept = xs_dict_get(req, "accept");
4285    int status = HTTP_STATUS_NOT_FOUND;
4286    const snac *user = NULL;
4287    snac snac;
4288    xs *uid = NULL;
4289    const char *p_path;
4290    int cache = 1;
4291    int save = 1;
4292    int proxy = 0;
4293    int terse = 0;
4294    const char *v;
4295
4296    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
4297
4298    xs *l = xs_split_n(q_path, "/", 2);
4299    v = xs_list_get(l, 1);
4300
4301    if (xs_is_null(v)) {
4302        srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
4303        return HTTP_STATUS_NOT_FOUND;
4304    }
4305
4306    if (!xs_is_null(xs_dict_get(q_vars, "terse")))
4307        terse = 1;
4308
4309    if (strcmp(v, "share-bridge") == 0) {
4310        /* temporary redirect for a post */
4311        const char *login = xs_dict_get(q_vars, "login");
4312        const char *content = xs_dict_get(q_vars, "content");
4313
4314        if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
4315            xs *b64 = xs_base64_enc(content, strlen(content));
4316
4317            srv_log(xs_fmt("share-bridge for user '%s'", login));
4318
4319            *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
4320            return HTTP_STATUS_SEE_OTHER;
4321        }
4322        else
4323            return HTTP_STATUS_NOT_FOUND;
4324    }
4325    else
4326    if (strcmp(v, "auth-int-bridge") == 0) {
4327        const char *login  = xs_dict_get(q_vars, "login");
4328        const char *id     = xs_dict_get(q_vars, "id");
4329        const char *action = xs_dict_get(q_vars, "action");
4330
4331        if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
4332            *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
4333                srv_baseurl, login, action, id);
4334
4335            return HTTP_STATUS_SEE_OTHER;
4336        }
4337        else
4338            return HTTP_STATUS_NOT_FOUND;
4339    }
4340
4341    uid = xs_dup(v);
4342
4343    /* rss extension? */
4344    if (xs_endswith(uid, ".rss")) {
4345        uid = xs_crop_i(uid, 0, -4);
4346        p_path = ".rss";
4347    }
4348    else
4349        p_path = xs_list_get(l, 2);
4350
4351    if (!uid || !user_open(&snac, uid)) {
4352        /* invalid user */
4353        status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
4354        srv_debug(1, xs_fmt("html_get_handler bad user %s %d", uid, status));
4355        return status;
4356    }
4357
4358    user = &snac; /* for L() */
4359    set_user_lang(&snac);
4360
4361    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4362        proxy = 1;
4363
4364    /* return the RSS if requested by Accept header */
4365    if (accept != NULL) {
4366        if (xs_str_in(accept, "text/xml") != -1 ||
4367            xs_str_in(accept, "application/rss+xml") != -1)
4368            p_path = ".rss";
4369    }
4370
4371    /* check if server config variable 'disable_cache' is set */
4372    if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
4373        cache = 0;
4374
4375    int skip = 0;
4376    const char *max_show_default = "50";
4377    int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
4378                                 max_show_default));
4379    int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
4380                                 xs_dict_get_def(srv_config, "max_timeline_entries",
4381                                 max_show_default)));
4382    int show = def_show;
4383
4384    if ((v = xs_dict_get(q_vars, "skip")) != NULL)
4385        skip = atoi(v), cache = 0, save = 0;
4386    if ((v = xs_dict_get(q_vars, "show")) != NULL)
4387        show = atoi(v), cache = 0, save = 0;
4388    if ((v = xs_dict_get(q_vars, "da")) != NULL) {
4389        /* user dismissed an announcement */
4390        if (login(&snac, req)) {
4391            double ts = atof(v);
4392            xs *timestamp = xs_number_new(ts);
4393            srv_log(xs_fmt("user dismissed announcements until %d", ts));
4394            snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
4395            user_persist(&snac, 0);
4396        }
4397    }
4398
4399    /* get a possible error message */
4400    const char *error = xs_dict_get(q_vars, "error");
4401    if (error != NULL)
4402        cache = 0;
4403
4404    /* a show of 0 has no sense */
4405    if (show == 0)
4406        show = def_show;
4407    if (show > max_show)
4408        show = max_show;
4409
4410    if (p_path == NULL) { /** public timeline **/
4411        xs *h = xs_str_localtime(0, "%Y-%m.html");
4412
4413        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
4414            /** empty public timeline for private users **/
4415            *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse);
4416            *b_size = strlen(*body);
4417            status  = HTTP_STATUS_OK;
4418        }
4419        else
4420        if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
4421            snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
4422
4423            status = history_get(&snac, h, body, b_size,
4424                        xs_dict_get(req, "if-none-match"), etag);
4425        }
4426        else {
4427            xs *list = NULL;
4428            int more = 0;
4429
4430            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
4431                list = timeline_simple_list(&snac, "public", skip, show, &more);
4432            else
4433                list = timeline_list(&snac, "public", skip, show, &more);
4434
4435            xs *pins = pinned_list(&snac);
4436            pins = xs_list_cat(pins, list);
4437
4438            *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse);
4439
4440            *b_size = strlen(*body);
4441            status  = HTTP_STATUS_OK;
4442
4443            if (save)
4444                history_add(&snac, h, *body, *b_size, etag);
4445        }
4446    }
4447    else
4448    if (strcmp(p_path, "admin") == 0) { /** private timeline **/
4449        if (!login(&snac, req)) {
4450            *body  = xs_dup(uid);
4451            status = HTTP_STATUS_UNAUTHORIZED;
4452        }
4453        else {
4454            const char *q = NULL;
4455            xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
4456            xs *url_acct = NULL;
4457
4458            if (xs_is_string(cq)) {
4459                cq = xs_strip_i(cq);
4460                q = cq;
4461            }
4462
4463            /* searching for an URL? */
4464            if (q && xs_match(q, "https://*|http://*")) {
4465                /* may by an actor; try a webfinger */
4466                xs *actor_obj = NULL;
4467
4468                if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) {
4469                    /* it's an actor; do the dirty trick of changing q to the account name */
4470                    q = url_acct;
4471                }
4472                else {
4473                    /* bring it to the user's timeline */
4474                    xs *object = NULL;
4475                    int status;
4476
4477                    status = activitypub_request(&snac, q, &object);
4478                    snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
4479
4480                    if (valid_status(status)) {
4481                        /* got it; also request the actor */
4482                        const char *attr_to = get_atto(object);
4483
4484                        if (!xs_is_null(attr_to)) {
4485                            status = actor_request(&snac, attr_to, &actor_obj);
4486
4487                            if (valid_status(status)) {
4488                                /* reset the query string to be the real id */
4489                                url_acct = xs_dup(xs_dict_get(object, "id"));
4490                                q = url_acct;
4491
4492                                /* add the post to the timeline */
4493                                if (!timeline_here(&snac, q))
4494                                    timeline_add(&snac, q, object);
4495                            }
4496                        }
4497                        else {
4498                            /* retry webfinger, this time with the 'official' id */
4499                            const char *id = xs_dict_get(object, "id");
4500
4501                            if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) &&
4502                                xs_is_string(url_acct))
4503                                q = url_acct;
4504                        }
4505                    }
4506                }
4507
4508                /* fall through */
4509            }
4510
4511            if (q && *q) {
4512                if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
4513                    /** search account **/
4514                    xs *actor = NULL;
4515                    xs *acct = NULL;
4516                    xs *l = xs_list_new();
4517                    xs_html *page = NULL;
4518
4519                    if (valid_status(webfinger_request(q, &actor, &acct))) {
4520                        xs *actor_obj = NULL;
4521
4522                        if (valid_status(actor_request(&snac, actor, &actor_obj))) {
4523                            actor_add(actor, actor_obj);
4524
4525                            /* create a people list with only one element */
4526                            l = xs_list_append(l, actor);
4527
4528                            xs *title = xs_fmt(L("Search results for account %s"), q);
4529
4530                            page = html_people_list(&snac, l, title, "wf", NULL);
4531                        }
4532                    }
4533
4534                    if (page == NULL) {
4535                        xs *title = xs_fmt(L("Account %s not found"), q);
4536
4537                        page = xs_html_tag("div",
4538                            xs_html_tag("h2",
4539                                xs_html_attr("class", "snac-header"),
4540                                xs_html_text(title)));
4541                    }
4542
4543                    xs_html *html = xs_html_tag("html",
4544                        html_user_head(&snac, NULL, NULL),
4545                        xs_html_add(html_user_body(&snac, 0),
4546                        page,
4547                        html_footer(user)));
4548
4549                    *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
4550                    *b_size = strlen(*body);
4551                    status = HTTP_STATUS_OK;
4552                }
4553                else
4554                if (*q == '#') {
4555                    /** search by tag **/
4556                    xs *tl = tag_search(q, skip, show + 1);
4557                    int more = 0;
4558                    if (xs_list_len(tl) >= show + 1) {
4559                        /* drop the last one */
4560                        tl = xs_list_del(tl, -1);
4561                        more = 1;
4562                    }
4563
4564                    xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
4565                    xs *title = xs_fmt(xs_list_len(tl) ?
4566                        L("Search results for tag %s") : L("Nothing found for tag %s"), q);
4567
4568                    *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse);
4569                    *b_size = strlen(*body);
4570                    status  = HTTP_STATUS_OK;
4571                }
4572                else {
4573                    /** search by content **/
4574                    int to = 0;
4575                    int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
4576                    xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
4577                    xs *title = NULL;
4578                    xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
4579                    int tl_len = xs_list_len(tl);
4580
4581                    if (to)
4582                        title = xs_fmt(L("Search results for '%s' (may be more)"), q);
4583                    else
4584                    if (tl_len)
4585                        title = xs_fmt(L("Search results for '%s'"), q);
4586                    else
4587                    if (skip)
4588                        title = xs_fmt(L("No more matches for '%s'"), q);
4589                    else
4590                        title = xs_fmt(L("Nothing found for '%s'"), q);
4591
4592                    *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
4593                                            title, page, 0, error, terse);
4594                    *b_size = strlen(*body);
4595                    status  = HTTP_STATUS_OK;
4596                }
4597            }
4598            else {
4599                /** the private timeline **/
4600                double t = history_mtime(&snac, "timeline.html_");
4601
4602                /* if enabled by admin, return a cached page if its timestamp is:
4603                   a) newer than the timeline timestamp
4604                   b) newer than the start time of the server
4605                */
4606                if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
4607                    snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
4608
4609                    status = history_get(&snac, "timeline.html_", body, b_size,
4610                                xs_dict_get(req, "if-none-match"), etag);
4611                }
4612                else {
4613                    int more = 0;
4614
4615                    snac_debug(&snac, 1, xs_fmt("building timeline"));
4616
4617                    xs *list = timeline_list(&snac, "private", skip, show, &more);
4618
4619                    *body = html_timeline(&snac, list, 0, skip, show,
4620                            more, NULL, "/admin", 1, error, terse);
4621
4622                    *b_size = strlen(*body);
4623                    status  = HTTP_STATUS_OK;
4624
4625                    if (save)
4626                        history_add(&snac, "timeline.html_", *body, *b_size, etag);
4627
4628                    timeline_add_mark(&snac);
4629                }
4630            }
4631        }
4632    }
4633    else
4634    if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
4635        if (!login(&snac, req)) {
4636            *body  = xs_dup(uid);
4637            status = HTTP_STATUS_UNAUTHORIZED;
4638        }
4639        else {
4640            xs *l = xs_split(p_path, "/");
4641            const char *md5 = xs_list_get(l, -1);
4642
4643            if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) {
4644                xs *list0 = xs_list_append(xs_list_new(), md5);
4645                xs *list  = timeline_top_level(&snac, list0);
4646
4647                *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse);
4648                *b_size = strlen(*body);
4649                status  = HTTP_STATUS_OK;
4650            }
4651        }
4652    }
4653    else
4654    if (strcmp(p_path, "people") == 0) { /** the list of people **/
4655        if (!login(&snac, req)) {
4656            *body  = xs_dup(uid);
4657            status = HTTP_STATUS_UNAUTHORIZED;
4658        }
4659        else {
4660            *body   = html_people(&snac);
4661            *b_size = strlen(*body);
4662            status  = HTTP_STATUS_OK;
4663        }
4664    }
4665    else
4666    if (xs_startswith(p_path, "people/")) { /** a single actor **/
4667        if (!login(&snac, req)) {
4668            *body  = xs_dup(uid);
4669            status = HTTP_STATUS_UNAUTHORIZED;
4670        }
4671        else {
4672            xs *actor_dict = NULL;
4673            const char *actor_id = NULL;
4674            xs *actor = NULL;
4675            xs_list *page_lst = xs_split_n(p_path, "?", 2);
4676            xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
4677            xs_list *l = xs_split_n(page, "/", 3);
4678            const char *actor_md5 = xs_list_get(l, 2);
4679
4680            if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
4681               (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
4682               valid_status(actor_get(actor_id, &actor))) {
4683                int more = 0;
4684                xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
4685
4686                *body   = html_people_one(&snac, actor_id, list, skip, show, more, page);
4687                *b_size = strlen(*body);
4688                status  = HTTP_STATUS_OK;
4689            }
4690            else {
4691                *body = xs_dup(uid);
4692                status = HTTP_STATUS_NOT_FOUND;
4693            }
4694        }
4695    }
4696    else
4697    if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
4698        if (!login(&snac, req)) {
4699            *body  = xs_dup(uid);
4700            status = HTTP_STATUS_UNAUTHORIZED;
4701        }
4702        else {
4703            *body   = html_notifications(&snac, skip, show);
4704            *b_size = strlen(*body);
4705            status  = HTTP_STATUS_OK;
4706        }
4707    }
4708    else
4709    if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
4710        if (!login(&snac, req)) {
4711            *body  = xs_dup(uid);
4712            status = HTTP_STATUS_UNAUTHORIZED;
4713        }
4714        else {
4715            xs *list = timeline_instance_list(skip, show);
4716            xs *next = timeline_instance_list(skip + show, 1);
4717
4718            *body = html_timeline(&snac, list, 0, skip, show,
4719                xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse);
4720            *b_size = strlen(*body);
4721            status  = HTTP_STATUS_OK;
4722        }
4723    }
4724    else
4725    if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
4726        if (!login(&snac, req)) {
4727            *body  = xs_dup(uid);
4728            status = HTTP_STATUS_UNAUTHORIZED;
4729        }
4730        else {
4731            xs *list = pinned_list(&snac);
4732
4733            *body = html_timeline(&snac, list, 0, skip, show,
4734                0, L("Pinned posts"), "", 0, error, terse);
4735            *b_size = strlen(*body);
4736            status  = HTTP_STATUS_OK;
4737        }
4738    }
4739    else
4740    if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
4741        if (!login(&snac, req)) {
4742            *body  = xs_dup(uid);
4743            status = HTTP_STATUS_UNAUTHORIZED;
4744        }
4745        else {
4746            xs *list = bookmark_list(&snac);
4747
4748            *body = html_timeline(&snac, list, 0, skip, show,
4749                0, L("Bookmarked posts"), "", 0, error, terse);
4750            *b_size = strlen(*body);
4751            status  = HTTP_STATUS_OK;
4752        }
4753    }
4754    else
4755    if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
4756        if (!login(&snac, req)) {
4757            *body  = xs_dup(uid);
4758            status = HTTP_STATUS_UNAUTHORIZED;
4759        }
4760        else {
4761            xs *list = draft_list(&snac);
4762
4763            *body = html_timeline(&snac, list, 0, skip, show,
4764                0, L("Post drafts"), "", 0, error, terse);
4765            *b_size = strlen(*body);
4766            status  = HTTP_STATUS_OK;
4767        }
4768    }
4769    else
4770    if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
4771        if (!login(&snac, req)) {
4772            *body  = xs_dup(uid);
4773            status = HTTP_STATUS_UNAUTHORIZED;
4774        }
4775        else {
4776            xs *list = scheduled_list(&snac);
4777
4778            *body = html_timeline(&snac, list, 0, skip, show,
4779                0, L("Scheduled posts"), "", 0, error, terse);
4780            *b_size = strlen(*body);
4781            status  = HTTP_STATUS_OK;
4782        }
4783    }
4784    else
4785    if (xs_startswith(p_path, "list/")) { /** list timelines **/
4786        if (!login(&snac, req)) {
4787            *body  = xs_dup(uid);
4788            status = HTTP_STATUS_UNAUTHORIZED;
4789        }
4790        else {
4791            xs *l = xs_split(p_path, "/");
4792            const char *lid = xs_list_get(l, -1);
4793
4794            xs *list = list_timeline(&snac, lid, skip, show);
4795            xs *next = list_timeline(&snac, lid, skip + show, 1);
4796
4797            if (list != NULL) {
4798                xs *ttl = timeline_top_level(&snac, list);
4799
4800                xs *base = xs_fmt("/list/%s", lid);
4801                xs *name = list_maint(&snac, lid, 3);
4802                xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
4803
4804                *body = html_timeline(&snac, ttl, 0, skip, show,
4805                    xs_list_len(next), title, base, 1, error, terse);
4806                *b_size = strlen(*body);
4807                status  = HTTP_STATUS_OK;
4808            }
4809        }
4810    }
4811    else
4812    if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
4813        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4814            return HTTP_STATUS_FORBIDDEN;
4815
4816        xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
4817        xs *msg = NULL;
4818
4819        if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) {
4820            xs *md5  = xs_md5_hex(id, strlen(id));
4821            xs *list = xs_list_new();
4822
4823            list = xs_list_append(list, md5);
4824
4825            *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse);
4826            *b_size = strlen(*body);
4827            status  = HTTP_STATUS_OK;
4828        }
4829    }
4830    else
4831    if (xs_startswith(p_path, "s/")) { /** a static file **/
4832        xs *l    = xs_split(p_path, "/");
4833        const char *id = xs_list_get(l, 1);
4834        int sz;
4835
4836        if (id && *id) {
4837            status = static_get(&snac, id, body, &sz,
4838                        xs_dict_get(req, "if-none-match"), etag);
4839
4840            if (valid_status(status)) {
4841                *b_size = sz;
4842                *ctype  = (char *)xs_mime_by_ext(id);
4843            }
4844        }
4845    }
4846    else
4847    if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
4848        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4849            return HTTP_STATUS_FORBIDDEN;
4850
4851        if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
4852            return HTTP_STATUS_FORBIDDEN;
4853
4854        xs *l = xs_split(p_path, "/");
4855        const char *id = xs_list_get(l, 1);
4856
4857        if (id && *id) {
4858            if (xs_endswith(id, "timeline.html_")) {
4859                /* Don't let them in */
4860                *b_size = 0;
4861                status = HTTP_STATUS_NOT_FOUND;
4862            }
4863            else
4864                status = history_get(&snac, id, body, b_size,
4865                            xs_dict_get(req, "if-none-match"), etag);
4866        }
4867    }
4868    else
4869    if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
4870        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4871            return HTTP_STATUS_FORBIDDEN;
4872
4873        int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
4874
4875        xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
4876        xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
4877
4878        xs *rss_title = xs_fmt("%s (@%s@%s)",
4879            xs_dict_get(snac.config, "name"),
4880            snac.uid,
4881            xs_dict_get(srv_config, "host"));
4882        xs *rss_link = xs_fmt("%s.rss", snac.actor);
4883
4884        *body   = rss_from_timeline(&snac, elems, rss_title, rss_link, bio);
4885        *b_size = strlen(*body);
4886        *ctype  = "application/rss+xml; charset=utf-8";
4887        status  = HTTP_STATUS_OK;
4888
4889        snac_debug(&snac, 1, xs_fmt("serving RSS"));
4890    }
4891    else
4892    if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
4893        xs *proxy_prefix = NULL;
4894
4895        if (xs_startswith(p_path, "x/")) {
4896            /* proxy usage authorized by http basic auth */
4897            if (login(&snac, req))
4898                proxy_prefix = xs_str_new("x/");
4899            else {
4900                *body  = xs_dup(uid);
4901                status = HTTP_STATUS_UNAUTHORIZED;
4902            }
4903        }
4904        else {
4905            /* proxy usage authorized by proxy_token */
4906            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
4907            xs *tk = xs_md5_hex(tks, strlen(tks));
4908            xs *p = xs_fmt("y/%s/", tk);
4909
4910            if (xs_startswith(p_path, p))
4911                proxy_prefix = xs_dup(p);
4912        }
4913
4914        if (proxy_prefix) {
4915            /* pick the raw path (including optional ? arguments) */
4916            const char *raw_path = xs_dict_get(req, "raw_path");
4917
4918            /* skip to where the proxy/ string starts */
4919            raw_path += xs_str_in(raw_path, proxy_prefix);
4920
4921            xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
4922            xs *hdrs = xs_dict_new();
4923
4924            hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
4925
4926            const char *ims = xs_dict_get(req, "if-modified-since");
4927            const char *inm = xs_dict_get(req, "if-none-match");
4928
4929            if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
4930            if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
4931
4932            xs *rsp = xs_http_request("GET", url, hdrs,
4933                        NULL, 0, &status, body, b_size, 0);
4934
4935            if (valid_status(status)) {
4936                const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
4937                const char *lm = xs_dict_get(rsp, "last-modified");
4938                const char *et = xs_dict_get(rsp, "etag");
4939
4940                if (lm) *last_modified = xs_dup(lm);
4941                if (et) *etag = xs_dup(et);
4942
4943                /* find the content-type in the static mime types,
4944                   and return that value instead of ct, which will
4945                   be destroyed when out of scope */
4946                for (int n = 0; xs_mime_types[n]; n += 2) {
4947                    if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
4948                        *ctype = (char *)xs_mime_types[n + 1];
4949                        break;
4950                    }
4951                }
4952            }
4953
4954            snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
4955        }
4956    }
4957    else
4958    if (strcmp(p_path, "share") == 0) { /** direct post **/
4959        if (!login(&snac, req)) {
4960            *body  = xs_dup(uid);
4961            status = HTTP_STATUS_UNAUTHORIZED;
4962        }
4963        else {
4964            const char *b64 = xs_dict_get(q_vars, "content");
4965            int sz;
4966            xs *content = xs_base64_dec(b64, &sz);
4967            xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL);
4968            xs *c_msg = msg_create(&snac, msg);
4969
4970            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
4971
4972            enqueue_message(&snac, c_msg);
4973
4974            snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
4975
4976            *body   = xs_fmt("%s/admin", snac.actor);
4977            *b_size = strlen(*body);
4978            status  = HTTP_STATUS_SEE_OTHER;
4979        }
4980    }
4981    else
4982    if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
4983        if (!login(&snac, req)) {
4984            *body  = xs_dup(uid);
4985            status = HTTP_STATUS_UNAUTHORIZED;
4986        }
4987        else {
4988            status = HTTP_STATUS_NOT_FOUND;
4989
4990            const char *id     = xs_dict_get(q_vars, "id");
4991            const char *action = xs_dict_get(q_vars, "action");
4992
4993            if (xs_is_string(id) && xs_is_string(action)) {
4994                if (strcmp(action, "Follow") == 0) {
4995                    xs *msg = msg_follow(&snac, id);
4996
4997                    if (msg != NULL) {
4998                        const char *actor = xs_dict_get(msg, "object");
4999
5000                        following_add(&snac, actor, msg);
5001
5002                        enqueue_output_by_actor(&snac, msg, actor, 0);
5003
5004                        status = HTTP_STATUS_SEE_OTHER;
5005                    }
5006                }
5007                else
5008                if (xs_match(action, "Like|Boost|Announce")) {
5009                    /* bring the post */
5010                    xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
5011
5012                    if (msg != NULL) {
5013                        timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
5014                        enqueue_message(&snac, msg);
5015
5016                        status = HTTP_STATUS_SEE_OTHER;
5017                    }
5018                }
5019            }
5020
5021            if (status == HTTP_STATUS_SEE_OTHER) {
5022                *body   = xs_fmt("%s/admin", snac.actor);
5023                *b_size = strlen(*body);
5024            }
5025        }
5026    }
5027    else
5028        status = HTTP_STATUS_NOT_FOUND;
5029
5030    user_free(&snac);
5031
5032    if (valid_status(status) && *ctype == NULL) {
5033        *ctype = "text/html; charset=utf-8";
5034    }
5035
5036    return status;
5037}
5038
5039
5040int html_post_handler(const xs_dict *req, const char *q_path,
5041                      char *payload, int p_size,
5042                      char **body, int *b_size, char **ctype)
5043{
5044    (void)p_size;
5045    (void)ctype;
5046
5047    int status = 0;
5048    const snac *user = NULL;
5049    snac snac;
5050    const char *uid;
5051    const char *p_path;
5052    const xs_dict *p_vars;
5053
5054    xs *l = xs_split_n(q_path, "/", 2);
5055
5056    uid = xs_list_get(l, 1);
5057    if (!uid || !user_open(&snac, uid)) {
5058        /* invalid user */
5059        srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
5060        return HTTP_STATUS_NOT_FOUND;
5061    }
5062
5063    p_path = xs_list_get(l, 2);
5064
5065    /* all posts must be authenticated */
5066    if (!login(&snac, req)) {
5067        user_free(&snac);
5068        *body  = xs_dup(uid);
5069        return HTTP_STATUS_UNAUTHORIZED;
5070    }
5071
5072    user = &snac; /* for L() */
5073    set_user_lang(&snac);
5074
5075    p_vars = xs_dict_get(req, "p_vars");
5076
5077    if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
5078        snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
5079
5080        /* post note */
5081        const char *content      = xs_dict_get(p_vars, "content");
5082        const char *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
5083        const char *to           = xs_dict_get(p_vars, "to");
5084        const char *sensitive    = xs_dict_get(p_vars, "sensitive");
5085        const char *summary      = xs_dict_get(p_vars, "summary");
5086        const char *edit_id      = xs_dict_get(p_vars, "edit_id");
5087        const char *post_date    = xs_dict_get_def(p_vars, "post_date", "");
5088        const char *post_time    = xs_dict_get_def(p_vars, "post_time", "");
5089        const char *post_lang    = xs_dict_get(p_vars, "post_lang");
5090        const char *visibility   = xs_dict_get(p_vars, "visibility");
5091        int scope = SCOPE_PUBLIC;  /* default to public */
5092        if (!xs_is_null(visibility)) {
5093            if (strcmp(visibility, "unlisted") == 0)
5094                scope = SCOPE_UNLISTED;
5095            else
5096            if (strcmp(visibility, "followers") == 0)
5097                scope = SCOPE_FOLLOWERS;
5098            else
5099            if (strcmp(visibility, "mentioned") == 0)
5100                scope = SCOPE_MENTIONED;
5101        }
5102        int store_as_draft   = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
5103        xs *attach_list      = xs_list_new();
5104
5105        /* iterate the attachments */
5106        int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
5107
5108        for (int att_n = 0; att_n < max_attachments; att_n++) {
5109            xs *url_lbl = xs_fmt("attach_url_%d", att_n);
5110            xs *att_lbl = xs_fmt("attach_%d", att_n);
5111            xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
5112
5113            const char *attach_url     = xs_dict_get(p_vars, url_lbl);
5114            const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
5115            const char *alt_text       = xs_dict_get_def(p_vars, alt_lbl, "");
5116
5117            if (xs_is_string(attach_url) && *attach_url != '\0') {
5118                xs *l = xs_list_new();
5119
5120                l = xs_list_append(l, attach_url);
5121                l = xs_list_append(l, alt_text);
5122
5123                attach_list = xs_list_append(attach_list, l);
5124            }
5125            else
5126            if (xs_is_list(attach_file)) {
5127                const char *fn = xs_list_get(attach_file, 0);
5128
5129                if (xs_is_string(fn) && *fn != '\0') {
5130                    char rnd[32];
5131                    xs_rnd_buf(rnd, sizeof(rnd));
5132
5133                    const char *ext = strrchr(fn, '.');
5134                    xs *hash  = xs_md5_hex(rnd, sizeof(rnd));
5135                    xs *id    = xs_fmt("post-%s%s", hash, ext ? ext : "");
5136                    xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
5137                    int fo    = xs_number_get(xs_list_get(attach_file, 1));
5138                    int fs    = xs_number_get(xs_list_get(attach_file, 2));
5139
5140                    /* store */
5141                    static_put(&snac, id, payload + fo, fs);
5142
5143                    xs *l = xs_list_new();
5144
5145                    l = xs_list_append(l, url);
5146                    l = xs_list_append(l, alt_text);
5147
5148                    attach_list = xs_list_append(attach_list, l);
5149                }
5150            }
5151        }
5152
5153        if (content != NULL) {
5154            xs *msg       = NULL;
5155            xs *c_msg     = NULL;
5156            xs *content_2 = xs_replace(content, "\r", "");
5157            xs *poll_opts = NULL;
5158
5159            /* is there a valid set of poll options? */
5160            const char *v = xs_dict_get(p_vars, "poll_options");
5161            if (!xs_is_null(v) && *v) {
5162                xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
5163
5164                poll_opts = xs_split(v2, "\n");
5165            }
5166
5167            if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
5168                /* get the rest of poll configuration */
5169                const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
5170                const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
5171                int multiple = 0;
5172
5173                int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
5174
5175                if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
5176                    multiple = 1;
5177
5178                msg = msg_question(&snac, content_2, attach_list,
5179                                   poll_opts, multiple, end_secs);
5180
5181                enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
5182            }
5183            else
5184                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
5185
5186            if (sensitive != NULL) {
5187                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
5188                msg = xs_dict_set(msg, "summary",   xs_is_null(summary) ? "..." : summary);
5189            }
5190
5191            if (xs_is_string(post_date) && *post_date) {
5192                xs *post_pubdate = xs_fmt("%sT%s", post_date,
5193                    xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
5194
5195                time_t t = xs_parse_iso_date(post_pubdate, 0);
5196
5197                if (t != 0) {
5198                    t -= xs_tz_offset(snac.tz);
5199
5200                    xs *iso_date = xs_str_iso_date(t);
5201                    msg = xs_dict_set(msg, "published", iso_date);
5202
5203                    snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
5204                }
5205                else
5206                    snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
5207            }
5208
5209            /* is the published date from the future? */
5210            int future_post = 0;
5211            xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
5212
5213            if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
5214                future_post = 1;
5215
5216            if (xs_is_null(edit_id)) {
5217                /* new message */
5218                const char *id = xs_dict_get(msg, "id");
5219
5220                if (store_as_draft) {
5221                    draft_add(&snac, id, msg);
5222                }
5223                else
5224                if (future_post) {
5225                    schedule_add(&snac, id, msg);
5226                }
5227                else {
5228                    c_msg = msg_create(&snac, msg);
5229                    timeline_add(&snac, id, msg);
5230                }
5231            }
5232            else {
5233                /* an edition of a previous message */
5234                xs *p_msg = NULL;
5235
5236                if (valid_status(object_get(edit_id, &p_msg))) {
5237                    /* copy relevant fields from previous version */
5238                    char *fields[] = { "id", "context", "url",
5239                                       "to", "inReplyTo", NULL };
5240                    int n;
5241
5242                    for (n = 0; fields[n]; n++) {
5243                        const char *v = xs_dict_get(p_msg, fields[n]);
5244                        msg = xs_dict_set(msg, fields[n], v);
5245                    }
5246
5247                    if (store_as_draft) {
5248                        draft_add(&snac, edit_id, msg);
5249                    }
5250                    else
5251                    if (is_draft(&snac, edit_id)) {
5252                        /* message was previously a draft; it's a create activity */
5253
5254                        /* if the date is from the past, overwrite it with right_now */
5255                        if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
5256                            snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
5257                            msg = xs_dict_set(msg, "published", right_now);
5258                        }
5259
5260                        /* overwrite object */
5261                        object_add_ow(edit_id, msg);
5262
5263                        if (future_post) {
5264                            schedule_add(&snac, edit_id, msg);
5265                        }
5266                        else {
5267                            c_msg = msg_create(&snac, msg);
5268                            timeline_add(&snac, edit_id, msg);
5269                        }
5270
5271                        draft_del(&snac, edit_id);
5272                    }
5273                    else
5274                    if (is_scheduled(&snac, edit_id)) {
5275                        /* editing an scheduled post; just update it */
5276                        schedule_add(&snac, edit_id, msg);
5277                    }
5278                    else {
5279                        /* ignore the (possibly changed) published date */
5280                        msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
5281
5282                        /* set the updated field */
5283                        xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
5284                        msg = xs_dict_set(msg, "updated", updated);
5285
5286                        /* overwrite object, not updating the indexes */
5287                        object_add_ow(edit_id, msg);
5288
5289                        /* index tags */
5290                        tag_index(edit_id, msg);
5291
5292                        /* update message */
5293                        c_msg = msg_update(&snac, msg);
5294                    }
5295                }
5296                else
5297                    snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
5298            }
5299
5300            if (c_msg != NULL) {
5301                enqueue_message(&snac, c_msg);
5302                enqueue_webmention(msg);
5303            }
5304
5305            history_del(&snac, "timeline.html_");
5306        }
5307
5308        status = HTTP_STATUS_SEE_OTHER;
5309    }
5310    else
5311    if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
5312        /* action on an entry */
5313        const char *id     = xs_dict_get(p_vars, "id");
5314        const char *actor  = xs_dict_get(p_vars, "actor");
5315        const char *action = xs_dict_get(p_vars, "action");
5316        const char *group  = xs_dict_get(p_vars, "group");
5317
5318        if (action == NULL)
5319            return HTTP_STATUS_NOT_FOUND;
5320
5321        snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
5322
5323        status = HTTP_STATUS_SEE_OTHER;
5324
5325        if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
5326            const char *eid = xs_dict_get(p_vars, "eid");
5327
5328            if (eid != NULL) {
5329                xs *n_msg = msg_emoji_unreact(&snac, id, eid);
5330
5331                if (n_msg != NULL)
5332                    enqueue_message(&snac, n_msg);
5333            }
5334        }
5335        else
5336        if (strcmp(action, L("EmojiReact")) == 0) { /** **/
5337            xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
5338
5339            eid = xs_strip_chars_i(eid, ":");
5340
5341            const xs_dict *ret = msg_emoji_init(&snac, id, eid);
5342            /* fails if either invalid or already reacted */
5343            if (!ret)
5344                ret = msg_emoji_unreact(&snac, id, eid);
5345            if (!ret)
5346                status = HTTP_STATUS_NOT_FOUND;
5347        }
5348        else
5349        if (strcmp(action, L("Like")) == 0) { /** **/
5350            xs *msg = msg_admiration(&snac, id, "Like");
5351
5352            if (msg != NULL) {
5353                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
5354                enqueue_message(&snac, msg);
5355            }
5356        }
5357        else
5358        if (strcmp(action, L("Boost")) == 0) { /** **/
5359            xs *msg = msg_admiration(&snac, id, "Announce");
5360
5361            if (msg != NULL) {
5362                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
5363                enqueue_message(&snac, msg);
5364            }
5365        }
5366        else
5367        if (strcmp(action, L("Unlike")) == 0) { /** **/
5368            xs *msg = msg_repulsion(&snac, id, "Like");
5369
5370            if (msg != NULL) {
5371                enqueue_message(&snac, msg);
5372            }
5373        }
5374        else
5375        if (strcmp(action, L("Unboost")) == 0) { /** **/
5376            xs *msg = msg_repulsion(&snac, id, "Announce");
5377
5378            if (msg != NULL) {
5379                enqueue_message(&snac, msg);
5380            }
5381        }
5382        else
5383        if (strcmp(action, L("MUTE")) == 0) { /** **/
5384            mute(&snac, actor);
5385        }
5386        else
5387        if (strcmp(action, L("Unmute")) == 0) { /** **/
5388            unmute(&snac, actor);
5389        }
5390        else
5391        if (strcmp(action, L("Hide")) == 0) { /** **/
5392            if (is_draft(&snac, id))
5393                draft_del(&snac, id);
5394            else
5395            if (is_scheduled(&snac, id))
5396                schedule_del(&snac, id);
5397            else
5398                hide(&snac, id);
5399        }
5400        else
5401        if (strcmp(action, L("Limit")) == 0) { /** **/
5402            limit(&snac, actor);
5403        }
5404        else
5405        if (strcmp(action, L("Unlimit")) == 0) { /** **/
5406            unlimit(&snac, actor);
5407        }
5408        else
5409        if (strcmp(action, L("Follow")) == 0) { /** **/
5410            xs *msg = msg_follow(&snac, actor);
5411
5412            if (msg != NULL) {
5413                /* reload the actor from the message, in may be different */
5414                actor = xs_dict_get(msg, "object");
5415
5416                following_add(&snac, actor, msg);
5417
5418                enqueue_output_by_actor(&snac, msg, actor, 0);
5419            }
5420        }
5421        else
5422        if (strcmp(action, L("Unfollow")) == 0) { /** **/
5423            /* get the following object */
5424            xs *object = NULL;
5425
5426            if (valid_status(following_get(&snac, actor, &object))) {
5427                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5428
5429                following_del(&snac, actor);
5430
5431                enqueue_output_by_actor(&snac, msg, actor, 0);
5432
5433                snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
5434            }
5435            else
5436                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5437        }
5438        else
5439        if (strcmp(action, L("Follow Group")) == 0) { /** **/
5440            xs *msg = msg_follow(&snac, group);
5441
5442            if (msg != NULL) {
5443                /* reload the group from the message, in may be different */
5444                group = xs_dict_get(msg, "object");
5445
5446                following_add(&snac, group, msg);
5447
5448                enqueue_output_by_actor(&snac, msg, group, 0);
5449            }
5450        }
5451        else
5452        if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
5453            /* get the following object */
5454            xs *object = NULL;
5455
5456            if (valid_status(following_get(&snac, group, &object))) {
5457                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5458
5459                following_del(&snac, group);
5460
5461                enqueue_output_by_actor(&snac, msg, group, 0);
5462
5463                snac_log(&snac, xs_fmt("unfollowed group %s", group));
5464            }
5465            else
5466                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5467        }
5468        else
5469        if (strcmp(action, L("Delete")) == 0) { /** **/
5470            const char *actor_form = xs_dict_get(p_vars, "actor-form");
5471            if (actor_form != NULL) {
5472                /* delete follower */
5473                if (valid_status(follower_del(&snac, actor)))
5474                    snac_log(&snac, xs_fmt("deleted follower %s", actor));
5475                else
5476                    snac_log(&snac, xs_fmt("error deleting follower %s", actor));
5477            }
5478            else {
5479                /* delete an entry */
5480                if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
5481                    /* it's a post by us: generate a delete */
5482                    xs *msg = msg_delete(&snac, id);
5483
5484                    enqueue_message(&snac, msg);
5485
5486                    snac_log(&snac, xs_fmt("posted tombstone for %s", id));
5487                }
5488
5489                timeline_del(&snac, id);
5490
5491                draft_del(&snac, id);
5492
5493                schedule_del(&snac, id);
5494
5495                snac_log(&snac, xs_fmt("deleted entry %s", id));
5496            }
5497        }
5498        else
5499        if (strcmp(action, L("Pin")) == 0) { /** **/
5500            pin(&snac, id);
5501            timeline_touch(&snac);
5502        }
5503        else
5504        if (strcmp(action, L("Unpin")) == 0) { /** **/
5505            unpin(&snac, id);
5506            timeline_touch(&snac);
5507        }
5508        else
5509        if (strcmp(action, L("Bookmark")) == 0) { /** **/
5510            bookmark(&snac, id);
5511            timeline_touch(&snac);
5512        }
5513        else
5514        if (strcmp(action, L("Unbookmark")) == 0) { /** **/
5515            unbookmark(&snac, id);
5516            timeline_touch(&snac);
5517        }
5518        else
5519        if (strcmp(action, L("Approve")) == 0) { /** **/
5520            xs *fwreq = pending_get(&snac, actor);
5521
5522            if (fwreq != NULL) {
5523                xs *reply = msg_accept(&snac, fwreq, actor);
5524
5525                enqueue_message(&snac, reply);
5526
5527                if (xs_is_null(xs_dict_get(fwreq, "published"))) {
5528                    /* add a date if it doesn't include one (Mastodon) */
5529                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
5530                    fwreq = xs_dict_set(fwreq, "published", date);
5531                }
5532
5533                timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
5534
5535                follower_add(&snac, actor);
5536
5537                pending_del(&snac, actor);
5538
5539                snac_log(&snac, xs_fmt("new follower %s", actor));
5540            }
5541        }
5542        else
5543        if (strcmp(action, L("Discard")) == 0) { /** **/
5544            pending_del(&snac, actor);
5545        }
5546        else
5547            status = HTTP_STATUS_NOT_FOUND;
5548
5549        /* delete the cached timeline */
5550        if (status == HTTP_STATUS_SEE_OTHER)
5551            history_del(&snac, "timeline.html_");
5552    }
5553    else
5554    if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
5555        /* change of user data */
5556        const char *v;
5557        const char *p1, *p2;
5558
5559        if ((v = xs_dict_get(p_vars, "name")) != NULL)
5560            snac.config = xs_dict_set(snac.config, "name", v);
5561        if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
5562            snac.config = xs_dict_set(snac.config, "avatar", v);
5563        if ((v = xs_dict_get(p_vars, "bio")) != NULL)
5564            snac.config = xs_dict_set(snac.config, "bio", v);
5565        if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
5566            strcmp(v, "on") == 0) {
5567            snac.config = xs_dict_set(snac.config, "cw", "open");
5568        } else { /* if the checkbox is not set, the parameter is missing */
5569            snac.config = xs_dict_set(snac.config, "cw", "");
5570        }
5571        if ((v = xs_dict_get(p_vars, "email")) != NULL)
5572            snac.config = xs_dict_set(snac.config, "email", v);
5573        if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
5574            snac.config = xs_dict_set(snac.config, "telegram_bot", v);
5575        if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
5576            snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
5577        if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
5578            snac.config = xs_dict_set(snac.config, "ntfy_server", v);
5579        if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
5580            snac.config = xs_dict_set(snac.config, "ntfy_token", v);
5581        if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
5582            xs *days    = xs_number_new(atof(v));
5583            snac.config = xs_dict_set(snac.config, "purge_days", days);
5584        }
5585        if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
5586            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
5587        else
5588            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
5589        if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
5590            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
5591        else
5592            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
5593        if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
5594            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
5595        else
5596            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
5597        if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
5598            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
5599        else
5600            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
5601        if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
5602            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
5603        else
5604            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
5605        if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
5606            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
5607        else
5608            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
5609        if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
5610            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
5611        else
5612            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
5613        if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
5614            snac.config = xs_dict_set(snac.config, "lang", v);
5615        if ((v = xs_dict_get(p_vars, "tz")) != NULL)
5616            snac.config = xs_dict_set(snac.config, "tz", v);
5617        if ((v = xs_dict_get(p_vars, "post_langs")) != NULL)
5618            snac.config = xs_dict_set(snac.config, "post_langs", v);
5619
5620        snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
5621        snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
5622
5623        snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", ""));
5624
5625        if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
5626            snac.config = xs_dict_set(snac.config, "metadata", v);
5627
5628        /* uploads */
5629        const char *uploads[] = { "avatar", "header", NULL };
5630        int n;
5631        for (n = 0; uploads[n]; n++) {
5632            xs *var_name = xs_fmt("%s_file", uploads[n]);
5633
5634            const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
5635            if (xs_type(uploaded_file) == XSTYPE_LIST) {
5636                const char *fn = xs_list_get(uploaded_file, 0);
5637
5638                if (fn && *fn) {
5639                    const char *mimetype = xs_mime_by_ext(fn);
5640
5641                    if (xs_startswith(mimetype, "image/")) {
5642                        const char *ext = strrchr(fn, '.');
5643                        xs *hash        = xs_md5_hex(fn, strlen(fn));
5644                        xs *id          = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
5645                        xs *url         = xs_fmt("%s/s/%s", snac.actor, id);
5646                        int fo          = xs_number_get(xs_list_get(uploaded_file, 1));
5647                        int fs          = xs_number_get(xs_list_get(uploaded_file, 2));
5648
5649                        /* store */
5650                        static_put(&snac, id, payload + fo, fs);
5651
5652                        snac.config = xs_dict_set(snac.config, uploads[n], url);
5653                    }
5654                }
5655            }
5656        }
5657
5658        /* delete images by removing url from user.json */
5659        for (n = 0; uploads[n]; n++) {
5660            xs *var_name = xs_fmt("%s_delete", uploads[n]);
5661            const char *delete_var = xs_dict_get(p_vars, var_name);
5662
5663            if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
5664                    snac.config = xs_dict_set(snac.config, uploads[n], "");
5665            }
5666        }
5667
5668        /* password change? */
5669        if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
5670            (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
5671            *p1 && strcmp(p1, p2) == 0) {
5672            xs *pw = hash_password(snac.uid, p1, NULL);
5673            snac.config = xs_dict_set(snac.config, "passwd", pw);
5674        }
5675
5676        user_persist(&snac, 1);
5677
5678        status = HTTP_STATUS_SEE_OTHER;
5679    }
5680    else
5681    if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
5682        notify_clear(&snac);
5683        timeline_touch(&snac);
5684
5685        status = HTTP_STATUS_SEE_OTHER;
5686    }
5687    else
5688    if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
5689        const char *irt   = xs_dict_get(p_vars, "irt");
5690        const char *opt   = xs_dict_get(p_vars, "question");
5691        const char *actor = xs_dict_get(p_vars, "actor");
5692
5693        xs *ls = NULL;
5694
5695        /* multiple choices? */
5696        if (xs_type(opt) == XSTYPE_LIST)
5697            ls = xs_dup(opt);
5698        else
5699        if (xs_type(opt) == XSTYPE_STRING) {
5700            ls = xs_list_new();
5701            ls = xs_list_append(ls, opt);
5702        }
5703
5704        const xs_str *v;
5705        int c = 0;
5706
5707        while (xs_list_next(ls, &v, &c)) {
5708            xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL);
5709
5710            /* set the option */
5711            msg = xs_dict_append(msg, "name", v);
5712
5713            /* delete the content */
5714            msg = xs_dict_del(msg, "content");
5715
5716            xs *c_msg = msg_create(&snac, msg);
5717
5718            enqueue_message(&snac, c_msg);
5719
5720            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
5721        }
5722
5723        if (ls != NULL) {
5724            /* get the poll object */
5725            xs *poll = NULL;
5726
5727            if (valid_status(object_get(irt, &poll))) {
5728                const char *date = xs_dict_get(poll, "endTime");
5729                if (xs_is_null(date))
5730                    date = xs_dict_get(poll, "closed");
5731
5732                if (!xs_is_null(date)) {
5733                    time_t t = xs_parse_iso_date(date, 0) - time(NULL);
5734
5735                    /* request the poll when it's closed;
5736                       Pleroma does not send and update when the poll closes */
5737                    enqueue_object_request(&snac, irt, t + 2);
5738                }
5739            }
5740        }
5741
5742        status = HTTP_STATUS_SEE_OTHER;
5743    }
5744    else
5745    if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
5746        const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
5747
5748        if (xs_is_string(followed_hashtags)) {
5749            xs *new_hashtags = xs_list_new();
5750            xs *l = xs_split(followed_hashtags, "\n");
5751            const char *v;
5752
5753            xs_list_foreach(l, v) {
5754                xs *s1 = xs_strip_i(xs_dup(v));
5755                s1 = xs_replace_i(s1, " ", "");
5756
5757                if (*s1 == '\0')
5758                    continue;
5759
5760                xs *s2 = NULL;
5761
5762                if (xs_startswith(s1, "https:/"))
5763                    s2 = xs_dup(s1);
5764                else {
5765                    s2 = xs_utf8_to_lower(s1);
5766
5767                    if (*s2 != '#')
5768                        s2 = xs_str_prepend_i(s2, "#");
5769                }
5770
5771                new_hashtags = xs_list_append(new_hashtags, s2);
5772            }
5773
5774            snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
5775            user_persist(&snac, 0);
5776        }
5777
5778        status = HTTP_STATUS_SEE_OTHER;
5779    }
5780    else
5781    if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/
5782        const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags");
5783
5784        if (xs_is_string(hashtags)) {
5785            xs *new_hashtags = xs_list_new();
5786            xs *l = xs_split(hashtags, "\n");
5787            const char *v;
5788
5789            xs_list_foreach(l, v) {
5790                xs *s1 = xs_strip_i(xs_dup(v));
5791                s1 = xs_replace_i(s1, " ", "");
5792
5793                if (*s1 == '\0')
5794                    continue;
5795
5796                xs *s2 = xs_utf8_to_lower(s1);
5797                if (*s2 != '#')
5798                    s2 = xs_str_prepend_i(s2, "#");
5799
5800                new_hashtags = xs_list_append(new_hashtags, s2);
5801            }
5802
5803            snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags);
5804            user_persist(&snac, 0);
5805        }
5806
5807        status = HTTP_STATUS_SEE_OTHER;
5808    }
5809
5810    if (status == HTTP_STATUS_SEE_OTHER) {
5811        const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
5812
5813        if (xs_is_string(hard_redir))
5814            *body = xs_dup(hard_redir);
5815        else {
5816            const char *redir = xs_dict_get(p_vars, "redir");
5817
5818            if (xs_is_null(redir))
5819                redir = "top";
5820
5821            *body = xs_fmt("%s/admin#%s", snac.actor, redir);
5822        }
5823
5824        *b_size = strlen(*body);
5825    }
5826
5827    user_free(&snac);
5828
5829    return status;
5830}