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