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