2.68 html.c
   1/* snac - A simple, minimalistic ActivityPub instance */
   2/* copyright (c) 2022 - 2024 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
  16#include "snac.h"
  17
  18int login(snac *snac, const xs_dict *headers)
  19/* tries a login */
  20{
  21    int logged_in = 0;
  22    const char *auth = xs_dict_get(headers, "authorization");
  23
  24    if (auth && xs_startswith(auth, "Basic ")) {
  25        int sz;
  26        xs *s1 = xs_crop_i(xs_dup(auth), 6, 0);
  27        xs *s2 = xs_base64_dec(s1, &sz);
  28
  29        xs *l1 = xs_split_n(s2, ":", 1);
  30
  31        if (xs_list_len(l1) == 2) {
  32            const char *user = xs_list_get(l1, 0);
  33            const char *pwd  = xs_list_get(l1, 1);
  34            const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
  35                                     xs_dict_get(headers, "x-forwarded-for"));
  36
  37            if (badlogin_check(user, addr)) {
  38                logged_in = check_password(user, pwd,
  39                    xs_dict_get(snac->config, "passwd"));
  40
  41                if (!logged_in)
  42                    badlogin_inc(user, addr);
  43            }
  44        }
  45    }
  46
  47    if (logged_in)
  48        lastlog_write(snac, "web");
  49
  50    return logged_in;
  51}
  52
  53
  54xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
  55/* replaces all the :shortnames: with the emojis in tag */
  56{
  57    if (!xs_is_null(tag)) {
  58        xs *tag_list = NULL;
  59        if (xs_type(tag) == XSTYPE_DICT) {
  60            /* not a list */
  61            tag_list = xs_list_new();
  62            tag_list = xs_list_append(tag_list, tag);
  63        } else {
  64            /* is a list */
  65            tag_list = xs_dup(tag);
  66        }
  67
  68        xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
  69
  70        const char *v;
  71        int c = 0;
  72
  73        while (xs_list_next(tag_list, &v, &c)) {
  74            const char *t = xs_dict_get(v, "type");
  75
  76            if (t && strcmp(t, "Emoji") == 0) {
  77                const char *n = xs_dict_get(v, "name");
  78                const char *i = xs_dict_get(v, "icon");
  79
  80                if (n && i) {
  81                    const char *u = xs_dict_get(i, "url");
  82                    xs *url = make_url(u, proxy, 0);
  83
  84                    xs_html *img = xs_html_sctag("img",
  85                        xs_html_attr("loading", "lazy"),
  86                        xs_html_attr("src", url),
  87                        xs_html_attr("style", style));
  88
  89                    xs *s1 = xs_html_render(img);
  90                    s = xs_replace_i(s, n, s1);
  91                }
  92            }
  93        }
  94    }
  95
  96    return s;
  97}
  98
  99
 100xs_str *actor_name(xs_dict *actor, const char *proxy)
 101/* gets the actor name */
 102{
 103    const char *v;
 104
 105    if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
 106        if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
 107            v = "anonymous";
 108        }
 109    }
 110
 111    return replace_shortnames(xs_html_encode(v), xs_dict_get(actor, "tag"), 1, proxy);
 112}
 113
 114
 115xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
 116                        const char *udate, const char *url, int priv, int in_people, const char *proxy)
 117{
 118    xs_html *actor_icon = xs_html_tag("p", NULL);
 119
 120    xs *avatar = NULL;
 121    const char *v;
 122    int fwing = 0;
 123    int fwer = 0;
 124
 125    xs *name = actor_name(actor, proxy);
 126
 127    /* get the avatar */
 128    if ((v = xs_dict_get(actor, "icon")) != NULL) {
 129        /* if it's a list (Peertube), get the first one */
 130        if (xs_type(v) == XSTYPE_LIST)
 131            v = xs_list_get(v, 0);
 132
 133        if ((v = xs_dict_get(v, "url")) != NULL)
 134            avatar = make_url(v, proxy, 0);
 135    }
 136
 137    if (avatar == NULL)
 138        avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
 139
 140    const char *actor_id = xs_dict_get(actor, "id");
 141    xs *href = NULL;
 142
 143    if (user) {
 144        fwer = follower_check(user, actor_id);
 145        fwing = following_check(user, actor_id);
 146    }
 147
 148    if (user && !in_people) {
 149        /* if this actor is a follower or being followed, create an
 150           anchored link to the people page instead of the actor url */
 151        if (fwer || fwing) {
 152            xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
 153            href = xs_fmt("%s/people#%s", user->actor, md5);
 154        }
 155    }
 156
 157    if (href == NULL)
 158        href = xs_dup(actor_id);
 159
 160    xs_html_add(actor_icon,
 161        xs_html_sctag("img",
 162            xs_html_attr("loading", "lazy"),
 163            xs_html_attr("class",   "snac-avatar"),
 164            xs_html_attr("src",     avatar),
 165            xs_html_attr("alt",     "")),
 166        xs_html_tag("a",
 167            xs_html_attr("href",    href),
 168            xs_html_attr("class",   "p-author h-card snac-author"),
 169            xs_html_raw(name))); /* name is already html-escaped */
 170
 171    if (!xs_is_null(url)) {
 172        xs *md5 = xs_md5_hex(url, strlen(url));
 173
 174        xs_html_add(actor_icon,
 175            xs_html_text(" "),
 176            xs_html_tag("a",
 177                xs_html_attr("href", (char *)url),
 178                xs_html_attr("title", md5),
 179                xs_html_text("ยป")));
 180    }
 181
 182    if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
 183        xs_html_add(actor_icon,
 184            xs_html_text(" "),
 185            xs_html_tag("span",
 186                xs_html_attr("title", "bot"),
 187                xs_html_raw("🤖")));
 188    }
 189
 190    if (fwing && fwer) {
 191        xs_html_add(actor_icon,
 192            xs_html_text(" "),
 193            xs_html_tag("span",
 194                xs_html_attr("title", "mutual relation"),
 195                xs_html_raw("🤝")));
 196    }
 197
 198    if (priv) {
 199        xs_html_add(actor_icon,
 200            xs_html_text(" "),
 201            xs_html_tag("span",
 202                xs_html_attr("title", "private"),
 203                xs_html_raw("🔒")));
 204    }
 205
 206    if (xs_is_null(date)) {
 207        xs_html_add(actor_icon,
 208            xs_html_raw(" "));
 209    }
 210    else {
 211        xs *date_label = xs_crop_i(xs_dup(date), 0, 10);
 212        xs *date_title = xs_dup(date);
 213
 214        if (!xs_is_null(udate)) {
 215            xs *sd = xs_crop_i(xs_dup(udate), 0, 10);
 216
 217            date_label = xs_str_cat(date_label, " / ", sd);
 218
 219            date_title = xs_str_cat(date_title, " / ", udate);
 220        }
 221
 222        xs_html_add(actor_icon,
 223            xs_html_text(" "),
 224            xs_html_tag("time",
 225                xs_html_attr("class", "dt-published snac-pubdate"),
 226                xs_html_attr("title", date_title),
 227                xs_html_text(date_label)));
 228    }
 229
 230    {
 231        const char *username, *id;
 232
 233        if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
 234            /* This should never be reached */
 235            username = "anonymous";
 236        }
 237
 238        if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') {
 239            /* This should never be reached */
 240            id = "https://social.example.org/anonymous";
 241        }
 242
 243        /* "LIKE AN ANIMAL" */
 244        xs *domain = xs_split(id, "/");
 245        xs *user   = xs_fmt("@%s@%s", username, xs_list_get(domain, 2));
 246
 247        xs_html_add(actor_icon,
 248            xs_html_sctag("br", NULL),
 249            xs_html_tag("a",
 250                xs_html_attr("href",  xs_dict_get(actor, "id")),
 251                xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
 252                xs_html_text(user)));
 253    }
 254
 255    return actor_icon;
 256}
 257
 258
 259xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy)
 260{
 261    xs *actor = NULL;
 262    xs_html *actor_icon = NULL;
 263
 264    if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
 265        const char *date  = NULL;
 266        const char *udate = NULL;
 267        const char *url   = NULL;
 268        int priv    = 0;
 269        const char *type = xs_dict_get(msg, "type");
 270
 271        if (xs_match(type, POSTLIKE_OBJECT_TYPE))
 272            url = xs_dict_get(msg, "id");
 273
 274        priv = !is_msg_public(msg);
 275
 276        date  = xs_dict_get(msg, "published");
 277        udate = xs_dict_get(msg, "updated");
 278
 279        actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy);
 280    }
 281
 282    return actor_icon;
 283}
 284
 285
 286xs_html *html_note(snac *user, const char *summary,
 287                   const char *div_id, const char *form_id,
 288                   const char *ta_plh, const char *ta_content,
 289                   const char *edit_id, const char *actor_id,
 290                   const xs_val *cw_yn, const char *cw_text,
 291                   const xs_val *mnt_only, const char *redir,
 292                   const char *in_reply_to, int poll,
 293                   const char *att_file, const char *att_alt_text,
 294                   int is_draft)
 295/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
 296{
 297    xs *action = xs_fmt("%s/admin/note", user->actor);
 298
 299    xs_html *form;
 300
 301    xs_html *note = xs_html_tag("div",
 302        xs_html_tag("details",
 303            xs_html_tag("summary",
 304                xs_html_text(summary)),
 305                xs_html_tag("p", NULL),
 306                xs_html_tag("div",
 307                    xs_html_attr("class", "snac-note"),
 308                    xs_html_attr("id",    div_id),
 309                    form = xs_html_tag("form",
 310                        xs_html_attr("autocomplete", "off"),
 311                        xs_html_attr("method",       "post"),
 312                        xs_html_attr("action",       action),
 313                        xs_html_attr("enctype",      "multipart/form-data"),
 314                        xs_html_attr("id",           form_id),
 315                        xs_html_tag("textarea",
 316                            xs_html_attr("class",    "snac-textarea"),
 317                            xs_html_attr("name",     "content"),
 318                            xs_html_attr("rows",     "4"),
 319                            xs_html_attr("wrap",     "virtual"),
 320                            xs_html_attr("required", "required"),
 321                            xs_html_attr("placeholder", ta_plh),
 322                            xs_html_text(ta_content)),
 323                        xs_html_tag("p", NULL),
 324                        xs_html_text(L("Sensitive content: ")),
 325                        xs_html_sctag("input",
 326                            xs_html_attr("type", "checkbox"),
 327                            xs_html_attr("name", "sensitive"),
 328                            xs_html_attr(xs_type(cw_yn) == XSTYPE_TRUE ? "checked" : "", NULL)),
 329                        xs_html_sctag("input",
 330                            xs_html_attr("type", "text"),
 331                            xs_html_attr("name", "summary"),
 332                            xs_html_attr("placeholder", L("Sensitive content description")),
 333                            xs_html_attr("value", xs_is_null(cw_text) ? "" : cw_text))))));
 334
 335    if (actor_id)
 336        xs_html_add(form,
 337            xs_html_sctag("input",
 338                xs_html_attr("type",  "hidden"),
 339                xs_html_attr("name",  "to"),
 340                xs_html_attr("value", actor_id)));
 341    else {
 342        /* no actor_id; ask for mentioned_only */
 343        xs_html_add(form,
 344            xs_html_tag("p", NULL),
 345            xs_html_text(L("Only for mentioned people: ")),
 346            xs_html_sctag("input",
 347                xs_html_attr("type",  "checkbox"),
 348                xs_html_attr("name",  "mentioned_only"),
 349                xs_html_attr(xs_type(mnt_only) == XSTYPE_TRUE ? "checked" : "", NULL)));
 350    }
 351
 352    if (redir)
 353        xs_html_add(form,
 354            xs_html_sctag("input",
 355                xs_html_attr("type",  "hidden"),
 356                xs_html_attr("name",  "redir"),
 357                xs_html_attr("value", redir)));
 358
 359    if (in_reply_to)
 360        xs_html_add(form,
 361            xs_html_sctag("input",
 362                xs_html_attr("type",  "hidden"),
 363                xs_html_attr("name",  "in_reply_to"),
 364                xs_html_attr("value", in_reply_to)));
 365    else
 366        xs_html_add(form,
 367            xs_html_tag("p", NULL),
 368            xs_html_text(L("Reply to (URL): ")),
 369            xs_html_sctag("input",
 370                xs_html_attr("type",     "url"),
 371                xs_html_attr("name",     "in_reply_to"),
 372                xs_html_attr("placeholder", "Optional URL to reply to")));
 373
 374    xs_html_add(form,
 375        xs_html_tag("p", NULL),
 376        xs_html_tag("span",
 377            xs_html_attr("title", L("Don't send, but store as a draft")),
 378            xs_html_text(L("Draft:")),
 379            xs_html_sctag("input",
 380                xs_html_attr("type", "checkbox"),
 381                xs_html_attr("name", "is_draft"),
 382                xs_html_attr(is_draft ? "checked" : "", NULL))));
 383
 384    if (edit_id)
 385        xs_html_add(form,
 386            xs_html_sctag("input",
 387                xs_html_attr("type",  "hidden"),
 388                xs_html_attr("name",  "edit_id"),
 389                xs_html_attr("value", edit_id)));
 390
 391    /* attachment controls */
 392    xs_html *att;
 393
 394    xs_html_add(form,
 395        xs_html_tag("p", NULL),
 396        att = xs_html_tag("details",
 397            xs_html_tag("summary",
 398                xs_html_text(L("Attachment..."))),
 399            xs_html_tag("p", NULL)));
 400
 401    if (att_file && *att_file)
 402        xs_html_add(att,
 403            xs_html_text(L("File:")),
 404            xs_html_sctag("input",
 405                xs_html_attr("type", "text"),
 406                xs_html_attr("name", "attach_url"),
 407                xs_html_attr("title", L("Clear this field to delete the attachment")),
 408                xs_html_attr("value", att_file)));
 409    else
 410        xs_html_add(att,
 411            xs_html_sctag("input",
 412                xs_html_attr("type",    "file"),
 413                xs_html_attr("name",    "attach")));
 414
 415    xs_html_add(att,
 416        xs_html_text(" "),
 417        xs_html_sctag("input",
 418            xs_html_attr("type",    "text"),
 419            xs_html_attr("name",    "alt_text"),
 420            xs_html_attr("value",   att_alt_text),
 421            xs_html_attr("placeholder", L("Attachment description"))));
 422
 423    /* add poll controls */
 424    if (poll) {
 425        xs_html_add(form,
 426            xs_html_tag("p", NULL),
 427            xs_html_tag("details",
 428                xs_html_tag("summary",
 429                    xs_html_text(L("Poll..."))),
 430                xs_html_tag("p",
 431                    xs_html_text(L("Poll options (one per line, up to 8):")),
 432                    xs_html_sctag("br", NULL),
 433                    xs_html_tag("textarea",
 434                        xs_html_attr("class",    "snac-textarea"),
 435                        xs_html_attr("name",     "poll_options"),
 436                        xs_html_attr("rows",     "4"),
 437                        xs_html_attr("wrap",     "virtual"),
 438                        xs_html_attr("placeholder", "Option 1...\nOption 2...\nOption 3...\n..."))),
 439                xs_html_tag("select",
 440                    xs_html_attr("name",    "poll_multiple"),
 441                    xs_html_tag("option",
 442                        xs_html_attr("value", "off"),
 443                        xs_html_text(L("One choice"))),
 444                    xs_html_tag("option",
 445                        xs_html_attr("value", "on"),
 446                        xs_html_text(L("Multiple choices")))),
 447                xs_html_text(" "),
 448                xs_html_tag("select",
 449                    xs_html_attr("name",    "poll_end_secs"),
 450                    xs_html_tag("option",
 451                        xs_html_attr("value", "300"),
 452                        xs_html_text(L("End in 5 minutes"))),
 453                    xs_html_tag("option",
 454                        xs_html_attr("value", "3600"),
 455                        xs_html_attr("selected", NULL),
 456                        xs_html_text(L("End in 1 hour"))),
 457                    xs_html_tag("option",
 458                        xs_html_attr("value", "86400"),
 459                        xs_html_text(L("End in 1 day"))))));
 460    }
 461
 462    xs_html_add(form,
 463        xs_html_tag("p", NULL),
 464        xs_html_sctag("input",
 465            xs_html_attr("type",     "submit"),
 466            xs_html_attr("class",    "button"),
 467            xs_html_attr("value",    L("Post"))),
 468        xs_html_tag("p", NULL));
 469
 470    return note;
 471}
 472
 473
 474static xs_html *html_base_head(void)
 475{
 476    xs_html *head = xs_html_tag("head",
 477        xs_html_sctag("meta",
 478            xs_html_attr("name", "viewport"),
 479            xs_html_attr("content", "width=device-width, initial-scale=1")),
 480        xs_html_sctag("meta",
 481            xs_html_attr("name", "generator"),
 482            xs_html_attr("content", USER_AGENT)));
 483
 484    /* add server CSS and favicon */
 485    xs *f;
 486    f = xs_fmt("%s/favicon.ico", srv_baseurl);
 487    const xs_list *p = xs_dict_get(srv_config, "cssurls");
 488    const char *v;
 489    int c = 0;
 490
 491    while (xs_list_next(p, &v, &c)) {
 492        xs_html_add(head,
 493            xs_html_sctag("link",
 494                xs_html_attr("rel",  "stylesheet"),
 495                xs_html_attr("type", "text/css"),
 496                xs_html_attr("href", v)));
 497    }
 498
 499    xs_html_add(head,
 500        xs_html_sctag("link",
 501            xs_html_attr("rel",  "icon"),
 502            xs_html_attr("type", "image/x-icon"),
 503            xs_html_attr("href", f)));
 504
 505    return head;
 506}
 507
 508
 509xs_html *html_instance_head(void)
 510{
 511    xs_html *head = html_base_head();
 512
 513    {
 514        FILE *f;
 515        xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
 516
 517        if ((f = fopen(g_css_fn, "r")) != NULL) {
 518            xs *css = xs_readall(f);
 519            fclose(f);
 520
 521            xs_html_add(head,
 522                xs_html_tag("style",
 523                    xs_html_raw(css)));
 524        }
 525    }
 526
 527    const char *host  = xs_dict_get(srv_config, "host");
 528    const char *title = xs_dict_get(srv_config, "title");
 529
 530    xs_html_add(head,
 531        xs_html_tag("title",
 532            xs_html_text(title && *title ? title : host)));
 533
 534    return head;
 535}
 536
 537
 538static xs_html *html_instance_body(void)
 539{
 540    const char *host  = xs_dict_get(srv_config, "host");
 541    const char *sdesc = xs_dict_get(srv_config, "short_description");
 542    const char *email = xs_dict_get(srv_config, "admin_email");
 543    const char *acct  = xs_dict_get(srv_config, "admin_account");
 544
 545    xs *blurb = xs_replace(snac_blurb, "%host%", host);
 546
 547    xs_html *dl;
 548
 549    xs_html *body = xs_html_tag("body",
 550        xs_html_tag("div",
 551            xs_html_attr("class", "snac-instance-blurb"),
 552            xs_html_raw(blurb), /* pure html */
 553            dl = xs_html_tag("dl", NULL)));
 554
 555    if (sdesc && *sdesc) {
 556        xs_html_add(dl,
 557            xs_html_tag("di",
 558                xs_html_tag("dt",
 559                    xs_html_text(L("Site description"))),
 560                xs_html_tag("dd",
 561                    xs_html_text(sdesc))));
 562    }
 563    if (email && *email) {
 564        xs *mailto = xs_fmt("mailto:%s", email);
 565
 566        xs_html_add(dl,
 567            xs_html_tag("di",
 568                xs_html_tag("dt",
 569                    xs_html_text(L("Admin email"))),
 570                xs_html_tag("dd",
 571                    xs_html_tag("a",
 572                        xs_html_attr("href", mailto),
 573                        xs_html_text(email)))));
 574    }
 575    if (acct && *acct) {
 576        xs *url    = xs_fmt("%s/%s", srv_baseurl, acct);
 577        xs *handle = xs_fmt("@%s@%s", acct, host);
 578
 579        xs_html_add(dl,
 580            xs_html_tag("di",
 581                xs_html_tag("dt",
 582                    xs_html_text(L("Admin account"))),
 583                xs_html_tag("dd",
 584                    xs_html_tag("a",
 585                        xs_html_attr("href", url),
 586                        xs_html_text(handle)))));
 587    }
 588
 589    return body;
 590}
 591
 592
 593xs_html *html_user_head(snac *user, const char *desc, const char *url)
 594{
 595    xs_html *head = html_base_head();
 596
 597    /* add the user CSS */
 598    {
 599        xs *css = NULL;
 600        int size;
 601
 602        /* try to open the user css */
 603        if (!valid_status(static_get(user, "style.css", &css, &size, NULL, NULL))) {
 604            /* it's not there; try to open the server-wide css */
 605            FILE *f;
 606            xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
 607
 608            if ((f = fopen(g_css_fn, "r")) != NULL) {
 609                css = xs_readall(f);
 610                fclose(f);
 611            }
 612        }
 613
 614        if (css != NULL) {
 615            xs_html_add(head,
 616                xs_html_tag("style",
 617                    xs_html_raw(css)));
 618        }
 619    }
 620
 621    /* title */
 622    xs *title = xs_fmt("%s (@%s@%s)", xs_dict_get(user->config, "name"),
 623        user->uid, xs_dict_get(srv_config, "host"));
 624
 625    xs_html_add(head,
 626        xs_html_tag("title",
 627            xs_html_text(title)));
 628
 629    xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
 630
 631    if (avatar == NULL || *avatar == '\0') {
 632        xs_free(avatar);
 633        avatar = xs_fmt("%s/susie.png", srv_baseurl);
 634    }
 635
 636    /* create a description field */
 637    xs *s_desc = NULL;
 638    int n;
 639
 640    if (desc == NULL)
 641        s_desc = xs_dup(xs_dict_get(user->config, "bio"));
 642    else
 643        s_desc = xs_dup(desc);
 644
 645    /* show metrics in og:description? */
 646    if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
 647        xs *fwers = follower_list(user);
 648        xs *fwing = following_list(user);
 649
 650        xs *s1 = xs_fmt(L("%d following, %d followers ยท "),
 651            xs_list_len(fwing), xs_list_len(fwers));
 652
 653        s_desc = xs_str_prepend_i(s_desc, s1);
 654    }
 655
 656    /* shorten desc to a reasonable size */
 657    for (n = 0; s_desc[n]; n++) {
 658        if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
 659            break;
 660    }
 661
 662    s_desc[n] = '\0';
 663
 664    /* og properties */
 665    xs_html_add(head,
 666        xs_html_sctag("meta",
 667            xs_html_attr("property", "og:site_name"),
 668            xs_html_attr("content", xs_dict_get(srv_config, "host"))),
 669        xs_html_sctag("meta",
 670            xs_html_attr("property", "og:title"),
 671            xs_html_attr("content", title)),
 672        xs_html_sctag("meta",
 673            xs_html_attr("property", "og:description"),
 674            xs_html_attr("content", s_desc)),
 675        xs_html_sctag("meta",
 676            xs_html_attr("property", "og:image"),
 677            xs_html_attr("content", avatar)),
 678        xs_html_sctag("meta",
 679            xs_html_attr("property", "og:width"),
 680            xs_html_attr("content", "300")),
 681        xs_html_sctag("meta",
 682            xs_html_attr("property", "og:height"),
 683            xs_html_attr("content", "300")));
 684
 685    /* RSS link */
 686    xs *rss_url = xs_fmt("%s.rss", user->actor);
 687    xs_html_add(head,
 688        xs_html_sctag("link",
 689            xs_html_attr("rel", "alternate"),
 690            xs_html_attr("type", "application/rss+xml"),
 691            xs_html_attr("title", "RSS"),
 692            xs_html_attr("href", rss_url)));
 693
 694    /* ActivityPub alternate link (actor id) */
 695    xs_html_add(head,
 696        xs_html_sctag("link",
 697            xs_html_attr("rel", "alternate"),
 698            xs_html_attr("type", "application/activity+json"),
 699            xs_html_attr("href", url ? url : user->actor)));
 700
 701    return head;
 702}
 703
 704
 705static xs_html *html_user_body(snac *user, int read_only)
 706{
 707    const char *proxy = NULL;
 708
 709    if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
 710        proxy = user->actor;
 711
 712    xs_html *body = xs_html_tag("body", NULL);
 713
 714    /* top nav */
 715    xs_html *top_nav = xs_html_tag("nav",
 716        xs_html_attr("class", "snac-top-nav"));
 717
 718    xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
 719
 720    if (avatar == NULL || *avatar == '\0') {
 721        xs_free(avatar);
 722        avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
 723    }
 724
 725    xs_html_add(top_nav,
 726        xs_html_sctag("img",
 727            xs_html_attr("src", avatar),
 728            xs_html_attr("class", "snac-avatar"),
 729            xs_html_attr("alt", "")));
 730
 731    if (read_only) {
 732        xs *rss_url = xs_fmt("%s.rss", user->actor);
 733        xs *admin_url = xs_fmt("%s/admin", user->actor);
 734
 735        xs_html_add(top_nav,
 736            xs_html_tag("a",
 737                xs_html_attr("href", rss_url),
 738                xs_html_text(L("RSS"))),
 739            xs_html_text(" - "),
 740            xs_html_tag("a",
 741                xs_html_attr("href", admin_url),
 742                xs_html_attr("rel", "nofollow"),
 743                xs_html_text(L("private"))));
 744    }
 745    else {
 746        int n_len = notify_new_num(user);
 747        xs_html *notify_count = NULL;
 748
 749        /* show the number of new notifications, if there are any */
 750        if (n_len) {
 751            xs *n_len_str = xs_fmt(" %d ", n_len);
 752            notify_count = xs_html_tag("sup",
 753                xs_html_attr("style", "background-color: red; color: white;"),
 754                xs_html_text(n_len_str));
 755        }
 756        else
 757            notify_count = xs_html_text("");
 758
 759        xs *admin_url = xs_fmt("%s/admin", user->actor);
 760        xs *notify_url = xs_fmt("%s/notifications", user->actor);
 761        xs *people_url = xs_fmt("%s/people", user->actor);
 762        xs *instance_url = xs_fmt("%s/instance", user->actor);
 763
 764        xs_html_add(top_nav,
 765            xs_html_tag("a",
 766                xs_html_attr("href", user->actor),
 767                xs_html_text(L("public"))),
 768            xs_html_text(" - "),
 769            xs_html_tag("a",
 770                xs_html_attr("href", admin_url),
 771                xs_html_text(L("private"))),
 772            xs_html_text(" - "),
 773            xs_html_tag("a",
 774                xs_html_attr("href", notify_url),
 775                xs_html_text(L("notifications"))),
 776            notify_count,
 777            xs_html_text(" - "),
 778            xs_html_tag("a",
 779                xs_html_attr("href", people_url),
 780                xs_html_text(L("people"))),
 781            xs_html_text(" - "),
 782            xs_html_tag("a",
 783                xs_html_attr("href", instance_url),
 784                xs_html_text(L("instance"))),
 785            xs_html_text(" "),
 786            xs_html_tag("form",
 787                xs_html_attr("style", "display: inline!important"),
 788                xs_html_attr("class", "snac-search-box"),
 789                xs_html_attr("action", admin_url),
 790                    xs_html_sctag("input",
 791                        xs_html_attr("type", "text"),
 792                        xs_html_attr("name", "q"),
 793                        xs_html_attr("title", L("Search posts by URL or content (regular expression), @user@host accounts, or #tag")),
 794                        xs_html_attr("placeholder", L("Content search")))));
 795    }
 796
 797    xs_html_add(body,
 798        top_nav);
 799
 800    /* user info */
 801    xs_html *top_user = xs_html_tag("div",
 802        xs_html_attr("class", "h-card snac-top-user"));
 803
 804    if (read_only) {
 805        const char *header = xs_dict_get(user->config, "header");
 806        if (header && *header) {
 807            xs_html_add(top_user,
 808                xs_html_tag("div",
 809                    xs_html_attr("class", "snac-top-user-banner"),
 810                    xs_html_attr("style", "clear: both"),
 811                    xs_html_sctag("br", NULL),
 812                    xs_html_sctag("img",
 813                        xs_html_attr("src", header))));
 814        }
 815    }
 816
 817    xs *handle = xs_fmt("@%s@%s",
 818        xs_dict_get(user->config, "uid"),
 819        xs_dict_get(srv_config, "host"));
 820
 821    xs_html_add(top_user,
 822        xs_html_tag("p",
 823            xs_html_attr("class", "p-name snac-top-user-name"),
 824            xs_html_text(xs_dict_get(user->config, "name"))),
 825        xs_html_tag("p",
 826            xs_html_attr("class", "snac-top-user-id"),
 827            xs_html_text(handle)));
 828
 829    /** instance announcement **/
 830
 831    if (!read_only) {
 832        double la = 0.0;
 833        xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement"));
 834        if (user_la != NULL)
 835            la = xs_number_get(user_la);
 836
 837        const t_announcement *an = announcement(la);
 838        if (an != NULL && (an->text != NULL)) {
 839            xs *s = xs_fmt("?da=%.0f", an->timestamp);
 840
 841            xs_html_add(top_user,  xs_html_tag("div",
 842                xs_html_attr("class", "snac-announcement"),
 843                    xs_html_text(an->text),
 844                    xs_html_text(" "),
 845                    xs_html_tag("a",
 846                            xs_html_attr("href", s),
 847                            xs_html_text("Dismiss"))));
 848        }
 849    }
 850
 851    if (read_only) {
 852        xs *tags = xs_list_new();
 853        xs *bio1 = not_really_markdown(xs_dict_get(user->config, "bio"), NULL, &tags);
 854        xs *bio2 = process_tags(user, bio1, &tags);
 855        xs *bio3 = sanitize(bio2);
 856
 857        bio3 = replace_shortnames(bio3, tags, 2, proxy);
 858
 859        xs_html *top_user_bio = xs_html_tag("div",
 860            xs_html_attr("class", "p-note snac-top-user-bio"),
 861            xs_html_raw(bio3)); /* already sanitized */
 862
 863        xs_html_add(top_user,
 864            top_user_bio);
 865
 866        xs *metadata = NULL;
 867        const xs_dict *md = xs_dict_get(user->config, "metadata");
 868
 869        if (xs_type(md) == XSTYPE_DICT)
 870            metadata = xs_dup(md);
 871        else
 872        if (xs_type(md) == XSTYPE_STRING) {
 873            /* convert to dict for easier iteration */
 874            metadata = xs_dict_new();
 875            xs *l = xs_split(md, "\n");
 876            const char *ll;
 877
 878            xs_list_foreach(l, ll) {
 879                xs *kv = xs_split_n(ll, "=", 1);
 880                const char *k = xs_list_get(kv, 0);
 881                const char *v = xs_list_get(kv, 1);
 882
 883                if (k && v) {
 884                    xs *kk = xs_strip_i(xs_dup(k));
 885                    xs *vv = xs_strip_i(xs_dup(v));
 886                    metadata = xs_dict_set(metadata, kk, vv);
 887                }
 888            }
 889        }
 890
 891        if (xs_type(metadata) == XSTYPE_DICT) {
 892            const xs_str *k;
 893            const xs_str *v;
 894
 895            xs_dict *val_links = user->links;
 896            if (xs_is_null(val_links))
 897                val_links = xs_stock(XSTYPE_DICT);
 898
 899            xs_html *snac_metadata = xs_html_tag("div",
 900                xs_html_attr("class", "snac-metadata"));
 901
 902            int c = 0;
 903            while (xs_dict_next(metadata, &k, &v, &c)) {
 904                xs_html *value;
 905
 906                if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) {
 907                    /* is this link validated? */
 908                    xs *verified_link = NULL;
 909                    const xs_number *val_time = xs_dict_get(val_links, v);
 910
 911                    if (xs_type(val_time) == XSTYPE_NUMBER) {
 912                        time_t t = xs_number_get(val_time);
 913
 914                        if (t > 0) {
 915                            xs *s1 = xs_str_utctime(t, ISO_DATE_SPEC);
 916                            verified_link = xs_fmt("%s (%s)", L("verified link"), s1);
 917                        }
 918                    }
 919
 920                    if (!xs_is_null(verified_link)) {
 921                        value = xs_html_tag("span",
 922                            xs_html_attr("title", verified_link),
 923                            xs_html_raw("✔ "),
 924                            xs_html_tag("a",
 925                                xs_html_attr("rel", "me"),
 926                                xs_html_attr("href", v),
 927                                xs_html_text(v)));
 928                    }
 929                    else {
 930                        value = xs_html_tag("a",
 931                            xs_html_attr("rel", "me"),
 932                            xs_html_attr("href", v),
 933                            xs_html_text(v));
 934                    }
 935                }
 936                else
 937                if (xs_startswith(v, "gemini:/") || xs_startswith(v, "xmpp:")) {
 938                    value = xs_html_tag("a",
 939                        xs_html_attr("rel", "me"),
 940                        xs_html_attr("href", v),
 941                        xs_html_text(v));
 942                }
 943                else
 944                    value = xs_html_text(v);
 945
 946                xs_html_add(snac_metadata,
 947                    xs_html_tag("span",
 948                        xs_html_attr("class", "snac-property-name"),
 949                        xs_html_text(k)),
 950                    xs_html_text(":"),
 951                    xs_html_raw(" "),
 952                    xs_html_tag("span",
 953                        xs_html_attr("class", "snac-property-value"),
 954                        value),
 955                    xs_html_sctag("br", NULL));
 956            }
 957
 958            xs_html_add(top_user,
 959                snac_metadata);
 960        }
 961
 962        if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
 963            xs *fwers = follower_list(user);
 964            xs *fwing = following_list(user);
 965
 966            xs *s1 = xs_fmt(L("%d following %d followers"),
 967                xs_list_len(fwing), xs_list_len(fwers));
 968
 969            xs_html_add(top_user,
 970                xs_html_tag("p",
 971                    xs_html_text(s1)));
 972        }
 973    }
 974
 975    xs_html_add(body,
 976        top_user);
 977
 978    return body;
 979}
 980
 981
 982xs_html *html_top_controls(snac *snac)
 983/* generates the top controls */
 984{
 985    xs *ops_action = xs_fmt("%s/admin/action", snac->actor);
 986
 987    xs_html *top_controls = xs_html_tag("div",
 988        xs_html_attr("class", "snac-top-controls"),
 989
 990        /** new post **/
 991        html_note(snac, L("New Post..."),
 992            "new_post_div", "new_post_form",
 993            L("What's on your mind?"), "",
 994            NULL, NULL,
 995            xs_stock(XSTYPE_FALSE), "",
 996            xs_stock(XSTYPE_FALSE), NULL,
 997            NULL, 1, "", "", 0),
 998
 999        /** operations **/
1000        xs_html_tag("details",
1001            xs_html_tag("summary",
1002                xs_html_text(L("Operations..."))),
1003            xs_html_tag("p", NULL),
1004            xs_html_tag("form",
1005                xs_html_attr("autocomplete", "off"),
1006                xs_html_attr("method",       "post"),
1007                xs_html_attr("action",       ops_action),
1008                xs_html_sctag("input",
1009                    xs_html_attr("type",     "text"),
1010                    xs_html_attr("name",     "actor"),
1011                    xs_html_attr("required", "required"),
1012                    xs_html_attr("placeholder", "bob@example.com")),
1013                xs_html_text(" "),
1014                xs_html_sctag("input",
1015                    xs_html_attr("type",    "submit"),
1016                    xs_html_attr("name",    "action"),
1017                    xs_html_attr("value",   L("Follow"))),
1018            xs_html_text(" "),
1019            xs_html_text(L("(by URL or user@host)"))),
1020            xs_html_tag("p", NULL),
1021            xs_html_tag("form",
1022                xs_html_attr("autocomplete", "off"),
1023                xs_html_attr("method",       "post"),
1024                xs_html_attr("action",       ops_action),
1025                xs_html_sctag("input",
1026                    xs_html_attr("type",     "url"),
1027                    xs_html_attr("name",     "id"),
1028                    xs_html_attr("required", "required"),
1029                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1030                xs_html_text(" "),
1031                xs_html_sctag("input",
1032                    xs_html_attr("type",    "submit"),
1033                    xs_html_attr("name",    "action"),
1034                    xs_html_attr("value",   L("Boost"))),
1035                xs_html_text(" "),
1036                xs_html_text(L("(by URL)"))),
1037            xs_html_tag("p", NULL),
1038            xs_html_tag("form",
1039                xs_html_attr("autocomplete", "off"),
1040                xs_html_attr("method",       "post"),
1041                xs_html_attr("action",       ops_action),
1042                xs_html_sctag("input",
1043                    xs_html_attr("type",     "text"),
1044                    xs_html_attr("name",     "id"),
1045                    xs_html_attr("required", "required"),
1046                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1047                xs_html_text(" "),
1048                xs_html_sctag("input",
1049                    xs_html_attr("type",    "submit"),
1050                    xs_html_attr("name",    "action"),
1051                    xs_html_attr("value",   L("Like"))),
1052                xs_html_text(" "),
1053                xs_html_text(L("(by URL)"))),
1054            xs_html_tag("p", NULL)));
1055
1056    /** user settings **/
1057
1058    const char *email = "[disabled by admin]";
1059
1060    if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1061        email = xs_dict_get(snac->config_o, "email");
1062        if (xs_is_null(email)) {
1063            email = xs_dict_get(snac->config, "email");
1064
1065            if (xs_is_null(email))
1066                email = "";
1067        }
1068    }
1069
1070    const char *cw = xs_dict_get(snac->config, "cw");
1071    if (xs_is_null(cw))
1072        cw = "";
1073
1074    const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
1075    if (xs_is_null(telegram_bot))
1076        telegram_bot = "";
1077
1078    const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
1079    if (xs_is_null(telegram_chat_id))
1080        telegram_chat_id = "";
1081
1082    const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
1083    if (xs_is_null(ntfy_server))
1084        ntfy_server = "";
1085
1086    const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
1087    if (xs_is_null(ntfy_token))
1088        ntfy_token = "";
1089
1090    const char *purge_days = xs_dict_get(snac->config, "purge_days");
1091    if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
1092        purge_days = (char *)xs_number_str(purge_days);
1093    else
1094        purge_days = "0";
1095
1096    const xs_val *d_dm_f_u  = xs_dict_get(snac->config, "drop_dm_from_unknown");
1097    const xs_val *bot       = xs_dict_get(snac->config, "bot");
1098    const xs_val *a_private = xs_dict_get(snac->config, "private");
1099    const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
1100    const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
1101    const xs_val *pending    = xs_dict_get(snac->config, "approve_followers");
1102    const xs_val *show_foll  = xs_dict_get(snac->config, "show_contact_metrics");
1103
1104    xs *metadata = NULL;
1105    const xs_dict *md = xs_dict_get(snac->config, "metadata");
1106
1107    if (xs_type(md) == XSTYPE_DICT) {
1108        const xs_str *k;
1109        const xs_str *v;
1110
1111        metadata = xs_str_new(NULL);
1112
1113        xs_dict_foreach(md, k, v) {
1114            xs *kp = xs_fmt("%s=%s", k, v);
1115
1116            if (*metadata)
1117                metadata = xs_str_cat(metadata, "\n");
1118            metadata = xs_str_cat(metadata, kp);
1119        }
1120    }
1121    else
1122    if (xs_type(md) == XSTYPE_STRING)
1123        metadata = xs_dup(md);
1124    else
1125        metadata = xs_str_new(NULL);
1126
1127    xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
1128
1129    xs_html_add(top_controls,
1130        xs_html_tag("details",
1131        xs_html_tag("summary",
1132            xs_html_text(L("User Settings..."))),
1133        xs_html_tag("div",
1134            xs_html_attr("class", "snac-user-setup"),
1135            xs_html_tag("form",
1136                xs_html_attr("autocomplete", "off"),
1137                xs_html_attr("method",       "post"),
1138                xs_html_attr("action",       user_setup_action),
1139                xs_html_attr("enctype",      "multipart/form-data"),
1140                xs_html_tag("p",
1141                    xs_html_text(L("Display name:")),
1142                    xs_html_sctag("br", NULL),
1143                    xs_html_sctag("input",
1144                        xs_html_attr("type", "text"),
1145                        xs_html_attr("name", "name"),
1146                        xs_html_attr("value", xs_dict_get(snac->config, "name")),
1147                        xs_html_attr("placeholder", L("Your name")))),
1148                xs_html_tag("p",
1149                    xs_html_text(L("Avatar: ")),
1150                    xs_html_sctag("input",
1151                        xs_html_attr("type", "file"),
1152                        xs_html_attr("name", "avatar_file"))),
1153                xs_html_tag("p",
1154                    xs_html_sctag("input",
1155                        xs_html_attr("type", "checkbox"),
1156                        xs_html_attr("name", "avatar_delete")),
1157                        xs_html_text(L("Delete current avatar"))),
1158                xs_html_tag("p",
1159                    xs_html_text(L("Header image (banner): ")),
1160                    xs_html_sctag("input",
1161                        xs_html_attr("type", "file"),
1162                        xs_html_attr("name", "header_file"))),
1163                xs_html_tag("p",
1164                    xs_html_sctag("input",
1165                        xs_html_attr("type", "checkbox"),
1166                        xs_html_attr("name", "header_delete")),
1167                        xs_html_text(L("Delete current header image"))),
1168                xs_html_tag("p",
1169                    xs_html_text(L("Bio:")),
1170                    xs_html_sctag("br", NULL),
1171                    xs_html_tag("textarea",
1172                        xs_html_attr("name", "bio"),
1173                        xs_html_attr("cols", "40"),
1174                        xs_html_attr("rows", "4"),
1175                        xs_html_attr("placeholder", L("Write about yourself here...")),
1176                        xs_html_text(xs_dict_get(snac->config, "bio")))),
1177                xs_html_sctag("input",
1178                    xs_html_attr("type", "checkbox"),
1179                    xs_html_attr("name", "cw"),
1180                    xs_html_attr("id",   "cw"),
1181                    xs_html_attr(strcmp(cw, "open") == 0 ? "checked" : "", NULL)),
1182                xs_html_tag("label",
1183                    xs_html_attr("for", "cw"),
1184                    xs_html_text(L("Always show sensitive content"))),
1185                xs_html_tag("p",
1186                    xs_html_text(L("Email address for notifications:")),
1187                    xs_html_sctag("br", NULL),
1188                    xs_html_sctag("input",
1189                        xs_html_attr("type", "text"),
1190                        xs_html_attr("name", "email"),
1191                        xs_html_attr("value", email),
1192                        xs_html_attr("placeholder", "bob@example.com"))),
1193                xs_html_tag("p",
1194                    xs_html_text(L("Telegram notifications (bot key and chat id):")),
1195                    xs_html_sctag("br", NULL),
1196                    xs_html_sctag("input",
1197                        xs_html_attr("type", "text"),
1198                        xs_html_attr("name", "telegram_bot"),
1199                        xs_html_attr("value", telegram_bot),
1200                        xs_html_attr("placeholder", "Bot API key")),
1201                    xs_html_text(" "),
1202                    xs_html_sctag("input",
1203                        xs_html_attr("type", "text"),
1204                        xs_html_attr("name", "telegram_chat_id"),
1205                        xs_html_attr("value", telegram_chat_id),
1206                        xs_html_attr("placeholder", "Chat id"))),
1207                xs_html_tag("p",
1208                    xs_html_text(L("ntfy notifications (ntfy server and token):")),
1209                    xs_html_sctag("br", NULL),
1210                    xs_html_sctag("input",
1211                        xs_html_attr("type", "text"),
1212                        xs_html_attr("name", "ntfy_server"),
1213                        xs_html_attr("value", ntfy_server),
1214                        xs_html_attr("placeholder", "ntfy server - full URL (example: https://ntfy.sh/YourTopic)")),
1215                    xs_html_text(" "),
1216                    xs_html_sctag("input",
1217                        xs_html_attr("type", "text"),
1218                        xs_html_attr("name", "ntfy_token"),
1219                        xs_html_attr("value", ntfy_token),
1220                        xs_html_attr("placeholder", "ntfy token - if needed"))),
1221                xs_html_tag("p",
1222                    xs_html_text(L("Maximum days to keep posts (0: server settings):")),
1223                    xs_html_sctag("br", NULL),
1224                    xs_html_sctag("input",
1225                        xs_html_attr("type", "number"),
1226                        xs_html_attr("name", "purge_days"),
1227                        xs_html_attr("value", purge_days))),
1228                xs_html_tag("p",
1229                    xs_html_sctag("input",
1230                        xs_html_attr("type", "checkbox"),
1231                        xs_html_attr("name", "drop_dm_from_unknown"),
1232                        xs_html_attr("id", "drop_dm_from_unknown"),
1233                        xs_html_attr(xs_type(d_dm_f_u) == XSTYPE_TRUE ? "checked" : "", NULL)),
1234                    xs_html_tag("label",
1235                        xs_html_attr("for", "drop_dm_from_unknown"),
1236                        xs_html_text(L("Drop direct messages from people you don't follow")))),
1237                xs_html_tag("p",
1238                    xs_html_sctag("input",
1239                        xs_html_attr("type", "checkbox"),
1240                        xs_html_attr("name", "bot"),
1241                        xs_html_attr("id", "bot"),
1242                        xs_html_attr(xs_type(bot) == XSTYPE_TRUE ? "checked" : "", NULL)),
1243                    xs_html_tag("label",
1244                        xs_html_attr("for", "bot"),
1245                        xs_html_text(L("This account is a bot")))),
1246                xs_html_tag("p",
1247                    xs_html_sctag("input",
1248                        xs_html_attr("type", "checkbox"),
1249                        xs_html_attr("name", "auto_boost"),
1250                        xs_html_attr("id", "auto_boost"),
1251                        xs_html_attr(xs_is_true(auto_boost) ? "checked" : "", NULL)),
1252                    xs_html_tag("label",
1253                        xs_html_attr("for", "auto_boost"),
1254                        xs_html_text(L("Auto-boost all mentions to this account")))),
1255                xs_html_tag("p",
1256                    xs_html_sctag("input",
1257                        xs_html_attr("type", "checkbox"),
1258                        xs_html_attr("name", "private"),
1259                        xs_html_attr("id", "private"),
1260                        xs_html_attr(xs_type(a_private) == XSTYPE_TRUE ? "checked" : "", NULL)),
1261                    xs_html_tag("label",
1262                        xs_html_attr("for", "private"),
1263                        xs_html_text(L("This account is private "
1264                            "(posts are not shown through the web)")))),
1265                xs_html_tag("p",
1266                    xs_html_sctag("input",
1267                        xs_html_attr("type", "checkbox"),
1268                        xs_html_attr("name", "collapse_threads"),
1269                        xs_html_attr("id",   "collapse_threads"),
1270                        xs_html_attr(xs_is_true(coll_thrds) ? "checked" : "", NULL)),
1271                    xs_html_tag("label",
1272                        xs_html_attr("for", "collapse_threads"),
1273                        xs_html_text(L("Collapse top threads by default")))),
1274                xs_html_tag("p",
1275                    xs_html_sctag("input",
1276                        xs_html_attr("type", "checkbox"),
1277                        xs_html_attr("name", "approve_followers"),
1278                        xs_html_attr("id",   "approve_followers"),
1279                        xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
1280                    xs_html_tag("label",
1281                        xs_html_attr("for", "approve_followers"),
1282                        xs_html_text(L("Follow requests must be approved")))),
1283                xs_html_tag("p",
1284                    xs_html_sctag("input",
1285                        xs_html_attr("type", "checkbox"),
1286                        xs_html_attr("name", "show_contact_metrics"),
1287                        xs_html_attr("id",   "show_contact_metrics"),
1288                        xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
1289                    xs_html_tag("label",
1290                        xs_html_attr("for", "show_contact_metrics"),
1291                        xs_html_text(L("Publish follower and following metrics")))),
1292                xs_html_tag("p",
1293                    xs_html_text(L("Profile metadata (key=value pairs in each line):")),
1294                    xs_html_sctag("br", NULL),
1295                    xs_html_tag("textarea",
1296                        xs_html_attr("name", "metadata"),
1297                        xs_html_attr("cols", "40"),
1298                        xs_html_attr("rows", "4"),
1299                        xs_html_attr("placeholder", "Blog=https:/"
1300                            "/example.com/my-blog\nGPG Key=1FA54\n..."),
1301                    xs_html_text(metadata))),
1302
1303                xs_html_tag("p",
1304                    xs_html_text(L("New password:")),
1305                    xs_html_sctag("br", NULL),
1306                    xs_html_sctag("input",
1307                        xs_html_attr("type", "password"),
1308                        xs_html_attr("name", "passwd1"),
1309                        xs_html_attr("value", ""))),
1310                xs_html_tag("p",
1311                    xs_html_text(L("Repeat new password:")),
1312                    xs_html_sctag("br", NULL),
1313                    xs_html_sctag("input",
1314                        xs_html_attr("type", "password"),
1315                        xs_html_attr("name", "passwd2"),
1316                        xs_html_attr("value", ""))),
1317
1318                xs_html_sctag("input",
1319                    xs_html_attr("type", "submit"),
1320                    xs_html_attr("class", "button"),
1321                    xs_html_attr("value", L("Update user info")))))));
1322
1323    return top_controls;
1324}
1325
1326
1327static xs_html *html_button(char *clss, char *label, char *hint)
1328{
1329    xs *c = xs_fmt("snac-btn-%s", clss);
1330
1331    /* use an NULL tag to separate non-css-classed buttons from one another */
1332    return xs_html_container(
1333        xs_html_sctag("input",
1334            xs_html_attr("type",    "submit"),
1335            xs_html_attr("name",    "action"),
1336            xs_html_attr("class",   c),
1337            xs_html_attr("value",   label),
1338            xs_html_attr("title",   hint)),
1339        xs_html_text("\n"));
1340}
1341
1342
1343xs_str *build_mentions(snac *snac, const xs_dict *msg)
1344/* returns a string with the mentions in msg */
1345{
1346    xs_str *s = xs_str_new(NULL);
1347    const char *list = xs_dict_get(msg, "tag");
1348    const char *v;
1349    int c = 0;
1350
1351    while (xs_list_next(list, &v, &c)) {
1352        const char *type = xs_dict_get(v, "type");
1353        const char *href = xs_dict_get(v, "href");
1354        const char *name = xs_dict_get(v, "name");
1355
1356        if (type && strcmp(type, "Mention") == 0 &&
1357            href && strcmp(href, snac->actor) != 0 && name) {
1358            xs *s1 = NULL;
1359
1360            if (name[0] != '@') {
1361                s1 = xs_fmt("@%s", name);
1362                name = s1;
1363            }
1364
1365            xs *l = xs_split(name, "@");
1366
1367            /* is it a name without a host? */
1368            if (xs_list_len(l) < 3) {
1369                /* split the href and pick the host name LIKE AN ANIMAL */
1370                /* would be better to query the webfinger but *won't do that* here */
1371                xs *l2 = xs_split(href, "/");
1372
1373                if (xs_list_len(l2) >= 3) {
1374                    xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
1375
1376                    if (xs_str_in(s, s1) == -1)
1377                        s = xs_str_cat(s, s1);
1378                }
1379            }
1380            else {
1381                if (xs_str_in(s, name) == -1) {
1382                    s = xs_str_cat(s, name);
1383                    s = xs_str_cat(s, " ");
1384                }
1385            }
1386        }
1387    }
1388
1389    if (*s) {
1390        xs *s1 = s;
1391        s = xs_fmt("\n\n\nCC: %s", s1);
1392    }
1393
1394    return s;
1395}
1396
1397
1398xs_html *html_entry_controls(snac *snac, const char *actor,
1399                            const xs_dict *msg, const char *md5)
1400{
1401    const char *id    = xs_dict_get(msg, "id");
1402    const char *group = xs_dict_get(msg, "audience");
1403
1404    xs *likes   = object_likes(id);
1405    xs *boosts  = object_announces(id);
1406
1407    xs *action = xs_fmt("%s/admin/action", snac->actor);
1408    xs *redir  = xs_fmt("%s_entry", md5);
1409
1410    xs_html *form;
1411    xs_html *controls = xs_html_tag("div",
1412        xs_html_attr("class", "snac-controls"),
1413        form = xs_html_tag("form",
1414            xs_html_attr("autocomplete", "off"),
1415            xs_html_attr("method",       "post"),
1416            xs_html_attr("action",       action),
1417            xs_html_sctag("input",
1418                xs_html_attr("type",     "hidden"),
1419                xs_html_attr("name",     "id"),
1420                xs_html_attr("value",    id)),
1421            xs_html_sctag("input",
1422                xs_html_attr("type",     "hidden"),
1423                xs_html_attr("name",     "actor"),
1424                xs_html_attr("value",    actor)),
1425            xs_html_sctag("input",
1426                xs_html_attr("type",     "hidden"),
1427                xs_html_attr("name",     "group"),
1428                xs_html_attr("value",    xs_is_null(group) ? "" : group)),
1429            xs_html_sctag("input",
1430                xs_html_attr("type",     "hidden"),
1431                xs_html_attr("name",     "redir"),
1432                xs_html_attr("value",    redir))));
1433
1434    if (!xs_startswith(id, snac->actor)) {
1435        if (xs_list_in(likes, snac->md5) == -1) {
1436            /* not already liked; add button */
1437            xs_html_add(form,
1438                html_button("like", L("Like"), L("Say you like this post")));
1439        }
1440        else {
1441            /* not like it anymore */
1442            xs_html_add(form,
1443                html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
1444        }
1445    }
1446    else {
1447        if (is_pinned(snac, id))
1448            xs_html_add(form,
1449                html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
1450        else
1451            xs_html_add(form,
1452                html_button("pin", L("Pin"), L("Pin this post to the top of your timeline")));
1453    }
1454
1455    if (is_msg_public(msg)) {
1456        if (xs_list_in(boosts, snac->md5) == -1) {
1457            /* not already boosted; add button */
1458            xs_html_add(form,
1459                html_button("boost", L("Boost"), L("Announce this post to your followers")));
1460        }
1461        else {
1462            /* already boosted; add button to regret */
1463            xs_html_add(form,
1464                html_button("unboost", L("Unboost"), L("I regret I boosted this")));
1465        }
1466    }
1467
1468    if (is_bookmarked(snac, id))
1469            xs_html_add(form,
1470                html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
1471        else
1472            xs_html_add(form,
1473                html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
1474
1475    if (strcmp(actor, snac->actor) != 0) {
1476        /* controls for other actors than this one */
1477        if (following_check(snac, actor)) {
1478            xs_html_add(form,
1479                html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
1480        }
1481        else {
1482            xs_html_add(form,
1483                html_button("follow", L("Follow"), L("Start following this user's activity")));
1484        }
1485
1486        if (!xs_is_null(group)) {
1487            if (following_check(snac, group)) {
1488                xs_html_add(form,
1489                    html_button("unfollow", L("Unfollow Group"),
1490                        L("Stop following this group or channel")));
1491            }
1492            else {
1493                xs_html_add(form,
1494                    html_button("follow", L("Follow Group"),
1495                        L("Start following this group or channel")));
1496            }
1497        }
1498
1499        xs_html_add(form,
1500            html_button("mute", L("MUTE"),
1501                L("Block any activity from this user forever")));
1502    }
1503
1504    if (!xs_is_true(xs_dict_get(srv_config, "hide_delete_post_button")))
1505        xs_html_add(form,
1506            html_button("delete", L("Delete"), L("Delete this post")));
1507
1508    xs_html_add(form,
1509        html_button("hide",   L("Hide"), L("Hide this post and its children")));
1510
1511    const char *prev_src = xs_dict_get(msg, "sourceContent");
1512
1513    if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/
1514        /* post can be edited */
1515        xs *div_id  = xs_fmt("%s_edit", md5);
1516        xs *form_id = xs_fmt("%s_edit_form", md5);
1517        xs *redir   = xs_fmt("%s_entry", md5);
1518
1519        const char *att_file = "";
1520        const char *att_alt_text = "";
1521        const xs_list *att_list = xs_dict_get(msg, "attachment");
1522
1523        /* does it have an attachment? */
1524        if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) {
1525            const xs_dict *d = xs_list_get(att_list, 0);
1526
1527            if (xs_type(d) == XSTYPE_DICT) {
1528                att_file = xs_dict_get_def(d, "url", "");
1529                att_alt_text = xs_dict_get_def(d, "name", "");
1530            }
1531        }
1532
1533        xs_html_add(controls, xs_html_tag("div",
1534            xs_html_tag("p", NULL),
1535            html_note(snac, L("Edit..."),
1536                div_id, form_id,
1537                "", prev_src,
1538                id, NULL,
1539                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1540                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1541                NULL, 0, att_file, att_alt_text, is_draft(snac, id))),
1542            xs_html_tag("p", NULL));
1543    }
1544
1545    { /** reply **/
1546        /* the post textarea */
1547        xs *ct      = build_mentions(snac, msg);
1548        xs *div_id  = xs_fmt("%s_reply", md5);
1549        xs *form_id = xs_fmt("%s_reply_form", md5);
1550        xs *redir   = xs_fmt("%s_entry", md5);
1551
1552        xs_html_add(controls, xs_html_tag("div",
1553            xs_html_tag("p", NULL),
1554            html_note(snac, L("Reply..."),
1555                div_id, form_id,
1556                "", ct,
1557                NULL, NULL,
1558                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1559                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1560                id, 0, "", "", 0)),
1561            xs_html_tag("p", NULL));
1562    }
1563
1564    return controls;
1565}
1566
1567
1568xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1569                   int level, const char *md5, int hide_children)
1570{
1571    const char *id    = xs_dict_get(msg, "id");
1572    const char *type  = xs_dict_get(msg, "type");
1573    const char *actor;
1574    const char *v;
1575    int has_title = 0;
1576    int collapse_threads = 0;
1577    const char *proxy = NULL;
1578
1579    if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
1580        proxy = user->actor;
1581
1582    /* do not show non-public messages in the public timeline */
1583    if ((read_only || !user) && !is_msg_public(msg))
1584        return NULL;
1585
1586    if (id && is_instance_blocked(id))
1587        return NULL;
1588
1589    if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
1590        collapse_threads = 1;
1591
1592    /* hidden? do nothing more for this conversation */
1593    if (user && is_hidden(user, id)) {
1594        xs *s1 = xs_fmt("%s_entry", md5);
1595
1596        /* return just an dummy anchor, to keep position after hitting 'Hide' */
1597        return xs_html_tag("div",
1598            xs_html_tag("a",
1599                xs_html_attr("name", s1)));
1600    }
1601
1602    /* avoid too deep nesting, as it may be a loop */
1603    if (level >= MAX_CONVERSATION_LEVELS)
1604        return xs_html_tag("mark",
1605            xs_html_text(L("Truncated (too deep)")));
1606
1607    if (strcmp(type, "Follow") == 0) {
1608        return xs_html_tag("div",
1609            xs_html_attr("class", "snac-post"),
1610            xs_html_tag("div",
1611                xs_html_attr("class", "snac-post-header"),
1612                xs_html_tag("div",
1613                    xs_html_attr("class", "snac-origin"),
1614                    xs_html_text(L("follows you"))),
1615                html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy)));
1616    }
1617    else
1618    if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
1619        /* skip oddities */
1620        snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
1621        return NULL;
1622    }
1623
1624    /* ignore notes with "name", as they are votes to Questions */
1625    if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
1626        return NULL;
1627
1628    /* get the attributedTo */
1629    if ((actor = get_atto(msg)) == NULL)
1630        return NULL;
1631
1632    /* ignore muted morons immediately */
1633    if (user && is_muted(user, actor)) {
1634        xs *s1 = xs_fmt("%s_entry", md5);
1635
1636        /* return just an dummy anchor, to keep position after hitting 'MUTE' */
1637        return xs_html_tag("div",
1638            xs_html_tag("a",
1639                xs_html_attr("name", s1)));
1640    }
1641
1642    if ((user == NULL || strcmp(actor, user->actor) != 0)
1643        && !valid_status(actor_get(actor, NULL)))
1644        return NULL;
1645
1646    /** html_entry top tag **/
1647    xs_html *entry_top = xs_html_tag("div", NULL);
1648
1649    {
1650        xs *s1 = xs_fmt("%s_entry", md5);
1651        xs_html_add(entry_top,
1652            xs_html_tag("a",
1653                xs_html_attr("name", s1)));
1654    }
1655
1656    xs_html *entry = xs_html_tag("div",
1657        xs_html_attr("class", level == 0 ? "snac-post" : "snac-child"));
1658
1659    xs_html_add(entry_top,
1660        entry);
1661
1662    /** post header **/
1663
1664    xs_html *score;
1665    xs_html *post_header = xs_html_tag("div",
1666        xs_html_attr("class", "snac-post-header"),
1667        score = xs_html_tag("div",
1668            xs_html_attr("class", "snac-score")));
1669
1670    xs_html_add(entry,
1671        post_header);
1672
1673    if (user && is_pinned(user, id)) {
1674        /* add a pin emoji */
1675        xs_html_add(score,
1676            xs_html_tag("span",
1677                xs_html_attr("title", L("Pinned")),
1678                xs_html_raw(" &#128204; ")));
1679    }
1680
1681    if (user && is_bookmarked(user, id)) {
1682        /* add a bookmark emoji */
1683        xs_html_add(score,
1684            xs_html_tag("span",
1685                xs_html_attr("title", L("Bookmarked")),
1686                xs_html_raw(" &#128278; ")));
1687    }
1688
1689    if (strcmp(type, "Question") == 0) {
1690        /* add the ballot box emoji */
1691        xs_html_add(score,
1692            xs_html_tag("span",
1693                xs_html_attr("title", L("Poll")),
1694                xs_html_raw(" &#128499; ")));
1695
1696        if (user && was_question_voted(user, id)) {
1697            /* add a check to show this poll was voted */
1698            xs_html_add(score,
1699                xs_html_tag("span",
1700                    xs_html_attr("title", L("Voted")),
1701                    xs_html_raw(" &#10003; ")));
1702        }
1703    }
1704
1705    if (strcmp(type, "Event") == 0) {
1706        /* add the calendar emoji */
1707        xs_html_add(score,
1708            xs_html_tag("span",
1709                xs_html_attr("title", L("Event")),
1710                xs_html_raw(" &#128197; ")));
1711    }
1712
1713    /* if it's a user from this same instance, add the score */
1714    if (xs_startswith(id, srv_baseurl)) {
1715        int n_likes  = object_likes_len(id);
1716        int n_boosts = object_announces_len(id);
1717
1718        /* alternate emojis: %d &#128077; %d &#128257; */
1719        xs *s1 = xs_fmt("%d &#9733; %d &#8634;\n", n_likes, n_boosts);
1720
1721        xs_html_add(score,
1722            xs_html_raw(s1));
1723    }
1724
1725    xs *boosts = object_announces(id);
1726
1727    if (xs_list_len(boosts)) {
1728        /* if somebody boosted this, show as origin */
1729        const char *p = xs_list_get(boosts, -1);
1730        xs *actor_r = NULL;
1731
1732        if (user && xs_list_in(boosts, user->md5) != -1) {
1733            /* we boosted this */
1734            xs_html_add(post_header,
1735                xs_html_tag("div",
1736                    xs_html_attr("class", "snac-origin"),
1737                    xs_html_tag("a",
1738                        xs_html_attr("href", user->actor),
1739                        xs_html_text(xs_dict_get(user->config, "name"))),
1740                        xs_html_text(" "),
1741                        xs_html_text(L("boosted"))));
1742        }
1743        else
1744        if (valid_status(object_get_by_md5(p, &actor_r))) {
1745            xs *name = actor_name(actor_r, proxy);
1746
1747            if (!xs_is_null(name)) {
1748                xs *href = NULL;
1749                const char *id = xs_dict_get(actor_r, "id");
1750                int fwers = 0;
1751                int fwing = 0;
1752
1753                if (user != NULL) {
1754                    fwers = follower_check(user, id);
1755                    fwing = following_check(user, id);
1756                }
1757
1758                if (!read_only && (fwers || fwing))
1759                    href = xs_fmt("%s/people#%s", user->actor, p);
1760                else
1761                    href = xs_dup(id);
1762
1763                xs_html_add(post_header,
1764                    xs_html_tag("div",
1765                        xs_html_attr("class", "snac-origin"),
1766                        xs_html_tag("a",
1767                            xs_html_attr("href", href),
1768                            xs_html_raw(name)), /* already sanitized */
1769                            xs_html_text(" "),
1770                            xs_html_text(L("boosted"))));
1771            }
1772        }
1773    }
1774    else
1775    if (strcmp(type, "Note") == 0) {
1776        if (level == 0) {
1777            /* is the parent not here? */
1778            const char *parent = get_in_reply_to(msg);
1779
1780            if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
1781                xs_html_add(post_header,
1782                    xs_html_tag("div",
1783                        xs_html_attr("class", "snac-origin"),
1784                        xs_html_text(L("in reply to")),
1785                        xs_html_text(" "),
1786                        xs_html_tag("a",
1787                            xs_html_attr("href", parent),
1788                            xs_html_text("ยป"))));
1789            }
1790        }
1791    }
1792
1793    xs_html_add(post_header,
1794        html_msg_icon(read_only ? NULL : user, actor, msg, proxy));
1795
1796    /** post content **/
1797
1798    xs_html *snac_content_wrap = xs_html_tag("div",
1799        xs_html_attr("class", "e-content snac-content"));
1800
1801    xs_html_add(entry,
1802        snac_content_wrap);
1803
1804    if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
1805        xs_html_add(snac_content_wrap,
1806            xs_html_tag("h3",
1807                xs_html_attr("class", "snac-entry-title"),
1808                xs_html_text(v)));
1809
1810        has_title = 1;
1811    }
1812
1813    xs_html *snac_content = NULL;
1814
1815    v = xs_dict_get(msg, "summary");
1816
1817    /* is it sensitive? */
1818    if (xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
1819        if (xs_is_null(v) || *v == '\0')
1820            v = "...";
1821
1822        const char *cw = "";
1823
1824        if (user) {
1825            /* only show it when not in the public timeline and the config setting is "open" */
1826            cw = xs_dict_get(user->config, "cw");
1827            if (xs_is_null(cw) || read_only)
1828                cw = "";
1829        }
1830
1831        snac_content = xs_html_tag("details",
1832            xs_html_attr(cw, NULL),
1833            xs_html_tag("summary",
1834                xs_html_text(v),
1835                xs_html_text(L(" [SENSITIVE CONTENT]"))));
1836    }
1837    else {
1838        /* print the summary as a header (sites like e.g. Friendica can contain one) */
1839        if (!has_title && !xs_is_null(v) && *v) {
1840            xs_html_add(snac_content_wrap,
1841                xs_html_tag("h3",
1842                    xs_html_attr("class", "snac-entry-title"),
1843                    xs_html_text(v)));
1844
1845            has_title = 1;
1846        }
1847
1848        snac_content = xs_html_tag("div", NULL);
1849    }
1850
1851    xs_html_add(snac_content_wrap,
1852        snac_content);
1853
1854    {
1855        /** build the content string **/
1856        const char *content = xs_dict_get(msg, "content");
1857
1858        if (xs_type(content) != XSTYPE_STRING) {
1859            if (!xs_is_null(content))
1860                srv_archive_error("unexpected_content_xstype",
1861                    "content field type", xs_stock(XSTYPE_DICT), msg);
1862
1863            content = "";
1864        }
1865
1866        /* skip ugly line breaks at the beginning */
1867        while (xs_startswith(content, "<br>"))
1868            content += 4;
1869
1870        xs *c = sanitize(content);
1871
1872        /* do some tweaks to the content */
1873        c = xs_replace_i(c, "\r", "");
1874
1875        while (xs_endswith(c, "<br><br>"))
1876            c = xs_crop_i(c, 0, -4);
1877
1878        c = xs_replace_i(c, "<br><br>", "<p>");
1879
1880        c = xs_str_cat(c, "<p>");
1881
1882        /* replace the :shortnames: */
1883        c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
1884
1885        /* Peertube videos content is in markdown */
1886        const char *mtype = xs_dict_get(msg, "mediaType");
1887        if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
1888            /* a full conversion could be better */
1889            c = xs_replace_i(c, "\r", "");
1890            c = xs_replace_i(c, "\n", "<br>");
1891        }
1892
1893        /* c contains sanitized HTML */
1894        xs_html_add(snac_content,
1895            xs_html_raw(c));
1896    }
1897
1898    if (strcmp(type, "Question") == 0) { /** question content **/
1899        const xs_list *oo = xs_dict_get(msg, "oneOf");
1900        const xs_list *ao = xs_dict_get(msg, "anyOf");
1901        const xs_list *p;
1902        const xs_dict *v;
1903        int closed = 0;
1904        const char *f_closed = NULL;
1905
1906        xs_html *poll = xs_html_tag("div", NULL);
1907
1908        if (read_only)
1909            closed = 1; /* non-identified page; show as closed */
1910        else
1911        if (user && xs_startswith(id, user->actor))
1912            closed = 1; /* we questioned; closed for us */
1913        else
1914        if (user && was_question_voted(user, id))
1915            closed = 1; /* we already voted; closed for us */
1916
1917        if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
1918            /* it has a closed date... but is it in the past? */
1919            time_t t0 = time(NULL);
1920            time_t t1 = xs_parse_iso_date(f_closed, 0);
1921
1922            if (t1 < t0)
1923                closed = 2;
1924        }
1925
1926        /* get the appropriate list of options */
1927        p = oo != NULL ? oo : ao;
1928
1929        if (closed || user == NULL) {
1930            /* closed poll */
1931            xs_html *poll_result = xs_html_tag("table",
1932                xs_html_attr("class", "snac-poll-result"));
1933            int c = 0;
1934
1935            while (xs_list_next(p, &v, &c)) {
1936                const char *name       = xs_dict_get(v, "name");
1937                const xs_dict *replies = xs_dict_get(v, "replies");
1938
1939                if (name && replies) {
1940                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
1941
1942                    xs_html_add(poll_result,
1943                        xs_html_tag("tr",
1944                            xs_html_tag("td",
1945                                xs_html_text(name),
1946                                xs_html_text(":")),
1947                            xs_html_tag("td",
1948                                xs_html_text(ti))));
1949                }
1950            }
1951
1952            xs_html_add(poll,
1953                poll_result);
1954        }
1955        else {
1956            /* poll still active */
1957            xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
1958            xs_html *form;
1959            xs_html *poll_form = xs_html_tag("div",
1960                xs_html_attr("class", "snac-poll-form"),
1961                form = xs_html_tag("form",
1962                    xs_html_attr("autocomplete", "off"),
1963                    xs_html_attr("method", "post"),
1964                    xs_html_attr("action", vote_action),
1965                    xs_html_sctag("input",
1966                        xs_html_attr("type", "hidden"),
1967                        xs_html_attr("name", "actor"),
1968                        xs_html_attr("value", actor)),
1969                    xs_html_sctag("input",
1970                        xs_html_attr("type", "hidden"),
1971                        xs_html_attr("name", "irt"),
1972                        xs_html_attr("value", id))));
1973
1974            int c = 0;
1975            while (xs_list_next(p, &v, &c)) {
1976                const char *name = xs_dict_get(v, "name");
1977                const xs_dict *replies = xs_dict_get(v, "replies");
1978
1979                if (name) {
1980                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
1981
1982                    xs_html *btn = xs_html_sctag("input",
1983                            xs_html_attr("id", name),
1984                            xs_html_attr("value", name),
1985                            xs_html_attr("name", "question"));
1986
1987                    if (!xs_is_null(oo)) {
1988                        xs_html_add(btn,
1989                            xs_html_attr("type", "radio"),
1990                            xs_html_attr("required", "required"));
1991                    }
1992                    else
1993                        xs_html_add(btn,
1994                            xs_html_attr("type", "checkbox"));
1995
1996                    xs_html_add(form,
1997                        btn,
1998                        xs_html_text(" "),
1999                        xs_html_tag("span",
2000                            xs_html_attr("title", ti),
2001                            xs_html_text(name)),
2002                        xs_html_sctag("br", NULL));
2003                }
2004            }
2005
2006            xs_html_add(form,
2007                xs_html_tag("p", NULL),
2008                xs_html_sctag("input",
2009                    xs_html_attr("type", "submit"),
2010                    xs_html_attr("class", "button"),
2011                    xs_html_attr("value", L("Vote"))));
2012
2013            xs_html_add(poll,
2014                poll_form);
2015        }
2016
2017        /* if it's *really* closed, say it */
2018        if (closed == 2) {
2019            xs_html_add(poll,
2020                xs_html_tag("p",
2021                    xs_html_text(L("Closed"))));
2022        }
2023        else {
2024            /* show when the poll closes */
2025            const char *end_time = xs_dict_get(msg, "endTime");
2026
2027            /* Pleroma does not have an endTime field;
2028               it has a closed time in the future */
2029            if (xs_is_null(end_time))
2030                end_time = xs_dict_get(msg, "closed");
2031
2032            if (!xs_is_null(end_time)) {
2033                time_t t0 = time(NULL);
2034                time_t t1 = xs_parse_iso_date(end_time, 0);
2035
2036                if (t1 > 0 && t1 > t0) {
2037                    time_t diff_time = t1 - t0;
2038                    xs *tf = xs_str_time_diff(diff_time);
2039                    char *p = tf;
2040
2041                    /* skip leading zeros */
2042                    for (; *p == '0' || *p == ':'; p++);
2043
2044                    xs_html_add(poll,
2045                        xs_html_tag("p",
2046                            xs_html_text(L("Closes in")),
2047                            xs_html_text(" "),
2048                            xs_html_text(p)));
2049                }
2050            }
2051        }
2052
2053        xs_html_add(snac_content,
2054            poll);
2055    }
2056
2057    /** attachments **/
2058    xs *attach = get_attachments(msg);
2059
2060    {
2061        /* make custom css for attachments easier */
2062        xs_html *content_attachments = xs_html_tag("div",
2063            xs_html_attr("class", "snac-content-attachments"));
2064
2065        xs_html_add(snac_content,
2066            content_attachments);
2067
2068        const char *content = xs_dict_get(msg, "content");
2069
2070        int c = 0;
2071        const xs_dict *a;
2072        while (xs_list_next(attach, &a, &c)) {
2073            const char *type = xs_dict_get(a, "type");
2074            const char *o_href = xs_dict_get(a, "href");
2075            const char *name = xs_dict_get(a, "name");
2076
2077            /* if this image is already in the post content, skip */
2078            if (content && xs_str_in(content, o_href) != -1)
2079                continue;
2080
2081            /* do this attachment include an icon? */
2082            const xs_dict *icon = xs_dict_get(a, "icon");
2083            if (xs_type(icon) == XSTYPE_DICT) {
2084                const char *icon_mtype = xs_dict_get(icon, "mediaType");
2085                const char *icon_url   = xs_dict_get(icon, "url");
2086
2087                if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
2088                    xs_html_add(content_attachments,
2089                        xs_html_tag("a",
2090                            xs_html_attr("href", icon_url),
2091                            xs_html_attr("target", "_blank"),
2092                            xs_html_sctag("img",
2093                                xs_html_attr("loading", "lazy"),
2094                                xs_html_attr("src", icon_url))));
2095                }
2096            }
2097
2098            xs *href = make_url(o_href, proxy, 0);
2099
2100            if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
2101                xs_html_add(content_attachments,
2102                    xs_html_tag("a",
2103                        xs_html_attr("href", href),
2104                        xs_html_attr("target", "_blank"),
2105                        xs_html_sctag("img",
2106                            xs_html_attr("loading", "lazy"),
2107                            xs_html_attr("src", href),
2108                            xs_html_attr("alt", name),
2109                            xs_html_attr("title", name))));
2110            }
2111            else
2112            if (xs_startswith(type, "video/")) {
2113                xs_html_add(content_attachments,
2114                    xs_html_tag("video",
2115                        xs_html_attr("preload", "none"),
2116                        xs_html_attr("style", "width: 100%"),
2117                        xs_html_attr("class", "snac-embedded-video"),
2118                        xs_html_attr("controls", NULL),
2119                        xs_html_attr("src", href),
2120                        xs_html_text(L("Video")),
2121                        xs_html_text(": "),
2122                        xs_html_tag("a",
2123                            xs_html_attr("href", href),
2124                            xs_html_text(name))));
2125            }
2126            else
2127            if (xs_startswith(type, "audio/")) {
2128                xs_html_add(content_attachments,
2129                    xs_html_tag("audio",
2130                        xs_html_attr("preload", "none"),
2131                        xs_html_attr("style", "width: 100%"),
2132                        xs_html_attr("class", "snac-embedded-audio"),
2133                        xs_html_attr("controls", NULL),
2134                        xs_html_attr("src", href),
2135                        xs_html_text(L("Audio")),
2136                        xs_html_text(": "),
2137                        xs_html_tag("a",
2138                            xs_html_attr("href", href),
2139                            xs_html_text(name))));
2140            }
2141            else
2142            if (strcmp(type, "Link") == 0) {
2143                xs_html_add(content_attachments,
2144                    xs_html_tag("p",
2145                        xs_html_tag("a",
2146                            xs_html_attr("href", o_href),
2147                            xs_html_text(href))));
2148
2149                /* do not generate an Alt... */
2150                name = NULL;
2151            }
2152            else {
2153                xs_html_add(content_attachments,
2154                    xs_html_tag("p",
2155                        xs_html_tag("a",
2156                            xs_html_attr("href", o_href),
2157                            xs_html_text(L("Attachment")),
2158                            xs_html_text(": "),
2159                            xs_html_text(o_href))));
2160
2161                /* do not generate an Alt... */
2162                name = NULL;
2163            }
2164
2165            if (name != NULL && *name) {
2166                xs_html_add(content_attachments,
2167                    xs_html_tag("p",
2168                        xs_html_attr("class", "snac-alt-text"),
2169                        xs_html_tag("details",
2170                            xs_html_tag("summary",
2171                                xs_html_text(L("Alt..."))),
2172                            xs_html_text(name))));
2173            }
2174        }
2175    }
2176
2177    /* has this message an audience (i.e., comes from a channel or community)? */
2178    const char *audience = xs_dict_get(msg, "audience");
2179    if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
2180        xs_html *au_tag = xs_html_tag("p",
2181            xs_html_text("("),
2182            xs_html_tag("a",
2183                xs_html_attr("href", audience),
2184                xs_html_attr("title", L("Source channel or community")),
2185                xs_html_text(audience)),
2186            xs_html_text(")"));
2187
2188        xs_html_add(snac_content_wrap,
2189            au_tag);
2190    }
2191
2192    /** controls **/
2193
2194    if (!read_only && user) {
2195        xs_html_add(entry,
2196            html_entry_controls(user, actor, msg, md5));
2197    }
2198
2199    /** children **/
2200    if (!hide_children) {
2201        xs *children = object_children(id);
2202        int left     = xs_list_len(children);
2203
2204        if (left) {
2205            xs_html *ch_details = xs_html_tag("details",
2206                xs_html_attr(collapse_threads ? "" : "open", NULL),
2207                xs_html_tag("summary",
2208                    xs_html_text("...")));
2209
2210            xs_html_add(entry,
2211                ch_details);
2212
2213            xs_html *ch_container = xs_html_tag("div",
2214                xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
2215
2216            xs_html_add(ch_details,
2217                ch_container);
2218
2219            xs_html *ch_older = NULL;
2220            if (left > 3) {
2221                xs_html_add(ch_container,
2222                    ch_older = xs_html_tag("details",
2223                        xs_html_tag("summary",
2224                            xs_html_text(L("Older...")))));
2225            }
2226
2227            xs_list *p = children;
2228            const char *cmd5;
2229            int cnt = 0;
2230            int o_cnt = 0;
2231
2232            while (xs_list_iter(&p, &cmd5)) {
2233                xs *chd = NULL;
2234
2235                if (user)
2236                    timeline_get_by_md5(user, cmd5, &chd);
2237                else
2238                    object_get_by_md5(cmd5, &chd);
2239
2240                if (chd != NULL) {
2241                    if (xs_is_null(xs_dict_get(chd, "name"))) {
2242                        xs_html *che = html_entry(user, chd, read_only,
2243                            level + 1, cmd5, hide_children);
2244
2245                        if (che != NULL) {
2246                            if (left > 3) {
2247                                xs_html_add(ch_older,
2248                                    che);
2249
2250                                o_cnt++;
2251                            }
2252                            else
2253                                xs_html_add(ch_container,
2254                                    che);
2255
2256                            cnt++;
2257                        }
2258                    }
2259
2260                    left--;
2261                }
2262                else
2263                    srv_debug(2, xs_fmt("cannot read child %s", cmd5));
2264            }
2265
2266            /* if no children were finally added, hide the details */
2267            if (cnt == 0)
2268                xs_html_add(ch_details,
2269                    xs_html_attr("style", "display: none"));
2270
2271            if (o_cnt == 0 && ch_older)
2272                xs_html_add(ch_older,
2273                    xs_html_attr("style", "display: none"));
2274        }
2275    }
2276
2277    return entry_top;
2278}
2279
2280
2281xs_html *html_footer(void)
2282{
2283    return xs_html_tag("div",
2284        xs_html_attr("class", "snac-footer"),
2285        xs_html_tag("a",
2286            xs_html_attr("href", srv_baseurl),
2287            xs_html_text(L("about this site"))),
2288        xs_html_text(" - "),
2289        xs_html_text(L("powered by ")),
2290        xs_html_tag("a",
2291            xs_html_attr("href", WHAT_IS_SNAC_URL),
2292            xs_html_tag("abbr",
2293                xs_html_attr("title", "Social Networks Are Crap"),
2294                xs_html_text("snac"))));
2295}
2296
2297
2298xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2299                      int skip, int show, int show_more,
2300                      const char *title, const char *page,
2301                      int utl, const char *error)
2302/* returns the HTML for the timeline */
2303{
2304    xs_list *p = (xs_list *)list;
2305    const char *v;
2306    double t = ftime();
2307    int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
2308
2309    xs *desc = NULL;
2310    xs *alternate = NULL;
2311
2312    if (xs_list_len(list) == 1) {
2313        /* only one element? pick the description from the source */
2314        const char *id = xs_list_get(list, 0);
2315        xs *d = NULL;
2316        object_get_by_md5(id, &d);
2317        const char *sc = xs_dict_get(d, "sourceContent");
2318        if (d && sc != NULL)
2319            desc = xs_dup(sc);
2320
2321        alternate = xs_dup(xs_dict_get(d, "id"));
2322    }
2323
2324    xs_html *head;
2325    xs_html *body;
2326
2327    if (user) {
2328        head = html_user_head(user, desc, alternate);
2329        body = html_user_body(user, read_only);
2330    }
2331    else {
2332        head = html_instance_head();
2333        body = html_instance_body();
2334    }
2335
2336    xs_html *html = xs_html_tag("html",
2337        head,
2338        body);
2339
2340    if (user && !read_only)
2341        xs_html_add(body,
2342            html_top_controls(user));
2343
2344    if (error != NULL) {
2345        xs_html_add(body,
2346            xs_html_tag("dialog",
2347                xs_html_attr("open", NULL),
2348                xs_html_tag("p",
2349                    xs_html_text(error)),
2350                xs_html_tag("form",
2351                    xs_html_attr("method", "dialog"),
2352                    xs_html_sctag("input",
2353                        xs_html_attr("type", "submit"),
2354                        xs_html_attr("value", L("Dismiss"))))));
2355    }
2356
2357    /* show links to the available lists */
2358    if (user && !read_only) {
2359        xs_html *lol = xs_html_tag("ul",
2360            xs_html_attr("class", "snac-list-of-lists"));
2361        xs_html_add(body, lol);
2362
2363        xs *lists = list_maint(user, NULL, 0); /* get list of lists */
2364
2365        int ct = 0;
2366        const char *v;
2367
2368        while (xs_list_next(lists, &v, &ct)) {
2369            const char *lname = xs_list_get(v, 1);
2370            xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
2371            xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
2372
2373            xs_html_add(lol,
2374                xs_html_tag("li",
2375                    xs_html_tag("a",
2376                        xs_html_attr("href", url),
2377                        xs_html_attr("class", "snac-list-link"),
2378                        xs_html_attr("title", ttl),
2379                        xs_html_text(lname))));
2380        }
2381
2382        {
2383            /* show the list of pinned posts */
2384            xs *url = xs_fmt("%s/pinned", user->actor);
2385            xs_html_add(lol,
2386                xs_html_tag("li",
2387                    xs_html_tag("a",
2388                        xs_html_attr("href", url),
2389                        xs_html_attr("class", "snac-list-link"),
2390                        xs_html_attr("title", L("Pinned posts")),
2391                        xs_html_text("pinned"))));
2392        }
2393
2394        {
2395            /* show the list of bookmarked posts */
2396            xs *url = xs_fmt("%s/bookmarks", user->actor);
2397            xs_html_add(lol,
2398                xs_html_tag("li",
2399                    xs_html_tag("a",
2400                        xs_html_attr("href", url),
2401                        xs_html_attr("class", "snac-list-link"),
2402                        xs_html_attr("title", L("Bookmarked posts")),
2403                        xs_html_text("bookmarks"))));
2404        }
2405
2406        {
2407            /* show the list of drafts */
2408            xs *url = xs_fmt("%s/drafts", user->actor);
2409            xs_html_add(lol,
2410                xs_html_tag("li",
2411                    xs_html_tag("a",
2412                        xs_html_attr("href", url),
2413                        xs_html_attr("class", "snac-list-link"),
2414                        xs_html_attr("title", L("Post drafts")),
2415                        xs_html_text("drafts"))));
2416        }
2417    }
2418
2419    xs_html_add(body,
2420        xs_html_tag("a",
2421            xs_html_attr("name", "snac-posts")));
2422
2423    xs_html *posts = xs_html_tag("div",
2424        xs_html_attr("class", "snac-posts"));
2425
2426    if (title) {
2427        xs_html_add(posts,
2428            xs_html_tag("h2",
2429                xs_html_attr("class", "snac-header"),
2430                xs_html_text(title)));
2431    }
2432
2433    xs_html_add(body,
2434        posts);
2435
2436    while (xs_list_iter(&p, &v)) {
2437        xs *msg = NULL;
2438        int status;
2439
2440        if (utl && user && !is_pinned_by_md5(user, v))
2441            status = timeline_get_by_md5(user, v, &msg);
2442        else
2443            status = object_get_by_md5(v, &msg);
2444
2445        if (!valid_status(status))
2446            continue;
2447
2448        /* if it's an instance page, discard messages from private users */
2449        if (user == NULL && is_msg_from_private_user(msg))
2450            continue;
2451
2452        /* is this message a non-public reply? */
2453        if (user != NULL && !is_msg_public(msg)) {
2454            const char *irt = get_in_reply_to(msg);
2455
2456            /* is it a reply to something not in the storage? */
2457            if (!xs_is_null(irt) && !object_here(irt)) {
2458                /* is it for me? */
2459                const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
2460                const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
2461
2462                if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
2463                    snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
2464                    continue;
2465                }
2466            }
2467        }
2468
2469        xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
2470
2471        if (entry != NULL)
2472            xs_html_add(posts,
2473                entry);
2474    }
2475
2476    if (list && user && read_only) {
2477        /** history **/
2478        if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE) {
2479            xs_html *ul = xs_html_tag("ul", NULL);
2480
2481            xs_html *history = xs_html_tag("div",
2482                xs_html_attr("class", "snac-history"),
2483                xs_html_tag("p",
2484                    xs_html_attr("class", "snac-history-title"),
2485                    xs_html_text(L("History"))),
2486                    ul);
2487
2488            xs *list = history_list(user);
2489            xs_list *p = list;
2490            const char *v;
2491
2492            while (xs_list_iter(&p, &v)) {
2493                xs *fn  = xs_replace(v, ".html", "");
2494                xs *url = xs_fmt("%s/h/%s", user->actor, v);
2495
2496                xs_html_add(ul,
2497                    xs_html_tag("li",
2498                        xs_html_tag("a",
2499                            xs_html_attr("href", url),
2500                            xs_html_text(fn))));
2501            }
2502
2503            xs_html_add(body,
2504                history);
2505        }
2506    }
2507
2508    {
2509        xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
2510        xs_html_add(body,
2511            xs_html_raw(s1));
2512    }
2513
2514    if (show_more) {
2515        xs *m  = NULL;
2516        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
2517
2518        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
2519
2520        if (page != NULL)
2521            url = xs_str_cat(url, page);
2522
2523        if (xs_str_in(url, "?") != -1)
2524            m = xs_fmt("%s&%s", url, ss);
2525        else
2526            m = xs_fmt("%s?%s", url, ss);
2527
2528        xs_html *more_links = xs_html_tag("p",
2529            xs_html_tag("a",
2530                xs_html_attr("href", url),
2531                xs_html_attr("name", "snac-more"),
2532                xs_html_text(L("Back to top"))),
2533            xs_html_text(" - "),
2534            xs_html_tag("a",
2535                xs_html_attr("href", m),
2536                xs_html_attr("name", "snac-more"),
2537                xs_html_text(L("More..."))));
2538
2539        xs_html_add(body,
2540            more_links);
2541    }
2542
2543    xs_html_add(body,
2544        html_footer());
2545
2546    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2547}
2548
2549
2550xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, const char *proxy)
2551{
2552    xs_html *snac_posts;
2553    xs_html *people = xs_html_tag("div",
2554        xs_html_tag("h2",
2555            xs_html_attr("class", "snac-header"),
2556            xs_html_text(header)),
2557        snac_posts = xs_html_tag("details",
2558                xs_html_attr("open", NULL),
2559                xs_html_tag("summary",
2560                    xs_html_text("..."))));
2561
2562    const char *actor_id;
2563
2564    xs_list_foreach(list, actor_id) {
2565        xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
2566        xs *actor = NULL;
2567
2568        if (valid_status(actor_get(actor_id, &actor))) {
2569            xs_html *snac_post = xs_html_tag("div",
2570                xs_html_attr("class", "snac-post"),
2571                xs_html_tag("a",
2572                    xs_html_attr("name", md5)),
2573                xs_html_tag("div",
2574                    xs_html_attr("class", "snac-post-header"),
2575                    html_actor_icon(snac, actor, xs_dict_get(actor, "published"),
2576                                    NULL, NULL, 0, 1, proxy)));
2577
2578            /* content (user bio) */
2579            const char *c = xs_dict_get(actor, "summary");
2580
2581            if (!xs_is_null(c)) {
2582                xs *sc = sanitize(c);
2583
2584                xs_html *snac_content = xs_html_tag("div",
2585                    xs_html_attr("class", "snac-content"));
2586
2587                if (xs_startswith(sc, "<p>"))
2588                    xs_html_add(snac_content,
2589                        xs_html_raw(sc)); /* already sanitized */
2590                else
2591                    xs_html_add(snac_content,
2592                        xs_html_tag("p",
2593                            xs_html_raw(sc))); /* already sanitized */
2594
2595                xs_html_add(snac_post, snac_content);
2596            }
2597
2598            /* buttons */
2599            xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor);
2600
2601            xs_html *snac_controls = xs_html_tag("div",
2602                xs_html_attr("class",   "snac-controls"));
2603
2604            xs_html *form = xs_html_tag("form",
2605                xs_html_attr("autocomplete",    "off"),
2606                xs_html_attr("method",          "post"),
2607                xs_html_attr("action",          btn_form_action),
2608                xs_html_sctag("input",
2609                    xs_html_attr("type",    "hidden"),
2610                    xs_html_attr("name",    "actor"),
2611                    xs_html_attr("value",   actor_id)),
2612                xs_html_sctag("input",
2613                    xs_html_attr("type",    "hidden"),
2614                    xs_html_attr("name",    "actor-form"),
2615                    xs_html_attr("value",   "yes")));
2616
2617            xs_html_add(snac_controls, form);
2618
2619            if (following_check(snac, actor_id)) {
2620                xs_html_add(form,
2621                    html_button("unfollow", L("Unfollow"),
2622                                L("Stop following this user's activity")));
2623
2624                if (is_limited(snac, actor_id))
2625                    xs_html_add(form,
2626                        html_button("unlimit", L("Unlimit"),
2627                                L("Allow announces (boosts) from this user")));
2628                else
2629                    xs_html_add(form,
2630                        html_button("limit", L("Limit"),
2631                                L("Block announces (boosts) from this user")));
2632            }
2633            else {
2634                xs_html_add(form,
2635                    html_button("follow", L("Follow"),
2636                                L("Start following this user's activity")));
2637
2638                if (follower_check(snac, actor_id))
2639                    xs_html_add(form,
2640                        html_button("delete", L("Delete"), L("Delete this user")));
2641            }
2642
2643            if (pending_check(snac, actor_id)) {
2644                xs_html_add(form,
2645                    html_button("approve", L("Approve"),
2646                                L("Approve this follow request")));
2647
2648                xs_html_add(form,
2649                    html_button("discard", L("Discard"), L("Discard this follow request")));
2650            }
2651
2652            if (is_muted(snac, actor_id))
2653                xs_html_add(form,
2654                    html_button("unmute", L("Unmute"),
2655                                L("Stop blocking activities from this user")));
2656            else
2657                xs_html_add(form,
2658                    html_button("mute", L("MUTE"),
2659                                L("Block any activity from this user")));
2660
2661            /* the post textarea */
2662            xs *dm_div_id  = xs_fmt("%s_%s_dm", md5, t);
2663            xs *dm_form_id = xs_fmt("%s_reply_form", md5);
2664
2665            xs_html_add(snac_controls,
2666                xs_html_tag("p", NULL),
2667                html_note(snac, L("Direct Message..."),
2668                    dm_div_id, dm_form_id,
2669                    "", "",
2670                    NULL, actor_id,
2671                    xs_stock(XSTYPE_FALSE), "",
2672                    xs_stock(XSTYPE_FALSE), NULL,
2673                    NULL, 0, "", "", 0),
2674                xs_html_tag("p", NULL));
2675
2676            xs_html_add(snac_post, snac_controls);
2677
2678            xs_html_add(snac_posts, snac_post);
2679        }
2680    }
2681
2682    return people;
2683}
2684
2685
2686xs_str *html_people(snac *user)
2687{
2688    const char *proxy = NULL;
2689
2690    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2691        proxy = user->actor;
2692
2693    xs *wing = following_list(user);
2694    xs *wers = follower_list(user);
2695
2696    xs_html *lists = xs_html_tag("div",
2697        xs_html_attr("class", "snac-posts"));
2698
2699    if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
2700        xs *pending = pending_list(user);
2701        xs_html_add(lists,
2702            html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
2703    }
2704
2705    xs_html_add(lists,
2706        html_people_list(user, wing, L("People you follow"), "i", proxy),
2707        html_people_list(user, wers, L("People that follow you"), "e", proxy));
2708
2709    xs_html *html = xs_html_tag("html",
2710        html_user_head(user, NULL, NULL),
2711        xs_html_add(html_user_body(user, 0),
2712            lists,
2713            html_footer()));
2714
2715    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2716}
2717
2718
2719xs_str *html_notifications(snac *user, int skip, int show)
2720{
2721    const char *proxy = NULL;
2722
2723    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2724        proxy = user->actor;
2725
2726    xs *n_list = notify_list(user, skip, show);
2727    xs *n_time = notify_check_time(user, 0);
2728
2729    xs_html *body = html_user_body(user, 0);
2730
2731    xs_html *html = xs_html_tag("html",
2732        html_user_head(user, NULL, NULL),
2733        body);
2734
2735    xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
2736
2737    xs_html_add(body,
2738        xs_html_tag("form",
2739            xs_html_attr("autocomplete", "off"),
2740            xs_html_attr("method",       "post"),
2741            xs_html_attr("action",       clear_all_action),
2742            xs_html_attr("id",           "clear"),
2743            xs_html_sctag("input",
2744                xs_html_attr("type",     "submit"),
2745                xs_html_attr("class",    "snac-btn-like"),
2746                xs_html_attr("value",    L("Clear all")))));
2747
2748    xs_html *noti_new = NULL;
2749    xs_html *noti_seen = NULL;
2750
2751    xs_html *posts = xs_html_tag("div",
2752        xs_html_attr("class", "snac-posts"));
2753    xs_html_add(body, posts);
2754
2755    xs_list *p = n_list;
2756    const xs_str *v;
2757    while (xs_list_iter(&p, &v)) {
2758        xs *noti = notify_get(user, v);
2759
2760        if (noti == NULL)
2761            continue;
2762
2763        xs *obj = NULL;
2764        const char *type  = xs_dict_get(noti, "type");
2765        const char *utype = xs_dict_get(noti, "utype");
2766        const char *id    = xs_dict_get(noti, "objid");
2767        const char *date  = xs_dict_get(noti, "date");
2768        xs *wrk = NULL;
2769
2770        if (xs_is_null(id))
2771            continue;
2772
2773        if (is_hidden(user, id))
2774            continue;
2775
2776        object_get(id, &obj);
2777
2778        const char *actor_id = xs_dict_get(noti, "actor");
2779        xs *actor = NULL;
2780
2781        if (!valid_status(actor_get(actor_id, &actor)))
2782            continue;
2783
2784        xs *a_name = actor_name(actor, proxy);
2785        const char *label = type;
2786
2787        if (strcmp(type, "Create") == 0)
2788            label = L("Mention");
2789        else
2790        if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
2791            label = L("Finished poll");
2792        else
2793        if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
2794            label = L("Unfollow");
2795        else
2796        if (strcmp(type, "EmojiReact") == 0) {
2797            const char *content = xs_dict_get_path(noti, "msg.content");
2798
2799            if (xs_type(content) == XSTYPE_STRING) {
2800                wrk = xs_fmt("%s (%s)", type, content);
2801                label = wrk;
2802            }
2803        }
2804        else
2805        if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
2806            label = L("Follow Request");
2807
2808        xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
2809
2810        xs_html *entry = xs_html_tag("div",
2811            xs_html_attr("class", "snac-post-with-desc"),
2812            xs_html_tag("p",
2813                xs_html_tag("b",
2814                    xs_html_text(label),
2815                    xs_html_text(" by "),
2816                    xs_html_tag("a",
2817                        xs_html_attr("href", actor_id),
2818                        xs_html_raw(a_name))), /* a_name is already sanitized */
2819                xs_html_text(" "),
2820                xs_html_tag("time",
2821                    xs_html_attr("class", "dt-published snac-pubdate"),
2822                    xs_html_attr("title", date),
2823                    xs_html_text(s_date))));
2824
2825        if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) {
2826            xs_html_add(entry,
2827                xs_html_tag("div",
2828                    xs_html_attr("class", "snac-post"),
2829                    html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy)));
2830        }
2831        else
2832        if (strcmp(type, "Move") == 0) {
2833            const xs_dict *o_msg = xs_dict_get(noti, "msg");
2834            const char *target;
2835
2836            if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
2837                xs *old_actor = NULL;
2838
2839                if (valid_status(actor_get(target, &old_actor))) {
2840                    xs_html_add(entry,
2841                        xs_html_tag("div",
2842                            xs_html_attr("class", "snac-post"),
2843                            html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy)));
2844                }
2845            }
2846        }
2847        else
2848        if (obj != NULL) {
2849            xs *md5 = xs_md5_hex(id, strlen(id));
2850
2851            xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
2852
2853            if (h != NULL) {
2854                xs_html_add(entry,
2855                    h);
2856            }
2857        }
2858
2859        if (strcmp(v, n_time) > 0) {
2860            /* unseen notification */
2861            if (noti_new == NULL) {
2862                noti_new = xs_html_tag("div",
2863                    xs_html_tag("h2",
2864                        xs_html_attr("class", "snac-header"),
2865                        xs_html_text(L("New"))));
2866
2867                xs_html_add(posts,
2868                    noti_new);
2869            }
2870
2871            xs_html_add(noti_new,
2872                entry);
2873        }
2874        else {
2875            /* already seen notification */
2876            if (noti_seen == NULL) {
2877                noti_seen = xs_html_tag("div",
2878                    xs_html_tag("h2",
2879                        xs_html_attr("class", "snac-header"),
2880                        xs_html_text(L("Already seen"))));
2881
2882                xs_html_add(posts,
2883                    noti_seen);
2884            }
2885
2886            xs_html_add(noti_seen,
2887                entry);
2888        }
2889    }
2890
2891    if (noti_new == NULL && noti_seen == NULL)
2892        xs_html_add(body,
2893            xs_html_tag("h2",
2894                xs_html_attr("class", "snac-header"),
2895                xs_html_text(L("None"))));
2896
2897    /* add the navigation footer */
2898    xs *next_p = notify_list(user, skip + show, 1);
2899    if (xs_list_len(next_p)) {
2900        xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
2901            user->actor, skip + show, show);
2902
2903        xs_html_add(body,
2904            xs_html_tag("p",
2905                xs_html_tag("a",
2906                    xs_html_attr("href", url),
2907                    xs_html_text(L("More...")))));
2908    }
2909
2910    xs_html_add(body,
2911        html_footer());
2912
2913    /* set the check time to now */
2914    xs *dummy = notify_check_time(user, 1);
2915    dummy = xs_free(dummy);
2916
2917    timeline_touch(user);
2918
2919    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2920}
2921
2922
2923int html_get_handler(const xs_dict *req, const char *q_path,
2924                     char **body, int *b_size, char **ctype,
2925                     xs_str **etag, xs_str **last_modified)
2926{
2927    const char *accept = xs_dict_get(req, "accept");
2928    int status = HTTP_STATUS_NOT_FOUND;
2929    snac snac;
2930    xs *uid = NULL;
2931    const char *p_path;
2932    int cache = 1;
2933    int save = 1;
2934    int proxy = 0;
2935    const char *v;
2936
2937    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
2938
2939    xs *l = xs_split_n(q_path, "/", 2);
2940    v = xs_list_get(l, 1);
2941
2942    if (xs_is_null(v)) {
2943        srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
2944        return HTTP_STATUS_NOT_FOUND;
2945    }
2946
2947    if (strcmp(v, "share-bridge") == 0) {
2948        /* temporary redirect for a post */
2949        const char *login = xs_dict_get(q_vars, "login");
2950        const char *content = xs_dict_get(q_vars, "content");
2951
2952        if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
2953            xs *b64 = xs_base64_enc(content, strlen(content));
2954
2955            srv_log(xs_fmt("share-bridge for user '%s'", login));
2956
2957            *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
2958            return HTTP_STATUS_SEE_OTHER;
2959        }
2960        else
2961            return HTTP_STATUS_NOT_FOUND;
2962    }
2963
2964    uid = xs_dup(v);
2965
2966    /* rss extension? */
2967    if (xs_endswith(uid, ".rss")) {
2968        uid = xs_crop_i(uid, 0, -4);
2969        p_path = ".rss";
2970    }
2971    else
2972        p_path = xs_list_get(l, 2);
2973
2974    if (!uid || !user_open(&snac, uid)) {
2975        /* invalid user */
2976        srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
2977        return HTTP_STATUS_NOT_FOUND;
2978    }
2979
2980    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2981        proxy = 1;
2982
2983    /* return the RSS if requested by Accept header */
2984    if (accept != NULL) {
2985        if (xs_str_in(accept, "text/xml") != -1 ||
2986            xs_str_in(accept, "application/rss+xml") != -1)
2987            p_path = ".rss";
2988    }
2989
2990    /* check if server config variable 'disable_cache' is set */
2991    if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
2992        cache = 0;
2993
2994    int skip = 0;
2995    int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
2996    int show = def_show;
2997
2998    if ((v = xs_dict_get(q_vars, "skip")) != NULL)
2999        skip = atoi(v), cache = 0, save = 0;
3000    if ((v = xs_dict_get(q_vars, "show")) != NULL)
3001        show = atoi(v), cache = 0, save = 0;
3002    if ((v = xs_dict_get(q_vars, "da")) != NULL) {
3003        /* user dismissed an announcement */
3004        if (login(&snac, req)) {
3005            double ts = atof(v);
3006            xs *timestamp = xs_number_new(ts);
3007            srv_log(xs_fmt("user dismissed announcements until %d", ts));
3008            snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
3009            user_persist(&snac, 0);
3010        }
3011    }
3012
3013    /* get a possible error message */
3014    const char *error = xs_dict_get(q_vars, "error");
3015    if (error != NULL)
3016        cache = 0;
3017
3018    /* a show of 0 has no sense */
3019    if (show == 0)
3020        show = def_show;
3021
3022    if (p_path == NULL) { /** public timeline **/
3023        xs *h = xs_str_localtime(0, "%Y-%m.html");
3024
3025        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
3026            /** empty public timeline for private users **/
3027            *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error);
3028            *b_size = strlen(*body);
3029            status  = HTTP_STATUS_OK;
3030        }
3031        else
3032        if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
3033            snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
3034
3035            status = history_get(&snac, h, body, b_size,
3036                        xs_dict_get(req, "if-none-match"), etag);
3037        }
3038        else {
3039            xs *list = NULL;
3040            xs *next = NULL;
3041
3042            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) {
3043                list = timeline_simple_list(&snac, "public", skip, show);
3044                next = timeline_simple_list(&snac, "public", skip + show, 1);
3045            }
3046            else {
3047                list = timeline_list(&snac, "public", skip, show);
3048                next = timeline_list(&snac, "public", skip + show, 1);
3049            }
3050
3051            xs *pins = pinned_list(&snac);
3052            pins = xs_list_cat(pins, list);
3053
3054            *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error);
3055
3056            *b_size = strlen(*body);
3057            status  = HTTP_STATUS_OK;
3058
3059            if (save)
3060                history_add(&snac, h, *body, *b_size, etag);
3061        }
3062    }
3063    else
3064    if (strcmp(p_path, "admin") == 0) { /** private timeline **/
3065        if (!login(&snac, req)) {
3066            *body  = xs_dup(uid);
3067            status = HTTP_STATUS_UNAUTHORIZED;
3068        }
3069        else {
3070            const char *q = xs_dict_get(q_vars, "q");
3071            xs *url_acct = NULL;
3072
3073            /* searching for an URL? */
3074            if (q && xs_match(q, "https://*|http://*")) {
3075                /* may by an actor; try a webfinger */
3076                xs *actor_obj = NULL;
3077
3078                if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) {
3079                    /* it's an actor; do the dirty trick of changing q to the account name */
3080                    q = url_acct;
3081                }
3082                else {
3083                    /* bring it to the user's timeline */
3084                    xs *object = NULL;
3085                    int status;
3086
3087                    status = activitypub_request(&snac, q, &object);
3088                    snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
3089
3090                    if (valid_status(status)) {
3091                        /* got it; also request the actor */
3092                        const char *attr_to = get_atto(object);
3093
3094                        if (!xs_is_null(attr_to)) {
3095                            status = actor_request(&snac, attr_to, &actor_obj);
3096
3097                            if (valid_status(status)) {
3098                                /* reset the query string to be the real id */
3099                                url_acct = xs_dup(xs_dict_get(object, "id"));
3100                                q = url_acct;
3101
3102                                /* add the post to the timeline */
3103                                xs *md5 = xs_md5_hex(q, strlen(q));
3104
3105                                if (!timeline_here(&snac, md5))
3106                                    timeline_add(&snac, q, object);
3107                            }
3108                        }
3109                    }
3110                }
3111
3112                /* fall through */
3113            }
3114
3115            if (q && *q) {
3116                if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
3117                    /** search account **/
3118                    xs *actor = NULL;
3119                    xs *acct = NULL;
3120                    xs *l = xs_list_new();
3121                    xs_html *page = NULL;
3122
3123                    if (valid_status(webfinger_request(q, &actor, &acct))) {
3124                        xs *actor_obj = NULL;
3125
3126                        if (valid_status(actor_request(&snac, actor, &actor_obj))) {
3127                            actor_add(actor, actor_obj);
3128
3129                            /* create a people list with only one element */
3130                            l = xs_list_append(xs_list_new(), actor);
3131
3132                            xs *title = xs_fmt(L("Search results for account %s"), q);
3133
3134                            page = html_people_list(&snac, l, title, "wf", NULL);
3135                        }
3136                    }
3137
3138                    if (page == NULL) {
3139                        xs *title = xs_fmt(L("Account %s not found"), q);
3140
3141                        page = xs_html_tag("div",
3142                            xs_html_tag("h2",
3143                                xs_html_attr("class", "snac-header"),
3144                                xs_html_text(title)));
3145                    }
3146
3147                    xs_html *html = xs_html_tag("html",
3148                        html_user_head(&snac, NULL, NULL),
3149                        xs_html_add(html_user_body(&snac, 0),
3150                        page,
3151                        html_footer()));
3152
3153                    *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
3154                    *b_size = strlen(*body);
3155                    status = HTTP_STATUS_OK;
3156                }
3157                else
3158                if (*q == '#') {
3159                    /** search by tag **/
3160                    xs *tl = tag_search(q, skip, show + 1);
3161                    int more = 0;
3162                    if (xs_list_len(tl) >= show + 1) {
3163                        /* drop the last one */
3164                        tl = xs_list_del(tl, -1);
3165                        more = 1;
3166                    }
3167
3168                    xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
3169                    xs *title = xs_fmt(xs_list_len(tl) ?
3170                        L("Search results for tag %s") : L("Nothing found for tag %s"), q);
3171
3172                    *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error);
3173                    *b_size = strlen(*body);
3174                    status  = HTTP_STATUS_OK;
3175                }
3176                else {
3177                    /** search by content **/
3178                    int to = 0;
3179                    int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
3180                    xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
3181                    xs *title = NULL;
3182                    xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
3183                    int tl_len = xs_list_len(tl);
3184
3185                    if (to)
3186                        title = xs_fmt(L("Search results for '%s' (may be more)"), q);
3187                    else
3188                    if (tl_len)
3189                        title = xs_fmt(L("Search results for '%s'"), q);
3190                    else
3191                    if (skip)
3192                        title = xs_fmt(L("No more matches for '%s'"), q);
3193                    else
3194                        title = xs_fmt(L("Nothing found for '%s'"), q);
3195
3196                    *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
3197                                            title, page, 0, error);
3198                    *b_size = strlen(*body);
3199                    status  = HTTP_STATUS_OK;
3200                }
3201            }
3202            else {
3203                double t = history_mtime(&snac, "timeline.html_");
3204
3205                /* if enabled by admin, return a cached page if its timestamp is:
3206                   a) newer than the timeline timestamp
3207                   b) newer than the start time of the server
3208                */
3209                if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
3210                    snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
3211
3212                    status = history_get(&snac, "timeline.html_", body, b_size,
3213                                xs_dict_get(req, "if-none-match"), etag);
3214                }
3215                else {
3216                    snac_debug(&snac, 1, xs_fmt("building timeline"));
3217
3218                    xs *list = timeline_list(&snac, "private", skip, show);
3219                    xs *next = timeline_list(&snac, "private", skip + show, 1);
3220
3221                    *body = html_timeline(&snac, list, 0, skip, show,
3222                            xs_list_len(next), NULL, "/admin", 1, error);
3223
3224                    *b_size = strlen(*body);
3225                    status  = HTTP_STATUS_OK;
3226
3227                    if (save)
3228                        history_add(&snac, "timeline.html_", *body, *b_size, etag);
3229                }
3230            }
3231        }
3232    }
3233    else
3234    if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
3235        if (!login(&snac, req)) {
3236            *body  = xs_dup(uid);
3237            status = HTTP_STATUS_UNAUTHORIZED;
3238        }
3239        else {
3240            xs *l = xs_split(p_path, "/");
3241            const char *md5 = xs_list_get(l, -1);
3242
3243            if (md5 && *md5 && timeline_here(&snac, md5)) {
3244                xs *list = xs_list_append(xs_list_new(), md5);
3245
3246                *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error);
3247                *b_size = strlen(*body);
3248                status  = HTTP_STATUS_OK;
3249            }
3250        }
3251    }
3252    else
3253    if (strcmp(p_path, "people") == 0) { /** the list of people **/
3254        if (!login(&snac, req)) {
3255            *body  = xs_dup(uid);
3256            status = HTTP_STATUS_UNAUTHORIZED;
3257        }
3258        else {
3259            *body   = html_people(&snac);
3260            *b_size = strlen(*body);
3261            status  = HTTP_STATUS_OK;
3262        }
3263    }
3264    else
3265    if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
3266        if (!login(&snac, req)) {
3267            *body  = xs_dup(uid);
3268            status = HTTP_STATUS_UNAUTHORIZED;
3269        }
3270        else {
3271            *body   = html_notifications(&snac, skip, show);
3272            *b_size = strlen(*body);
3273            status  = HTTP_STATUS_OK;
3274        }
3275    }
3276    else
3277    if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
3278        if (!login(&snac, req)) {
3279            *body  = xs_dup(uid);
3280            status = HTTP_STATUS_UNAUTHORIZED;
3281        }
3282        else {
3283            xs *list = timeline_instance_list(skip, show);
3284            xs *next = timeline_instance_list(skip + show, 1);
3285
3286            *body = html_timeline(&snac, list, 0, skip, show,
3287                xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error);
3288            *b_size = strlen(*body);
3289            status  = HTTP_STATUS_OK;
3290        }
3291    }
3292    else
3293    if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
3294        if (!login(&snac, req)) {
3295            *body  = xs_dup(uid);
3296            status = HTTP_STATUS_UNAUTHORIZED;
3297        }
3298        else {
3299            xs *list = pinned_list(&snac);
3300
3301            *body = html_timeline(&snac, list, 0, skip, show,
3302                0, L("Pinned posts"), "", 0, error);
3303            *b_size = strlen(*body);
3304            status  = HTTP_STATUS_OK;
3305        }
3306    }
3307    else
3308    if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
3309        if (!login(&snac, req)) {
3310            *body  = xs_dup(uid);
3311            status = HTTP_STATUS_UNAUTHORIZED;
3312        }
3313        else {
3314            xs *list = bookmark_list(&snac);
3315
3316            *body = html_timeline(&snac, list, 0, skip, show,
3317                0, L("Bookmarked posts"), "", 0, error);
3318            *b_size = strlen(*body);
3319            status  = HTTP_STATUS_OK;
3320        }
3321    }
3322    else
3323    if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
3324        if (!login(&snac, req)) {
3325            *body  = xs_dup(uid);
3326            status = HTTP_STATUS_UNAUTHORIZED;
3327        }
3328        else {
3329            xs *list = draft_list(&snac);
3330
3331            *body = html_timeline(&snac, list, 0, skip, show,
3332                0, L("Post drafts"), "", 0, error);
3333            *b_size = strlen(*body);
3334            status  = HTTP_STATUS_OK;
3335        }
3336    }
3337    else
3338    if (xs_startswith(p_path, "list/")) { /** list timelines **/
3339        if (!login(&snac, req)) {
3340            *body  = xs_dup(uid);
3341            status = HTTP_STATUS_UNAUTHORIZED;
3342        }
3343        else {
3344            xs *l = xs_split(p_path, "/");
3345            const char *lid = xs_list_get(l, -1);
3346
3347            xs *list = list_timeline(&snac, lid, skip, show);
3348            xs *next = list_timeline(&snac, lid, skip + show, 1);
3349
3350            if (list != NULL) {
3351                xs *ttl = timeline_top_level(&snac, list);
3352
3353                xs *base = xs_fmt("/list/%s", lid);
3354                xs *name = list_maint(&snac, lid, 3);
3355                xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
3356
3357                *body = html_timeline(&snac, ttl, 0, skip, show,
3358                    xs_list_len(next), title, base, 1, error);
3359                *b_size = strlen(*body);
3360                status  = HTTP_STATUS_OK;
3361            }
3362        }
3363    }
3364    else
3365    if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
3366        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3367            return HTTP_STATUS_FORBIDDEN;
3368
3369        xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
3370        xs *msg = NULL;
3371
3372        if (valid_status(object_get(id, &msg))) {
3373            xs *md5  = xs_md5_hex(id, strlen(id));
3374            xs *list = xs_list_new();
3375
3376            list = xs_list_append(list, md5);
3377
3378            *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error);
3379            *b_size = strlen(*body);
3380            status  = HTTP_STATUS_OK;
3381        }
3382    }
3383    else
3384    if (xs_startswith(p_path, "s/")) { /** a static file **/
3385        xs *l    = xs_split(p_path, "/");
3386        const char *id = xs_list_get(l, 1);
3387        int sz;
3388
3389        if (id && *id) {
3390            status = static_get(&snac, id, body, &sz,
3391                        xs_dict_get(req, "if-none-match"), etag);
3392
3393            if (valid_status(status)) {
3394                *b_size = sz;
3395                *ctype  = (char *)xs_mime_by_ext(id);
3396            }
3397        }
3398    }
3399    else
3400    if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
3401        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3402            return HTTP_STATUS_FORBIDDEN;
3403
3404        if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
3405            return HTTP_STATUS_FORBIDDEN;
3406
3407        xs *l = xs_split(p_path, "/");
3408        const char *id = xs_list_get(l, 1);
3409
3410        if (id && *id) {
3411            if (xs_endswith(id, "timeline.html_")) {
3412                /* Don't let them in */
3413                *b_size = 0;
3414                status = HTTP_STATUS_NOT_FOUND;
3415            }
3416            else
3417                status = history_get(&snac, id, body, b_size,
3418                            xs_dict_get(req, "if-none-match"), etag);
3419        }
3420    }
3421    else
3422    if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
3423        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3424            return HTTP_STATUS_FORBIDDEN;
3425
3426        int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
3427
3428        xs *elems = timeline_simple_list(&snac, "public", 0, cnt);
3429        xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
3430
3431        xs *rss_title = xs_fmt("%s (@%s@%s)",
3432            xs_dict_get(snac.config, "name"),
3433            snac.uid,
3434            xs_dict_get(srv_config, "host"));
3435        xs *rss_link = xs_fmt("%s.rss", snac.actor);
3436
3437        *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
3438        *b_size = strlen(*body);
3439        *ctype  = "application/rss+xml; charset=utf-8";
3440        status  = HTTP_STATUS_OK;
3441
3442        snac_debug(&snac, 1, xs_fmt("serving RSS"));
3443    }
3444    else
3445    if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
3446        xs *proxy_prefix = NULL;
3447
3448        if (xs_startswith(p_path, "x/")) {
3449            /* proxy usage authorized by http basic auth */
3450            if (login(&snac, req))
3451                proxy_prefix = xs_str_new("x/");
3452            else {
3453                *body  = xs_dup(uid);
3454                status = HTTP_STATUS_UNAUTHORIZED;
3455            }
3456        }
3457        else {
3458            /* proxy usage authorized by proxy_token */
3459            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
3460            xs *tk = xs_md5_hex(tks, strlen(tks));
3461            xs *p = xs_fmt("y/%s/", tk);
3462
3463            if (xs_startswith(p_path, p))
3464                proxy_prefix = xs_dup(p);
3465        }
3466
3467        if (proxy_prefix) {
3468            /* pick the raw path (including optional ? arguments) */
3469            const char *raw_path = xs_dict_get(req, "raw_path");
3470
3471            /* skip to where the proxy/ string starts */
3472            raw_path += xs_str_in(raw_path, proxy_prefix);
3473
3474            xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
3475            xs *hdrs = xs_dict_new();
3476
3477            hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
3478
3479            const char *ims = xs_dict_get(req, "if-modified-since");
3480            const char *inm = xs_dict_get(req, "if-none-match");
3481
3482            if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
3483            if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
3484
3485            xs *rsp = xs_http_request("GET", url, hdrs,
3486                        NULL, 0, &status, body, b_size, 0);
3487
3488            if (valid_status(status)) {
3489                const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
3490                const char *lm = xs_dict_get(rsp, "last-modified");
3491                const char *et = xs_dict_get(rsp, "etag");
3492
3493                if (lm) *last_modified = xs_dup(lm);
3494                if (et) *etag = xs_dup(et);
3495
3496                /* find the content-type in the static mime types,
3497                   and return that value instead of ct, which will
3498                   be destroyed when out of scope */
3499                for (int n = 0; xs_mime_types[n]; n += 2) {
3500                    if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
3501                        *ctype = (char *)xs_mime_types[n + 1];
3502                        break;
3503                    }
3504                }
3505            }
3506
3507            snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
3508        }
3509    }
3510    else
3511    if (strcmp(p_path, "share") == 0) { /** direct post **/
3512        if (!login(&snac, req)) {
3513            *body  = xs_dup(uid);
3514            status = HTTP_STATUS_UNAUTHORIZED;
3515        }
3516        else {
3517            const char *b64 = xs_dict_get(q_vars, "content");
3518            int sz;
3519            xs *content = xs_base64_dec(b64, &sz);
3520            xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL);
3521            xs *c_msg = msg_create(&snac, msg);
3522
3523            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
3524
3525            enqueue_message(&snac, c_msg);
3526
3527            snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
3528
3529            *body   = xs_fmt("%s/admin", snac.actor);
3530            *b_size = strlen(*body);
3531            status  = HTTP_STATUS_SEE_OTHER;
3532        }
3533    }
3534    else
3535        status = HTTP_STATUS_NOT_FOUND;
3536
3537    user_free(&snac);
3538
3539    if (valid_status(status) && *ctype == NULL) {
3540        *ctype = "text/html; charset=utf-8";
3541    }
3542
3543    return status;
3544}
3545
3546
3547int html_post_handler(const xs_dict *req, const char *q_path,
3548                      char *payload, int p_size,
3549                      char **body, int *b_size, char **ctype)
3550{
3551    (void)p_size;
3552    (void)ctype;
3553
3554    int status = 0;
3555    snac snac;
3556    const char *uid;
3557    const char *p_path;
3558    const xs_dict *p_vars;
3559
3560    xs *l = xs_split_n(q_path, "/", 2);
3561
3562    uid = xs_list_get(l, 1);
3563    if (!uid || !user_open(&snac, uid)) {
3564        /* invalid user */
3565        srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
3566        return HTTP_STATUS_NOT_FOUND;
3567    }
3568
3569    p_path = xs_list_get(l, 2);
3570
3571    /* all posts must be authenticated */
3572    if (!login(&snac, req)) {
3573        user_free(&snac);
3574        *body  = xs_dup(uid);
3575        return HTTP_STATUS_UNAUTHORIZED;
3576    }
3577
3578    p_vars = xs_dict_get(req, "p_vars");
3579
3580    if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
3581        snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
3582
3583        /* post note */
3584        const xs_str *content      = xs_dict_get(p_vars, "content");
3585        const xs_str *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
3586        const xs_str *attach_url   = xs_dict_get(p_vars, "attach_url");
3587        const xs_list *attach_file = xs_dict_get(p_vars, "attach");
3588        const xs_str *to           = xs_dict_get(p_vars, "to");
3589        const xs_str *sensitive    = xs_dict_get(p_vars, "sensitive");
3590        const xs_str *summary      = xs_dict_get(p_vars, "summary");
3591        const xs_str *edit_id      = xs_dict_get(p_vars, "edit_id");
3592        const xs_str *alt_text     = xs_dict_get(p_vars, "alt_text");
3593        int priv             = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
3594        int store_as_draft   = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
3595        xs *attach_list      = xs_list_new();
3596
3597        /* default alt text */
3598        if (xs_is_null(alt_text))
3599            alt_text = "";
3600
3601        /* is attach_url set? */
3602        if (!xs_is_null(attach_url) && *attach_url != '\0') {
3603            xs *l = xs_list_new();
3604
3605            l = xs_list_append(l, attach_url);
3606            l = xs_list_append(l, alt_text);
3607
3608            attach_list = xs_list_append(attach_list, l);
3609        }
3610
3611        /* is attach_file set? */
3612        if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) {
3613            const char *fn = xs_list_get(attach_file, 0);
3614
3615            if (*fn != '\0') {
3616                char *ext = strrchr(fn, '.');
3617                xs *hash  = xs_md5_hex(fn, strlen(fn));
3618                xs *id    = xs_fmt("%s%s", hash, ext);
3619                xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
3620                int fo    = xs_number_get(xs_list_get(attach_file, 1));
3621                int fs    = xs_number_get(xs_list_get(attach_file, 2));
3622
3623                /* store */
3624                static_put(&snac, id, payload + fo, fs);
3625
3626                xs *l = xs_list_new();
3627
3628                l = xs_list_append(l, url);
3629                l = xs_list_append(l, alt_text);
3630
3631                attach_list = xs_list_append(attach_list, l);
3632            }
3633        }
3634
3635        if (content != NULL) {
3636            xs *msg       = NULL;
3637            xs *c_msg     = NULL;
3638            xs *content_2 = xs_replace(content, "\r", "");
3639            xs *poll_opts = NULL;
3640
3641            /* is there a valid set of poll options? */
3642            const char *v = xs_dict_get(p_vars, "poll_options");
3643            if (!xs_is_null(v) && *v) {
3644                xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
3645
3646                poll_opts = xs_split(v2, "\n");
3647            }
3648
3649            if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
3650                /* get the rest of poll configuration */
3651                const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
3652                const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
3653                int multiple = 0;
3654
3655                int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
3656
3657                if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
3658                    multiple = 1;
3659
3660                msg = msg_question(&snac, content_2, attach_list,
3661                                   poll_opts, multiple, end_secs);
3662
3663                enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
3664            }
3665            else
3666                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv, NULL);
3667
3668            if (sensitive != NULL) {
3669                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
3670                msg = xs_dict_set(msg, "summary",   xs_is_null(summary) ? "..." : summary);
3671            }
3672
3673            if (xs_is_null(edit_id)) {
3674                /* new message */
3675                const char *id = xs_dict_get(msg, "id");
3676
3677                if (store_as_draft) {
3678                    draft_add(&snac, id, msg);
3679                }
3680                else {
3681                    c_msg = msg_create(&snac, msg);
3682                    timeline_add(&snac, id, msg);
3683                }
3684            }
3685            else {
3686                /* an edition of a previous message */
3687                xs *p_msg = NULL;
3688
3689                if (valid_status(object_get(edit_id, &p_msg))) {
3690                    /* copy relevant fields from previous version */
3691                    char *fields[] = { "id", "context", "url", "published",
3692                                       "to", "inReplyTo", NULL };
3693                    int n;
3694
3695                    for (n = 0; fields[n]; n++) {
3696                        const char *v = xs_dict_get(p_msg, fields[n]);
3697                        msg = xs_dict_set(msg, fields[n], v);
3698                    }
3699
3700                    if (store_as_draft) {
3701                        draft_add(&snac, edit_id, msg);
3702                    }
3703                    else
3704                    if (is_draft(&snac, edit_id)) {
3705                        /* message was previously a draft; it's a create activity */
3706
3707                        /* set the published field to now */
3708                        xs *published = xs_str_utctime(0, ISO_DATE_SPEC);
3709                        msg = xs_dict_set(msg, "published", published);
3710
3711                        /* overwrite object */
3712                        object_add_ow(edit_id, msg);
3713
3714                        c_msg = msg_create(&snac, msg);
3715                        timeline_add(&snac, edit_id, msg);
3716                        draft_del(&snac, edit_id);
3717                    }
3718                    else {
3719                        /* set the updated field */
3720                        xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3721                        msg = xs_dict_set(msg, "updated", updated);
3722
3723                        /* overwrite object, not updating the indexes */
3724                        object_add_ow(edit_id, msg);
3725
3726                        /* update message */
3727                        c_msg = msg_update(&snac, msg);
3728                    }
3729                }
3730                else
3731                    snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
3732            }
3733
3734            if (c_msg != NULL)
3735                enqueue_message(&snac, c_msg);
3736
3737            history_del(&snac, "timeline.html_");
3738        }
3739
3740        status = HTTP_STATUS_SEE_OTHER;
3741    }
3742    else
3743    if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
3744        /* action on an entry */
3745        const char *id     = xs_dict_get(p_vars, "id");
3746        const char *actor  = xs_dict_get(p_vars, "actor");
3747        const char *action = xs_dict_get(p_vars, "action");
3748        const char *group  = xs_dict_get(p_vars, "group");
3749
3750        if (action == NULL)
3751            return HTTP_STATUS_NOT_FOUND;
3752
3753        snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
3754
3755        status = HTTP_STATUS_SEE_OTHER;
3756
3757        if (strcmp(action, L("Like")) == 0) { /** **/
3758            xs *msg = msg_admiration(&snac, id, "Like");
3759
3760            if (msg != NULL) {
3761                enqueue_message(&snac, msg);
3762                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1);
3763            }
3764        }
3765        else
3766        if (strcmp(action, L("Boost")) == 0) { /** **/
3767            xs *msg = msg_admiration(&snac, id, "Announce");
3768
3769            if (msg != NULL) {
3770                enqueue_message(&snac, msg);
3771                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
3772            }
3773        }
3774        else
3775        if (strcmp(action, L("Unlike")) == 0) { /** **/
3776            xs *msg = msg_repulsion(&snac, id, "Like");
3777
3778            if (msg != NULL) {
3779                enqueue_message(&snac, msg);
3780            }
3781        }
3782        else
3783        if (strcmp(action, L("Unboost")) == 0) { /** **/
3784            xs *msg = msg_repulsion(&snac, id, "Announce");
3785
3786            if (msg != NULL) {
3787                enqueue_message(&snac, msg);
3788            }
3789        }
3790        else
3791        if (strcmp(action, L("MUTE")) == 0) { /** **/
3792            mute(&snac, actor);
3793        }
3794        else
3795        if (strcmp(action, L("Unmute")) == 0) { /** **/
3796            unmute(&snac, actor);
3797        }
3798        else
3799        if (strcmp(action, L("Hide")) == 0) { /** **/
3800            if (is_draft(&snac, id))
3801                draft_del(&snac, id);
3802            else
3803                hide(&snac, id);
3804        }
3805        else
3806        if (strcmp(action, L("Limit")) == 0) { /** **/
3807            limit(&snac, actor);
3808        }
3809        else
3810        if (strcmp(action, L("Unlimit")) == 0) { /** **/
3811            unlimit(&snac, actor);
3812        }
3813        else
3814        if (strcmp(action, L("Follow")) == 0) { /** **/
3815            xs *msg = msg_follow(&snac, actor);
3816
3817            if (msg != NULL) {
3818                /* reload the actor from the message, in may be different */
3819                actor = xs_dict_get(msg, "object");
3820
3821                following_add(&snac, actor, msg);
3822
3823                enqueue_output_by_actor(&snac, msg, actor, 0);
3824            }
3825        }
3826        else
3827        if (strcmp(action, L("Unfollow")) == 0) { /** **/
3828            /* get the following object */
3829            xs *object = NULL;
3830
3831            if (valid_status(following_get(&snac, actor, &object))) {
3832                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3833
3834                following_del(&snac, actor);
3835
3836                enqueue_output_by_actor(&snac, msg, actor, 0);
3837
3838                snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
3839            }
3840            else
3841                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
3842        }
3843        else
3844        if (strcmp(action, L("Follow Group")) == 0) { /** **/
3845            xs *msg = msg_follow(&snac, group);
3846
3847            if (msg != NULL) {
3848                /* reload the group from the message, in may be different */
3849                group = xs_dict_get(msg, "object");
3850
3851                following_add(&snac, group, msg);
3852
3853                enqueue_output_by_actor(&snac, msg, group, 0);
3854            }
3855        }
3856        else
3857        if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
3858            /* get the following object */
3859            xs *object = NULL;
3860
3861            if (valid_status(following_get(&snac, group, &object))) {
3862                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3863
3864                following_del(&snac, group);
3865
3866                enqueue_output_by_actor(&snac, msg, group, 0);
3867
3868                snac_log(&snac, xs_fmt("unfollowed group %s", group));
3869            }
3870            else
3871                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
3872        }
3873        else
3874        if (strcmp(action, L("Delete")) == 0) { /** **/
3875            const char *actor_form = xs_dict_get(p_vars, "actor-form");
3876            if (actor_form != NULL) {
3877                /* delete follower */
3878                if (valid_status(follower_del(&snac, actor)))
3879                    snac_log(&snac, xs_fmt("deleted follower %s", actor));
3880                else
3881                    snac_log(&snac, xs_fmt("error deleting follower %s", actor));
3882            }
3883            else {
3884                /* delete an entry */
3885                if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) {
3886                    /* it's a post by us: generate a delete */
3887                    xs *msg = msg_delete(&snac, id);
3888
3889                    enqueue_message(&snac, msg);
3890
3891                    snac_log(&snac, xs_fmt("posted tombstone for %s", id));
3892                }
3893
3894                timeline_del(&snac, id);
3895
3896                draft_del(&snac, id);
3897
3898                snac_log(&snac, xs_fmt("deleted entry %s", id));
3899            }
3900        }
3901        else
3902        if (strcmp(action, L("Pin")) == 0) { /** **/
3903            pin(&snac, id);
3904            timeline_touch(&snac);
3905        }
3906        else
3907        if (strcmp(action, L("Unpin")) == 0) { /** **/
3908            unpin(&snac, id);
3909            timeline_touch(&snac);
3910        }
3911        else
3912        if (strcmp(action, L("Bookmark")) == 0) { /** **/
3913            bookmark(&snac, id);
3914            timeline_touch(&snac);
3915        }
3916        else
3917        if (strcmp(action, L("Unbookmark")) == 0) { /** **/
3918            unbookmark(&snac, id);
3919            timeline_touch(&snac);
3920        }
3921        else
3922        if (strcmp(action, L("Approve")) == 0) { /** **/
3923            xs *fwreq = pending_get(&snac, actor);
3924
3925            if (fwreq != NULL) {
3926                xs *reply = msg_accept(&snac, fwreq, actor);
3927
3928                enqueue_message(&snac, reply);
3929
3930                if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3931                    /* add a date if it doesn't include one (Mastodon) */
3932                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3933                    fwreq = xs_dict_set(fwreq, "published", date);
3934                }
3935
3936                timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3937
3938                follower_add(&snac, actor);
3939
3940                pending_del(&snac, actor);
3941
3942                snac_log(&snac, xs_fmt("new follower %s", actor));
3943            }
3944        }
3945        else
3946        if (strcmp(action, L("Discard")) == 0) { /** **/
3947            pending_del(&snac, actor);
3948        }
3949        else
3950            status = HTTP_STATUS_NOT_FOUND;
3951
3952        /* delete the cached timeline */
3953        if (status == HTTP_STATUS_SEE_OTHER)
3954            history_del(&snac, "timeline.html_");
3955    }
3956    else
3957    if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
3958        /* change of user data */
3959        const char *v;
3960        const char *p1, *p2;
3961
3962        if ((v = xs_dict_get(p_vars, "name")) != NULL)
3963            snac.config = xs_dict_set(snac.config, "name", v);
3964        if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
3965            snac.config = xs_dict_set(snac.config, "avatar", v);
3966        if ((v = xs_dict_get(p_vars, "bio")) != NULL)
3967            snac.config = xs_dict_set(snac.config, "bio", v);
3968        if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
3969            strcmp(v, "on") == 0) {
3970            snac.config = xs_dict_set(snac.config, "cw", "open");
3971        } else { /* if the checkbox is not set, the parameter is missing */
3972            snac.config = xs_dict_set(snac.config, "cw", "");
3973        }
3974        if ((v = xs_dict_get(p_vars, "email")) != NULL)
3975            snac.config = xs_dict_set(snac.config, "email", v);
3976        if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
3977            snac.config = xs_dict_set(snac.config, "telegram_bot", v);
3978        if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
3979            snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
3980        if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
3981            snac.config = xs_dict_set(snac.config, "ntfy_server", v);
3982        if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
3983            snac.config = xs_dict_set(snac.config, "ntfy_token", v);
3984        if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
3985            xs *days    = xs_number_new(atof(v));
3986            snac.config = xs_dict_set(snac.config, "purge_days", days);
3987        }
3988        if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
3989            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
3990        else
3991            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
3992        if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
3993            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
3994        else
3995            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
3996        if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
3997            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
3998        else
3999            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
4000        if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
4001            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
4002        else
4003            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
4004        if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
4005            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
4006        else
4007            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
4008        if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
4009            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
4010        else
4011            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
4012        if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
4013            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
4014        else
4015            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
4016
4017        if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
4018            snac.config = xs_dict_set(snac.config, "metadata", v);
4019
4020        /* uploads */
4021        const char *uploads[] = { "avatar", "header", NULL };
4022        int n;
4023        for (n = 0; uploads[n]; n++) {
4024            xs *var_name = xs_fmt("%s_file", uploads[n]);
4025
4026            const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
4027            if (xs_type(uploaded_file) == XSTYPE_LIST) {
4028                const char *fn = xs_list_get(uploaded_file, 0);
4029
4030                if (fn && *fn) {
4031                    const char *mimetype = xs_mime_by_ext(fn);
4032
4033                    if (xs_startswith(mimetype, "image/")) {
4034                        const char *ext = strrchr(fn, '.');
4035                        xs *hash        = xs_md5_hex(fn, strlen(fn));
4036                        xs *id          = xs_fmt("%s%s", hash, ext);
4037                        xs *url         = xs_fmt("%s/s/%s", snac.actor, id);
4038                        int fo          = xs_number_get(xs_list_get(uploaded_file, 1));
4039                        int fs          = xs_number_get(xs_list_get(uploaded_file, 2));
4040
4041                        /* store */
4042                        static_put(&snac, id, payload + fo, fs);
4043
4044                        snac.config = xs_dict_set(snac.config, uploads[n], url);
4045                    }
4046                }
4047            }
4048        }
4049
4050        /* delete images by removing url from user.json */
4051        for (n = 0; uploads[n]; n++) {
4052            xs *var_name = xs_fmt("%s_delete", uploads[n]);
4053            const char *delete_var = xs_dict_get(p_vars, var_name);
4054
4055            if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
4056                    snac.config = xs_dict_set(snac.config, uploads[n], "");
4057            }
4058        }
4059
4060        /* password change? */
4061        if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
4062            (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
4063            *p1 && strcmp(p1, p2) == 0) {
4064            xs *pw = hash_password(snac.uid, p1, NULL);
4065            snac.config = xs_dict_set(snac.config, "passwd", pw);
4066        }
4067
4068        user_persist(&snac, 1);
4069
4070        status = HTTP_STATUS_SEE_OTHER;
4071    }
4072    else
4073    if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
4074        notify_clear(&snac);
4075        timeline_touch(&snac);
4076
4077        status = HTTP_STATUS_SEE_OTHER;
4078    }
4079    else
4080    if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
4081        const char *irt   = xs_dict_get(p_vars, "irt");
4082        const char *opt   = xs_dict_get(p_vars, "question");
4083        const char *actor = xs_dict_get(p_vars, "actor");
4084
4085        xs *ls = NULL;
4086
4087        /* multiple choices? */
4088        if (xs_type(opt) == XSTYPE_LIST)
4089            ls = xs_dup(opt);
4090        else
4091        if (xs_type(opt) == XSTYPE_STRING) {
4092            ls = xs_list_new();
4093            ls = xs_list_append(ls, opt);
4094        }
4095
4096        const xs_str *v;
4097        int c = 0;
4098
4099        while (xs_list_next(ls, &v, &c)) {
4100            xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL);
4101
4102            /* set the option */
4103            msg = xs_dict_append(msg, "name", v);
4104
4105            xs *c_msg = msg_create(&snac, msg);
4106
4107            enqueue_message(&snac, c_msg);
4108
4109            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
4110        }
4111
4112        if (ls != NULL) {
4113            /* get the poll object */
4114            xs *poll = NULL;
4115
4116            if (valid_status(object_get(irt, &poll))) {
4117                const char *date = xs_dict_get(poll, "endTime");
4118                if (xs_is_null(date))
4119                    date = xs_dict_get(poll, "closed");
4120
4121                if (!xs_is_null(date)) {
4122                    time_t t = xs_parse_iso_date(date, 0) - time(NULL);
4123
4124                    /* request the poll when it's closed;
4125                       Pleroma does not send and update when the poll closes */
4126                    enqueue_object_request(&snac, irt, t + 2);
4127                }
4128            }
4129        }
4130
4131        status = HTTP_STATUS_SEE_OTHER;
4132    }
4133
4134    if (status == HTTP_STATUS_SEE_OTHER) {
4135        const char *redir = xs_dict_get(p_vars, "redir");
4136
4137        if (xs_is_null(redir))
4138            redir = "top";
4139
4140        *body   = xs_fmt("%s/admin#%s", snac.actor, redir);
4141        *b_size = strlen(*body);
4142    }
4143
4144    user_free(&snac);
4145
4146    return status;
4147}
4148
4149
4150xs_str *timeline_to_rss(snac *user, const xs_list *timeline,
4151                        const char *title, const char *link, const char *desc)
4152/* converts a timeline to rss */
4153{
4154    xs_html *rss = xs_html_tag("rss",
4155        xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"),
4156        xs_html_attr("version",       "2.0"),
4157        xs_html_attr("xmlns:atom",    "http:/" "/www.w3.org/2005/Atom"));
4158
4159    xs_html *channel = xs_html_tag("channel",
4160        xs_html_tag("title",
4161            xs_html_text(title)),
4162        xs_html_tag("language",
4163            xs_html_text("en")),
4164        xs_html_tag("link",
4165            xs_html_text(link)),
4166        xs_html_sctag("atom:link",
4167            xs_html_attr("href", link),
4168            xs_html_attr("rel", "self"),
4169            xs_html_attr("type", "application/rss+xml")),
4170        xs_html_tag("generator",
4171            xs_html_text(USER_AGENT)),
4172        xs_html_tag("description",
4173            xs_html_text(desc)));
4174
4175    xs_html_add(rss, channel);
4176
4177    int cnt = 0;
4178    const char *v;
4179
4180    xs_list_foreach(timeline, v) {
4181        xs *msg = NULL;
4182
4183        if (user) {
4184            if (!valid_status(timeline_get_by_md5(user, v, &msg)))
4185                continue;
4186        }
4187        else {
4188            if (!valid_status(object_get_by_md5(v, &msg)))
4189                continue;
4190        }
4191
4192        const char *id = xs_dict_get(msg, "id");
4193        const char *content = xs_dict_get(msg, "content");
4194        const char *published = xs_dict_get(msg, "published");
4195
4196        if (user && !xs_startswith(id, user->actor))
4197            continue;
4198
4199        if (!id || !content || !published)
4200            continue;
4201
4202        /* create a title with the first line of the content */
4203        xs *title = xs_replace(content, "<br>", "\n");
4204        title = xs_regex_replace_i(title, "<[^>]+>", " ");
4205        title = xs_regex_replace_i(title, "&[^;]+;", " ");
4206        int i;
4207
4208        for (i = 0; title[i] && title[i] != '\n' && i < 50; i++);
4209
4210        if (title[i] != '\0') {
4211            title[i] = '\0';
4212            title = xs_str_cat(title, "...");
4213        }
4214
4215        title = xs_strip_i(title);
4216
4217        /* convert the date */
4218        time_t t = xs_parse_iso_date(published, 0);
4219        xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000");
4220
4221        /* if it's the first one, add it to the header */
4222        if (cnt == 0)
4223            xs_html_add(channel,
4224                xs_html_tag("lastBuildDate",
4225                    xs_html_text(rss_date)));
4226
4227        xs_html_add(channel,
4228            xs_html_tag("item",
4229                xs_html_tag("title",
4230                    xs_html_text(title)),
4231                xs_html_tag("link",
4232                    xs_html_text(id)),
4233                xs_html_tag("guid",
4234                    xs_html_text(id)),
4235                xs_html_tag("pubDate",
4236                    xs_html_text(rss_date)),
4237                xs_html_tag("description",
4238                    xs_html_text(content))));
4239
4240        cnt++;
4241    }
4242
4243    return xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
4244}