2.67 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 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:/")) {
 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
1039    /** user settings **/
1040
1041    const char *email = "[disabled by admin]";
1042
1043    if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1044        email = xs_dict_get(snac->config_o, "email");
1045        if (xs_is_null(email)) {
1046            email = xs_dict_get(snac->config, "email");
1047
1048            if (xs_is_null(email))
1049                email = "";
1050        }
1051    }
1052
1053    const char *cw = xs_dict_get(snac->config, "cw");
1054    if (xs_is_null(cw))
1055        cw = "";
1056
1057    const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
1058    if (xs_is_null(telegram_bot))
1059        telegram_bot = "";
1060
1061    const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
1062    if (xs_is_null(telegram_chat_id))
1063        telegram_chat_id = "";
1064
1065    const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
1066    if (xs_is_null(ntfy_server))
1067        ntfy_server = "";
1068
1069    const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
1070    if (xs_is_null(ntfy_token))
1071        ntfy_token = "";
1072
1073    const char *purge_days = xs_dict_get(snac->config, "purge_days");
1074    if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
1075        purge_days = (char *)xs_number_str(purge_days);
1076    else
1077        purge_days = "0";
1078
1079    const xs_val *d_dm_f_u  = xs_dict_get(snac->config, "drop_dm_from_unknown");
1080    const xs_val *bot       = xs_dict_get(snac->config, "bot");
1081    const xs_val *a_private = xs_dict_get(snac->config, "private");
1082    const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
1083    const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
1084    const xs_val *pending    = xs_dict_get(snac->config, "approve_followers");
1085    const xs_val *show_foll  = xs_dict_get(snac->config, "show_contact_metrics");
1086
1087    xs *metadata = NULL;
1088    const xs_dict *md = xs_dict_get(snac->config, "metadata");
1089
1090    if (xs_type(md) == XSTYPE_DICT) {
1091        const xs_str *k;
1092        const xs_str *v;
1093
1094        metadata = xs_str_new(NULL);
1095
1096        xs_dict_foreach(md, k, v) {
1097            xs *kp = xs_fmt("%s=%s", k, v);
1098
1099            if (*metadata)
1100                metadata = xs_str_cat(metadata, "\n");
1101            metadata = xs_str_cat(metadata, kp);
1102        }
1103    }
1104    else
1105    if (xs_type(md) == XSTYPE_STRING)
1106        metadata = xs_dup(md);
1107    else
1108        metadata = xs_str_new(NULL);
1109
1110    xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
1111
1112    xs_html_add(top_controls,
1113        xs_html_tag("details",
1114        xs_html_tag("summary",
1115            xs_html_text(L("User Settings..."))),
1116        xs_html_tag("div",
1117            xs_html_attr("class", "snac-user-setup"),
1118            xs_html_tag("form",
1119                xs_html_attr("autocomplete", "off"),
1120                xs_html_attr("method",       "post"),
1121                xs_html_attr("action",       user_setup_action),
1122                xs_html_attr("enctype",      "multipart/form-data"),
1123                xs_html_tag("p",
1124                    xs_html_text(L("Display name:")),
1125                    xs_html_sctag("br", NULL),
1126                    xs_html_sctag("input",
1127                        xs_html_attr("type", "text"),
1128                        xs_html_attr("name", "name"),
1129                        xs_html_attr("value", xs_dict_get(snac->config, "name")),
1130                        xs_html_attr("placeholder", L("Your name")))),
1131                xs_html_tag("p",
1132                    xs_html_text(L("Avatar: ")),
1133                    xs_html_sctag("input",
1134                        xs_html_attr("type", "file"),
1135                        xs_html_attr("name", "avatar_file"))),
1136                xs_html_tag("p",
1137                    xs_html_sctag("input",
1138                        xs_html_attr("type", "checkbox"),
1139                        xs_html_attr("name", "avatar_delete")),
1140                        xs_html_text(L("Delete current avatar"))),
1141                xs_html_tag("p",
1142                    xs_html_text(L("Header image (banner): ")),
1143                    xs_html_sctag("input",
1144                        xs_html_attr("type", "file"),
1145                        xs_html_attr("name", "header_file"))),
1146                xs_html_tag("p",
1147                    xs_html_sctag("input",
1148                        xs_html_attr("type", "checkbox"),
1149                        xs_html_attr("name", "header_delete")),
1150                        xs_html_text(L("Delete current header image"))),
1151                xs_html_tag("p",
1152                    xs_html_text(L("Bio:")),
1153                    xs_html_sctag("br", NULL),
1154                    xs_html_tag("textarea",
1155                        xs_html_attr("name", "bio"),
1156                        xs_html_attr("cols", "40"),
1157                        xs_html_attr("rows", "4"),
1158                        xs_html_attr("placeholder", L("Write about yourself here...")),
1159                        xs_html_text(xs_dict_get(snac->config, "bio")))),
1160                xs_html_sctag("input",
1161                    xs_html_attr("type", "checkbox"),
1162                    xs_html_attr("name", "cw"),
1163                    xs_html_attr("id",   "cw"),
1164                    xs_html_attr(strcmp(cw, "open") == 0 ? "checked" : "", NULL)),
1165                xs_html_tag("label",
1166                    xs_html_attr("for", "cw"),
1167                    xs_html_text(L("Always show sensitive content"))),
1168                xs_html_tag("p",
1169                    xs_html_text(L("Email address for notifications:")),
1170                    xs_html_sctag("br", NULL),
1171                    xs_html_sctag("input",
1172                        xs_html_attr("type", "text"),
1173                        xs_html_attr("name", "email"),
1174                        xs_html_attr("value", email),
1175                        xs_html_attr("placeholder", "bob@example.com"))),
1176                xs_html_tag("p",
1177                    xs_html_text(L("Telegram notifications (bot key and chat id):")),
1178                    xs_html_sctag("br", NULL),
1179                    xs_html_sctag("input",
1180                        xs_html_attr("type", "text"),
1181                        xs_html_attr("name", "telegram_bot"),
1182                        xs_html_attr("value", telegram_bot),
1183                        xs_html_attr("placeholder", "Bot API key")),
1184                    xs_html_text(" "),
1185                    xs_html_sctag("input",
1186                        xs_html_attr("type", "text"),
1187                        xs_html_attr("name", "telegram_chat_id"),
1188                        xs_html_attr("value", telegram_chat_id),
1189                        xs_html_attr("placeholder", "Chat id"))),
1190                xs_html_tag("p",
1191                    xs_html_text(L("ntfy notifications (ntfy server and token):")),
1192                    xs_html_sctag("br", NULL),
1193                    xs_html_sctag("input",
1194                        xs_html_attr("type", "text"),
1195                        xs_html_attr("name", "ntfy_server"),
1196                        xs_html_attr("value", ntfy_server),
1197                        xs_html_attr("placeholder", "ntfy server - full URL (example: https://ntfy.sh/YourTopic)")),
1198                    xs_html_text(" "),
1199                    xs_html_sctag("input",
1200                        xs_html_attr("type", "text"),
1201                        xs_html_attr("name", "ntfy_token"),
1202                        xs_html_attr("value", ntfy_token),
1203                        xs_html_attr("placeholder", "ntfy token - if needed"))),
1204                xs_html_tag("p",
1205                    xs_html_text(L("Maximum days to keep posts (0: server settings):")),
1206                    xs_html_sctag("br", NULL),
1207                    xs_html_sctag("input",
1208                        xs_html_attr("type", "number"),
1209                        xs_html_attr("name", "purge_days"),
1210                        xs_html_attr("value", purge_days))),
1211                xs_html_tag("p",
1212                    xs_html_sctag("input",
1213                        xs_html_attr("type", "checkbox"),
1214                        xs_html_attr("name", "drop_dm_from_unknown"),
1215                        xs_html_attr("id", "drop_dm_from_unknown"),
1216                        xs_html_attr(xs_type(d_dm_f_u) == XSTYPE_TRUE ? "checked" : "", NULL)),
1217                    xs_html_tag("label",
1218                        xs_html_attr("for", "drop_dm_from_unknown"),
1219                        xs_html_text(L("Drop direct messages from people you don't follow")))),
1220                xs_html_tag("p",
1221                    xs_html_sctag("input",
1222                        xs_html_attr("type", "checkbox"),
1223                        xs_html_attr("name", "bot"),
1224                        xs_html_attr("id", "bot"),
1225                        xs_html_attr(xs_type(bot) == XSTYPE_TRUE ? "checked" : "", NULL)),
1226                    xs_html_tag("label",
1227                        xs_html_attr("for", "bot"),
1228                        xs_html_text(L("This account is a bot")))),
1229                xs_html_tag("p",
1230                    xs_html_sctag("input",
1231                        xs_html_attr("type", "checkbox"),
1232                        xs_html_attr("name", "auto_boost"),
1233                        xs_html_attr("id", "auto_boost"),
1234                        xs_html_attr(xs_is_true(auto_boost) ? "checked" : "", NULL)),
1235                    xs_html_tag("label",
1236                        xs_html_attr("for", "auto_boost"),
1237                        xs_html_text(L("Auto-boost all mentions to this account")))),
1238                xs_html_tag("p",
1239                    xs_html_sctag("input",
1240                        xs_html_attr("type", "checkbox"),
1241                        xs_html_attr("name", "private"),
1242                        xs_html_attr("id", "private"),
1243                        xs_html_attr(xs_type(a_private) == XSTYPE_TRUE ? "checked" : "", NULL)),
1244                    xs_html_tag("label",
1245                        xs_html_attr("for", "private"),
1246                        xs_html_text(L("This account is private "
1247                            "(posts are not shown through the web)")))),
1248                xs_html_tag("p",
1249                    xs_html_sctag("input",
1250                        xs_html_attr("type", "checkbox"),
1251                        xs_html_attr("name", "collapse_threads"),
1252                        xs_html_attr("id",   "collapse_threads"),
1253                        xs_html_attr(xs_is_true(coll_thrds) ? "checked" : "", NULL)),
1254                    xs_html_tag("label",
1255                        xs_html_attr("for", "collapse_threads"),
1256                        xs_html_text(L("Collapse top threads by default")))),
1257                xs_html_tag("p",
1258                    xs_html_sctag("input",
1259                        xs_html_attr("type", "checkbox"),
1260                        xs_html_attr("name", "approve_followers"),
1261                        xs_html_attr("id",   "approve_followers"),
1262                        xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
1263                    xs_html_tag("label",
1264                        xs_html_attr("for", "approve_followers"),
1265                        xs_html_text(L("Follow requests must be approved")))),
1266                xs_html_tag("p",
1267                    xs_html_sctag("input",
1268                        xs_html_attr("type", "checkbox"),
1269                        xs_html_attr("name", "show_contact_metrics"),
1270                        xs_html_attr("id",   "show_contact_metrics"),
1271                        xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
1272                    xs_html_tag("label",
1273                        xs_html_attr("for", "show_contact_metrics"),
1274                        xs_html_text(L("Publish follower and following metrics")))),
1275                xs_html_tag("p",
1276                    xs_html_text(L("Profile metadata (key=value pairs in each line):")),
1277                    xs_html_sctag("br", NULL),
1278                    xs_html_tag("textarea",
1279                        xs_html_attr("name", "metadata"),
1280                        xs_html_attr("cols", "40"),
1281                        xs_html_attr("rows", "4"),
1282                        xs_html_attr("placeholder", "Blog=https:/"
1283                            "/example.com/my-blog\nGPG Key=1FA54\n..."),
1284                    xs_html_text(metadata))),
1285
1286                xs_html_tag("p",
1287                    xs_html_text(L("New password:")),
1288                    xs_html_sctag("br", NULL),
1289                    xs_html_sctag("input",
1290                        xs_html_attr("type", "password"),
1291                        xs_html_attr("name", "passwd1"),
1292                        xs_html_attr("value", ""))),
1293                xs_html_tag("p",
1294                    xs_html_text(L("Repeat new password:")),
1295                    xs_html_sctag("br", NULL),
1296                    xs_html_sctag("input",
1297                        xs_html_attr("type", "password"),
1298                        xs_html_attr("name", "passwd2"),
1299                        xs_html_attr("value", ""))),
1300
1301                xs_html_sctag("input",
1302                    xs_html_attr("type", "submit"),
1303                    xs_html_attr("class", "button"),
1304                    xs_html_attr("value", L("Update user info")))))));
1305
1306    return top_controls;
1307}
1308
1309
1310static xs_html *html_button(char *clss, char *label, char *hint)
1311{
1312    xs *c = xs_fmt("snac-btn-%s", clss);
1313
1314    /* use an NULL tag to separate non-css-classed buttons from one another */
1315    return xs_html_container(
1316        xs_html_sctag("input",
1317            xs_html_attr("type",    "submit"),
1318            xs_html_attr("name",    "action"),
1319            xs_html_attr("class",   c),
1320            xs_html_attr("value",   label),
1321            xs_html_attr("title",   hint)),
1322        xs_html_text("\n"));
1323}
1324
1325
1326xs_str *build_mentions(snac *snac, const xs_dict *msg)
1327/* returns a string with the mentions in msg */
1328{
1329    xs_str *s = xs_str_new(NULL);
1330    const char *list = xs_dict_get(msg, "tag");
1331    const char *v;
1332    int c = 0;
1333
1334    while (xs_list_next(list, &v, &c)) {
1335        const char *type = xs_dict_get(v, "type");
1336        const char *href = xs_dict_get(v, "href");
1337        const char *name = xs_dict_get(v, "name");
1338
1339        if (type && strcmp(type, "Mention") == 0 &&
1340            href && strcmp(href, snac->actor) != 0 && name) {
1341            xs *s1 = NULL;
1342
1343            if (name[0] != '@') {
1344                s1 = xs_fmt("@%s", name);
1345                name = s1;
1346            }
1347
1348            xs *l = xs_split(name, "@");
1349
1350            /* is it a name without a host? */
1351            if (xs_list_len(l) < 3) {
1352                /* split the href and pick the host name LIKE AN ANIMAL */
1353                /* would be better to query the webfinger but *won't do that* here */
1354                xs *l2 = xs_split(href, "/");
1355
1356                if (xs_list_len(l2) >= 3) {
1357                    xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
1358
1359                    if (xs_str_in(s, s1) == -1)
1360                        s = xs_str_cat(s, s1);
1361                }
1362            }
1363            else {
1364                if (xs_str_in(s, name) == -1) {
1365                    s = xs_str_cat(s, name);
1366                    s = xs_str_cat(s, " ");
1367                }
1368            }
1369        }
1370    }
1371
1372    if (*s) {
1373        xs *s1 = s;
1374        s = xs_fmt("\n\n\nCC: %s", s1);
1375    }
1376
1377    return s;
1378}
1379
1380
1381xs_html *html_entry_controls(snac *snac, const char *actor,
1382                            const xs_dict *msg, const char *md5)
1383{
1384    const char *id    = xs_dict_get(msg, "id");
1385    const char *group = xs_dict_get(msg, "audience");
1386
1387    xs *likes   = object_likes(id);
1388    xs *boosts  = object_announces(id);
1389
1390    xs *action = xs_fmt("%s/admin/action", snac->actor);
1391    xs *redir  = xs_fmt("%s_entry", md5);
1392
1393    xs_html *form;
1394    xs_html *controls = xs_html_tag("div",
1395        xs_html_attr("class", "snac-controls"),
1396        form = xs_html_tag("form",
1397            xs_html_attr("autocomplete", "off"),
1398            xs_html_attr("method",       "post"),
1399            xs_html_attr("action",       action),
1400            xs_html_sctag("input",
1401                xs_html_attr("type",     "hidden"),
1402                xs_html_attr("name",     "id"),
1403                xs_html_attr("value",    id)),
1404            xs_html_sctag("input",
1405                xs_html_attr("type",     "hidden"),
1406                xs_html_attr("name",     "actor"),
1407                xs_html_attr("value",    actor)),
1408            xs_html_sctag("input",
1409                xs_html_attr("type",     "hidden"),
1410                xs_html_attr("name",     "group"),
1411                xs_html_attr("value",    xs_is_null(group) ? "" : group)),
1412            xs_html_sctag("input",
1413                xs_html_attr("type",     "hidden"),
1414                xs_html_attr("name",     "redir"),
1415                xs_html_attr("value",    redir))));
1416
1417    if (!xs_startswith(id, snac->actor)) {
1418        if (xs_list_in(likes, snac->md5) == -1) {
1419            /* not already liked; add button */
1420            xs_html_add(form,
1421                html_button("like", L("Like"), L("Say you like this post")));
1422        }
1423        else {
1424            /* not like it anymore */
1425            xs_html_add(form,
1426                html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
1427        }
1428    }
1429    else {
1430        if (is_pinned(snac, id))
1431            xs_html_add(form,
1432                html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
1433        else
1434            xs_html_add(form,
1435                html_button("pin", L("Pin"), L("Pin this post to the top of your timeline")));
1436    }
1437
1438    if (is_msg_public(msg)) {
1439        if (xs_list_in(boosts, snac->md5) == -1) {
1440            /* not already boosted; add button */
1441            xs_html_add(form,
1442                html_button("boost", L("Boost"), L("Announce this post to your followers")));
1443        }
1444        else {
1445            /* already boosted; add button to regret */
1446            xs_html_add(form,
1447                html_button("unboost", L("Unboost"), L("I regret I boosted this")));
1448        }
1449    }
1450
1451    if (is_bookmarked(snac, id))
1452            xs_html_add(form,
1453                html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
1454        else
1455            xs_html_add(form,
1456                html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
1457
1458    if (strcmp(actor, snac->actor) != 0) {
1459        /* controls for other actors than this one */
1460        if (following_check(snac, actor)) {
1461            xs_html_add(form,
1462                html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
1463        }
1464        else {
1465            xs_html_add(form,
1466                html_button("follow", L("Follow"), L("Start following this user's activity")));
1467        }
1468
1469        if (!xs_is_null(group)) {
1470            if (following_check(snac, group)) {
1471                xs_html_add(form,
1472                    html_button("unfollow", L("Unfollow Group"),
1473                        L("Stop following this group or channel")));
1474            }
1475            else {
1476                xs_html_add(form,
1477                    html_button("follow", L("Follow Group"),
1478                        L("Start following this group or channel")));
1479            }
1480        }
1481
1482        xs_html_add(form,
1483            html_button("mute", L("MUTE"),
1484                L("Block any activity from this user forever")));
1485    }
1486
1487    if (!xs_is_true(xs_dict_get(srv_config, "hide_delete_post_button")))
1488        xs_html_add(form,
1489            html_button("delete", L("Delete"), L("Delete this post")));
1490
1491    xs_html_add(form,
1492        html_button("hide",   L("Hide"), L("Hide this post and its children")));
1493
1494    const char *prev_src = xs_dict_get(msg, "sourceContent");
1495
1496    if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/
1497        /* post can be edited */
1498        xs *div_id  = xs_fmt("%s_edit", md5);
1499        xs *form_id = xs_fmt("%s_edit_form", md5);
1500        xs *redir   = xs_fmt("%s_entry", md5);
1501
1502        const char *att_file = "";
1503        const char *att_alt_text = "";
1504        const xs_list *att_list = xs_dict_get(msg, "attachment");
1505
1506        /* does it have an attachment? */
1507        if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) {
1508            const xs_dict *d = xs_list_get(att_list, 0);
1509
1510            if (xs_type(d) == XSTYPE_DICT) {
1511                att_file = xs_dict_get_def(d, "url", "");
1512                att_alt_text = xs_dict_get_def(d, "name", "");
1513            }
1514        }
1515
1516        xs_html_add(controls, xs_html_tag("div",
1517            xs_html_tag("p", NULL),
1518            html_note(snac, L("Edit..."),
1519                div_id, form_id,
1520                "", prev_src,
1521                id, NULL,
1522                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1523                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1524                NULL, 0, att_file, att_alt_text, is_draft(snac, id))),
1525            xs_html_tag("p", NULL));
1526    }
1527
1528    { /** reply **/
1529        /* the post textarea */
1530        xs *ct      = build_mentions(snac, msg);
1531        xs *div_id  = xs_fmt("%s_reply", md5);
1532        xs *form_id = xs_fmt("%s_reply_form", md5);
1533        xs *redir   = xs_fmt("%s_entry", md5);
1534
1535        xs_html_add(controls, xs_html_tag("div",
1536            xs_html_tag("p", NULL),
1537            html_note(snac, L("Reply..."),
1538                div_id, form_id,
1539                "", ct,
1540                NULL, NULL,
1541                xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1542                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1543                id, 0, "", "", 0)),
1544            xs_html_tag("p", NULL));
1545    }
1546
1547    return controls;
1548}
1549
1550
1551xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1552                   int level, const char *md5, int hide_children)
1553{
1554    const char *id    = xs_dict_get(msg, "id");
1555    const char *type  = xs_dict_get(msg, "type");
1556    const char *actor;
1557    const char *v;
1558    int has_title = 0;
1559    int collapse_threads = 0;
1560    const char *proxy = NULL;
1561
1562    if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
1563        proxy = user->actor;
1564
1565    /* do not show non-public messages in the public timeline */
1566    if ((read_only || !user) && !is_msg_public(msg))
1567        return NULL;
1568
1569    if (id && is_instance_blocked(id))
1570        return NULL;
1571
1572    if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
1573        collapse_threads = 1;
1574
1575    /* hidden? do nothing more for this conversation */
1576    if (user && is_hidden(user, id)) {
1577        xs *s1 = xs_fmt("%s_entry", md5);
1578
1579        /* return just an dummy anchor, to keep position after hitting 'Hide' */
1580        return xs_html_tag("div",
1581            xs_html_tag("a",
1582                xs_html_attr("name", s1)));
1583    }
1584
1585    /* avoid too deep nesting, as it may be a loop */
1586    if (level >= MAX_CONVERSATION_LEVELS)
1587        return xs_html_tag("mark",
1588            xs_html_text(L("Truncated (too deep)")));
1589
1590    if (strcmp(type, "Follow") == 0) {
1591        return xs_html_tag("div",
1592            xs_html_attr("class", "snac-post"),
1593            xs_html_tag("div",
1594                xs_html_attr("class", "snac-post-header"),
1595                xs_html_tag("div",
1596                    xs_html_attr("class", "snac-origin"),
1597                    xs_html_text(L("follows you"))),
1598                html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy)));
1599    }
1600    else
1601    if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
1602        /* skip oddities */
1603        snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
1604        return NULL;
1605    }
1606
1607    /* ignore notes with "name", as they are votes to Questions */
1608    if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
1609        return NULL;
1610
1611    /* get the attributedTo */
1612    if ((actor = get_atto(msg)) == NULL)
1613        return NULL;
1614
1615    /* ignore muted morons immediately */
1616    if (user && is_muted(user, actor)) {
1617        xs *s1 = xs_fmt("%s_entry", md5);
1618
1619        /* return just an dummy anchor, to keep position after hitting 'MUTE' */
1620        return xs_html_tag("div",
1621            xs_html_tag("a",
1622                xs_html_attr("name", s1)));
1623    }
1624
1625    if ((user == NULL || strcmp(actor, user->actor) != 0)
1626        && !valid_status(actor_get(actor, NULL)))
1627        return NULL;
1628
1629    /** html_entry top tag **/
1630    xs_html *entry_top = xs_html_tag("div", NULL);
1631
1632    {
1633        xs *s1 = xs_fmt("%s_entry", md5);
1634        xs_html_add(entry_top,
1635            xs_html_tag("a",
1636                xs_html_attr("name", s1)));
1637    }
1638
1639    xs_html *entry = xs_html_tag("div",
1640        xs_html_attr("class", level == 0 ? "snac-post" : "snac-child"));
1641
1642    xs_html_add(entry_top,
1643        entry);
1644
1645    /** post header **/
1646
1647    xs_html *score;
1648    xs_html *post_header = xs_html_tag("div",
1649        xs_html_attr("class", "snac-post-header"),
1650        score = xs_html_tag("div",
1651            xs_html_attr("class", "snac-score")));
1652
1653    xs_html_add(entry,
1654        post_header);
1655
1656    if (user && is_pinned(user, id)) {
1657        /* add a pin emoji */
1658        xs_html_add(score,
1659            xs_html_tag("span",
1660                xs_html_attr("title", L("Pinned")),
1661                xs_html_raw(" &#128204; ")));
1662    }
1663
1664    if (user && is_bookmarked(user, id)) {
1665        /* add a bookmark emoji */
1666        xs_html_add(score,
1667            xs_html_tag("span",
1668                xs_html_attr("title", L("Bookmarked")),
1669                xs_html_raw(" &#128278; ")));
1670    }
1671
1672    if (strcmp(type, "Question") == 0) {
1673        /* add the ballot box emoji */
1674        xs_html_add(score,
1675            xs_html_tag("span",
1676                xs_html_attr("title", L("Poll")),
1677                xs_html_raw(" &#128499; ")));
1678
1679        if (user && was_question_voted(user, id)) {
1680            /* add a check to show this poll was voted */
1681            xs_html_add(score,
1682                xs_html_tag("span",
1683                    xs_html_attr("title", L("Voted")),
1684                    xs_html_raw(" &#10003; ")));
1685        }
1686    }
1687
1688    if (strcmp(type, "Event") == 0) {
1689        /* add the calendar emoji */
1690        xs_html_add(score,
1691            xs_html_tag("span",
1692                xs_html_attr("title", L("Event")),
1693                xs_html_raw(" &#128197; ")));
1694    }
1695
1696    /* if it's a user from this same instance, add the score */
1697    if (xs_startswith(id, srv_baseurl)) {
1698        int n_likes  = object_likes_len(id);
1699        int n_boosts = object_announces_len(id);
1700
1701        /* alternate emojis: %d &#128077; %d &#128257; */
1702        xs *s1 = xs_fmt("%d &#9733; %d &#8634;\n", n_likes, n_boosts);
1703
1704        xs_html_add(score,
1705            xs_html_raw(s1));
1706    }
1707
1708    xs *boosts = object_announces(id);
1709
1710    if (xs_list_len(boosts)) {
1711        /* if somebody boosted this, show as origin */
1712        const char *p = xs_list_get(boosts, -1);
1713        xs *actor_r = NULL;
1714
1715        if (user && xs_list_in(boosts, user->md5) != -1) {
1716            /* we boosted this */
1717            xs_html_add(post_header,
1718                xs_html_tag("div",
1719                    xs_html_attr("class", "snac-origin"),
1720                    xs_html_tag("a",
1721                        xs_html_attr("href", user->actor),
1722                        xs_html_text(xs_dict_get(user->config, "name"))),
1723                        xs_html_text(" "),
1724                        xs_html_text(L("boosted"))));
1725        }
1726        else
1727        if (valid_status(object_get_by_md5(p, &actor_r))) {
1728            xs *name = actor_name(actor_r, proxy);
1729
1730            if (!xs_is_null(name)) {
1731                xs *href = NULL;
1732                const char *id = xs_dict_get(actor_r, "id");
1733                int fwers = 0;
1734                int fwing = 0;
1735
1736                if (user != NULL) {
1737                    fwers = follower_check(user, id);
1738                    fwing = following_check(user, id);
1739                }
1740
1741                if (!read_only && (fwers || fwing))
1742                    href = xs_fmt("%s/people#%s", user->actor, p);
1743                else
1744                    href = xs_dup(id);
1745
1746                xs_html_add(post_header,
1747                    xs_html_tag("div",
1748                        xs_html_attr("class", "snac-origin"),
1749                        xs_html_tag("a",
1750                            xs_html_attr("href", href),
1751                            xs_html_raw(name)), /* already sanitized */
1752                            xs_html_text(" "),
1753                            xs_html_text(L("boosted"))));
1754            }
1755        }
1756    }
1757    else
1758    if (strcmp(type, "Note") == 0) {
1759        if (level == 0) {
1760            /* is the parent not here? */
1761            const char *parent = get_in_reply_to(msg);
1762
1763            if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
1764                xs_html_add(post_header,
1765                    xs_html_tag("div",
1766                        xs_html_attr("class", "snac-origin"),
1767                        xs_html_text(L("in reply to")),
1768                        xs_html_text(" "),
1769                        xs_html_tag("a",
1770                            xs_html_attr("href", parent),
1771                            xs_html_text("ยป"))));
1772            }
1773        }
1774    }
1775
1776    xs_html_add(post_header,
1777        html_msg_icon(read_only ? NULL : user, actor, msg, proxy));
1778
1779    /** post content **/
1780
1781    xs_html *snac_content_wrap = xs_html_tag("div",
1782        xs_html_attr("class", "e-content snac-content"));
1783
1784    xs_html_add(entry,
1785        snac_content_wrap);
1786
1787    if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
1788        xs_html_add(snac_content_wrap,
1789            xs_html_tag("h3",
1790                xs_html_attr("class", "snac-entry-title"),
1791                xs_html_text(v)));
1792
1793        has_title = 1;
1794    }
1795
1796    xs_html *snac_content = NULL;
1797
1798    v = xs_dict_get(msg, "summary");
1799
1800    /* is it sensitive? */
1801    if (xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
1802        if (xs_is_null(v) || *v == '\0')
1803            v = "...";
1804
1805        const char *cw = "";
1806
1807        if (user) {
1808            /* only show it when not in the public timeline and the config setting is "open" */
1809            cw = xs_dict_get(user->config, "cw");
1810            if (xs_is_null(cw) || read_only)
1811                cw = "";
1812        }
1813
1814        snac_content = xs_html_tag("details",
1815            xs_html_attr(cw, NULL),
1816            xs_html_tag("summary",
1817                xs_html_text(v),
1818                xs_html_text(L(" [SENSITIVE CONTENT]"))));
1819    }
1820    else {
1821        /* print the summary as a header (sites like e.g. Friendica can contain one) */
1822        if (!has_title && !xs_is_null(v) && *v) {
1823            xs_html_add(snac_content_wrap,
1824                xs_html_tag("h3",
1825                    xs_html_attr("class", "snac-entry-title"),
1826                    xs_html_text(v)));
1827
1828            has_title = 1;
1829        }
1830
1831        snac_content = xs_html_tag("div", NULL);
1832    }
1833
1834    xs_html_add(snac_content_wrap,
1835        snac_content);
1836
1837    {
1838        /** build the content string **/
1839        const char *content = xs_dict_get(msg, "content");
1840
1841        if (xs_type(content) != XSTYPE_STRING) {
1842            if (!xs_is_null(content))
1843                srv_archive_error("unexpected_content_xstype",
1844                    "content field type", xs_stock(XSTYPE_DICT), msg);
1845
1846            content = "";
1847        }
1848
1849        /* skip ugly line breaks at the beginning */
1850        while (xs_startswith(content, "<br>"))
1851            content += 4;
1852
1853        xs *c = sanitize(content);
1854
1855        /* do some tweaks to the content */
1856        c = xs_replace_i(c, "\r", "");
1857
1858        while (xs_endswith(c, "<br><br>"))
1859            c = xs_crop_i(c, 0, -4);
1860
1861        c = xs_replace_i(c, "<br><br>", "<p>");
1862
1863        c = xs_str_cat(c, "<p>");
1864
1865        /* replace the :shortnames: */
1866        c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
1867
1868        /* Peertube videos content is in markdown */
1869        const char *mtype = xs_dict_get(msg, "mediaType");
1870        if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
1871            /* a full conversion could be better */
1872            c = xs_replace_i(c, "\r", "");
1873            c = xs_replace_i(c, "\n", "<br>");
1874        }
1875
1876        /* c contains sanitized HTML */
1877        xs_html_add(snac_content,
1878            xs_html_raw(c));
1879    }
1880
1881    if (strcmp(type, "Question") == 0) { /** question content **/
1882        const xs_list *oo = xs_dict_get(msg, "oneOf");
1883        const xs_list *ao = xs_dict_get(msg, "anyOf");
1884        const xs_list *p;
1885        const xs_dict *v;
1886        int closed = 0;
1887        const char *f_closed = NULL;
1888
1889        xs_html *poll = xs_html_tag("div", NULL);
1890
1891        if (read_only)
1892            closed = 1; /* non-identified page; show as closed */
1893        else
1894        if (user && xs_startswith(id, user->actor))
1895            closed = 1; /* we questioned; closed for us */
1896        else
1897        if (user && was_question_voted(user, id))
1898            closed = 1; /* we already voted; closed for us */
1899
1900        if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
1901            /* it has a closed date... but is it in the past? */
1902            time_t t0 = time(NULL);
1903            time_t t1 = xs_parse_iso_date(f_closed, 0);
1904
1905            if (t1 < t0)
1906                closed = 2;
1907        }
1908
1909        /* get the appropriate list of options */
1910        p = oo != NULL ? oo : ao;
1911
1912        if (closed || user == NULL) {
1913            /* closed poll */
1914            xs_html *poll_result = xs_html_tag("table",
1915                xs_html_attr("class", "snac-poll-result"));
1916            int c = 0;
1917
1918            while (xs_list_next(p, &v, &c)) {
1919                const char *name       = xs_dict_get(v, "name");
1920                const xs_dict *replies = xs_dict_get(v, "replies");
1921
1922                if (name && replies) {
1923                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
1924
1925                    xs_html_add(poll_result,
1926                        xs_html_tag("tr",
1927                            xs_html_tag("td",
1928                                xs_html_text(name),
1929                                xs_html_text(":")),
1930                            xs_html_tag("td",
1931                                xs_html_text(ti))));
1932                }
1933            }
1934
1935            xs_html_add(poll,
1936                poll_result);
1937        }
1938        else {
1939            /* poll still active */
1940            xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
1941            xs_html *form;
1942            xs_html *poll_form = xs_html_tag("div",
1943                xs_html_attr("class", "snac-poll-form"),
1944                form = xs_html_tag("form",
1945                    xs_html_attr("autocomplete", "off"),
1946                    xs_html_attr("method", "post"),
1947                    xs_html_attr("action", vote_action),
1948                    xs_html_sctag("input",
1949                        xs_html_attr("type", "hidden"),
1950                        xs_html_attr("name", "actor"),
1951                        xs_html_attr("value", actor)),
1952                    xs_html_sctag("input",
1953                        xs_html_attr("type", "hidden"),
1954                        xs_html_attr("name", "irt"),
1955                        xs_html_attr("value", id))));
1956
1957            int c = 0;
1958            while (xs_list_next(p, &v, &c)) {
1959                const char *name = xs_dict_get(v, "name");
1960                const xs_dict *replies = xs_dict_get(v, "replies");
1961
1962                if (name) {
1963                    char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
1964
1965                    xs_html *btn = xs_html_sctag("input",
1966                            xs_html_attr("id", name),
1967                            xs_html_attr("value", name),
1968                            xs_html_attr("name", "question"));
1969
1970                    if (!xs_is_null(oo)) {
1971                        xs_html_add(btn,
1972                            xs_html_attr("type", "radio"),
1973                            xs_html_attr("required", "required"));
1974                    }
1975                    else
1976                        xs_html_add(btn,
1977                            xs_html_attr("type", "checkbox"));
1978
1979                    xs_html_add(form,
1980                        btn,
1981                        xs_html_text(" "),
1982                        xs_html_tag("span",
1983                            xs_html_attr("title", ti),
1984                            xs_html_text(name)),
1985                        xs_html_sctag("br", NULL));
1986                }
1987            }
1988
1989            xs_html_add(form,
1990                xs_html_tag("p", NULL),
1991                xs_html_sctag("input",
1992                    xs_html_attr("type", "submit"),
1993                    xs_html_attr("class", "button"),
1994                    xs_html_attr("value", L("Vote"))));
1995
1996            xs_html_add(poll,
1997                poll_form);
1998        }
1999
2000        /* if it's *really* closed, say it */
2001        if (closed == 2) {
2002            xs_html_add(poll,
2003                xs_html_tag("p",
2004                    xs_html_text(L("Closed"))));
2005        }
2006        else {
2007            /* show when the poll closes */
2008            const char *end_time = xs_dict_get(msg, "endTime");
2009
2010            /* Pleroma does not have an endTime field;
2011               it has a closed time in the future */
2012            if (xs_is_null(end_time))
2013                end_time = xs_dict_get(msg, "closed");
2014
2015            if (!xs_is_null(end_time)) {
2016                time_t t0 = time(NULL);
2017                time_t t1 = xs_parse_iso_date(end_time, 0);
2018
2019                if (t1 > 0 && t1 > t0) {
2020                    time_t diff_time = t1 - t0;
2021                    xs *tf = xs_str_time_diff(diff_time);
2022                    char *p = tf;
2023
2024                    /* skip leading zeros */
2025                    for (; *p == '0' || *p == ':'; p++);
2026
2027                    xs_html_add(poll,
2028                        xs_html_tag("p",
2029                            xs_html_text(L("Closes in")),
2030                            xs_html_text(" "),
2031                            xs_html_text(p)));
2032                }
2033            }
2034        }
2035
2036        xs_html_add(snac_content,
2037            poll);
2038    }
2039
2040    /** attachments **/
2041    xs *attach = get_attachments(msg);
2042
2043    {
2044        /* make custom css for attachments easier */
2045        xs_html *content_attachments = xs_html_tag("div",
2046            xs_html_attr("class", "snac-content-attachments"));
2047
2048        xs_html_add(snac_content,
2049            content_attachments);
2050
2051        const char *content = xs_dict_get(msg, "content");
2052
2053        int c = 0;
2054        const xs_dict *a;
2055        while (xs_list_next(attach, &a, &c)) {
2056            const char *type = xs_dict_get(a, "type");
2057            const char *o_href = xs_dict_get(a, "href");
2058            const char *name = xs_dict_get(a, "name");
2059
2060            /* if this image is already in the post content, skip */
2061            if (content && xs_str_in(content, o_href) != -1)
2062                continue;
2063
2064            /* do this attachment include an icon? */
2065            const xs_dict *icon = xs_dict_get(a, "icon");
2066            if (xs_type(icon) == XSTYPE_DICT) {
2067                const char *icon_mtype = xs_dict_get(icon, "mediaType");
2068                const char *icon_url   = xs_dict_get(icon, "url");
2069
2070                if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
2071                    xs_html_add(content_attachments,
2072                        xs_html_tag("a",
2073                            xs_html_attr("href", icon_url),
2074                            xs_html_attr("target", "_blank"),
2075                            xs_html_sctag("img",
2076                                xs_html_attr("loading", "lazy"),
2077                                xs_html_attr("src", icon_url))));
2078                }
2079            }
2080
2081            xs *href = make_url(o_href, proxy, 0);
2082
2083            if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
2084                xs_html_add(content_attachments,
2085                    xs_html_tag("a",
2086                        xs_html_attr("href", href),
2087                        xs_html_attr("target", "_blank"),
2088                        xs_html_sctag("img",
2089                            xs_html_attr("loading", "lazy"),
2090                            xs_html_attr("src", href),
2091                            xs_html_attr("alt", name),
2092                            xs_html_attr("title", name))));
2093            }
2094            else
2095            if (xs_startswith(type, "video/")) {
2096                xs_html_add(content_attachments,
2097                    xs_html_tag("video",
2098                        xs_html_attr("preload", "none"),
2099                        xs_html_attr("style", "width: 100%"),
2100                        xs_html_attr("class", "snac-embedded-video"),
2101                        xs_html_attr("controls", NULL),
2102                        xs_html_attr("src", href),
2103                        xs_html_text(L("Video")),
2104                        xs_html_text(": "),
2105                        xs_html_tag("a",
2106                            xs_html_attr("href", href),
2107                            xs_html_text(name))));
2108            }
2109            else
2110            if (xs_startswith(type, "audio/")) {
2111                xs_html_add(content_attachments,
2112                    xs_html_tag("audio",
2113                        xs_html_attr("preload", "none"),
2114                        xs_html_attr("style", "width: 100%"),
2115                        xs_html_attr("class", "snac-embedded-audio"),
2116                        xs_html_attr("controls", NULL),
2117                        xs_html_attr("src", href),
2118                        xs_html_text(L("Audio")),
2119                        xs_html_text(": "),
2120                        xs_html_tag("a",
2121                            xs_html_attr("href", href),
2122                            xs_html_text(name))));
2123            }
2124            else
2125            if (strcmp(type, "Link") == 0) {
2126                xs_html_add(content_attachments,
2127                    xs_html_tag("p",
2128                        xs_html_tag("a",
2129                            xs_html_attr("href", o_href),
2130                            xs_html_text(href))));
2131
2132                /* do not generate an Alt... */
2133                name = NULL;
2134            }
2135            else {
2136                xs_html_add(content_attachments,
2137                    xs_html_tag("p",
2138                        xs_html_tag("a",
2139                            xs_html_attr("href", o_href),
2140                            xs_html_text(L("Attachment")),
2141                            xs_html_text(": "),
2142                            xs_html_text(o_href))));
2143
2144                /* do not generate an Alt... */
2145                name = NULL;
2146            }
2147
2148            if (name != NULL && *name) {
2149                xs_html_add(content_attachments,
2150                    xs_html_tag("p",
2151                        xs_html_attr("class", "snac-alt-text"),
2152                        xs_html_tag("details",
2153                            xs_html_tag("summary",
2154                                xs_html_text(L("Alt..."))),
2155                            xs_html_text(name))));
2156            }
2157        }
2158    }
2159
2160    /* has this message an audience (i.e., comes from a channel or community)? */
2161    const char *audience = xs_dict_get(msg, "audience");
2162    if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
2163        xs_html *au_tag = xs_html_tag("p",
2164            xs_html_text("("),
2165            xs_html_tag("a",
2166                xs_html_attr("href", audience),
2167                xs_html_attr("title", L("Source channel or community")),
2168                xs_html_text(audience)),
2169            xs_html_text(")"));
2170
2171        xs_html_add(snac_content_wrap,
2172            au_tag);
2173    }
2174
2175    /** controls **/
2176
2177    if (!read_only && user) {
2178        xs_html_add(entry,
2179            html_entry_controls(user, actor, msg, md5));
2180    }
2181
2182    /** children **/
2183    if (!hide_children) {
2184        xs *children = object_children(id);
2185        int left     = xs_list_len(children);
2186
2187        if (left) {
2188            xs_html *ch_details = xs_html_tag("details",
2189                xs_html_attr(collapse_threads ? "" : "open", NULL),
2190                xs_html_tag("summary",
2191                    xs_html_text("...")));
2192
2193            xs_html_add(entry,
2194                ch_details);
2195
2196            xs_html *ch_container = xs_html_tag("div",
2197                xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
2198
2199            xs_html_add(ch_details,
2200                ch_container);
2201
2202            xs_html *ch_older = NULL;
2203            if (left > 3) {
2204                xs_html_add(ch_container,
2205                    ch_older = xs_html_tag("details",
2206                        xs_html_tag("summary",
2207                            xs_html_text(L("Older...")))));
2208            }
2209
2210            xs_list *p = children;
2211            const char *cmd5;
2212            int cnt = 0;
2213            int o_cnt = 0;
2214
2215            while (xs_list_iter(&p, &cmd5)) {
2216                xs *chd = NULL;
2217
2218                if (user)
2219                    timeline_get_by_md5(user, cmd5, &chd);
2220                else
2221                    object_get_by_md5(cmd5, &chd);
2222
2223                if (chd != NULL) {
2224                    if (xs_is_null(xs_dict_get(chd, "name"))) {
2225                        xs_html *che = html_entry(user, chd, read_only,
2226                            level + 1, cmd5, hide_children);
2227
2228                        if (che != NULL) {
2229                            if (left > 3) {
2230                                xs_html_add(ch_older,
2231                                    che);
2232
2233                                o_cnt++;
2234                            }
2235                            else
2236                                xs_html_add(ch_container,
2237                                    che);
2238
2239                            cnt++;
2240                        }
2241                    }
2242
2243                    left--;
2244                }
2245                else
2246                    srv_debug(2, xs_fmt("cannot read child %s", cmd5));
2247            }
2248
2249            /* if no children were finally added, hide the details */
2250            if (cnt == 0)
2251                xs_html_add(ch_details,
2252                    xs_html_attr("style", "display: none"));
2253
2254            if (o_cnt == 0 && ch_older)
2255                xs_html_add(ch_older,
2256                    xs_html_attr("style", "display: none"));
2257        }
2258    }
2259
2260    return entry_top;
2261}
2262
2263
2264xs_html *html_footer(void)
2265{
2266    return xs_html_tag("div",
2267        xs_html_attr("class", "snac-footer"),
2268        xs_html_tag("a",
2269            xs_html_attr("href", srv_baseurl),
2270            xs_html_text(L("about this site"))),
2271        xs_html_text(" - "),
2272        xs_html_text(L("powered by ")),
2273        xs_html_tag("a",
2274            xs_html_attr("href", WHAT_IS_SNAC_URL),
2275            xs_html_tag("abbr",
2276                xs_html_attr("title", "Social Networks Are Crap"),
2277                xs_html_text("snac"))));
2278}
2279
2280
2281xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2282                      int skip, int show, int show_more,
2283                      const char *title, const char *page,
2284                      int utl, const char *error)
2285/* returns the HTML for the timeline */
2286{
2287    xs_list *p = (xs_list *)list;
2288    const char *v;
2289    double t = ftime();
2290    int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
2291
2292    xs *desc = NULL;
2293    xs *alternate = NULL;
2294
2295    if (xs_list_len(list) == 1) {
2296        /* only one element? pick the description from the source */
2297        const char *id = xs_list_get(list, 0);
2298        xs *d = NULL;
2299        object_get_by_md5(id, &d);
2300        const char *sc = xs_dict_get(d, "sourceContent");
2301        if (d && sc != NULL)
2302            desc = xs_dup(sc);
2303
2304        alternate = xs_dup(xs_dict_get(d, "id"));
2305    }
2306
2307    xs_html *head;
2308    xs_html *body;
2309
2310    if (user) {
2311        head = html_user_head(user, desc, alternate);
2312        body = html_user_body(user, read_only);
2313    }
2314    else {
2315        head = html_instance_head();
2316        body = html_instance_body();
2317    }
2318
2319    xs_html *html = xs_html_tag("html",
2320        head,
2321        body);
2322
2323    if (user && !read_only)
2324        xs_html_add(body,
2325            html_top_controls(user));
2326
2327    if (error != NULL) {
2328        xs_html_add(body,
2329            xs_html_tag("dialog",
2330                xs_html_attr("open", NULL),
2331                xs_html_tag("p",
2332                    xs_html_text(error)),
2333                xs_html_tag("form",
2334                    xs_html_attr("method", "dialog"),
2335                    xs_html_sctag("input",
2336                        xs_html_attr("type", "submit"),
2337                        xs_html_attr("value", L("Dismiss"))))));
2338    }
2339
2340    /* show links to the available lists */
2341    if (user && !read_only) {
2342        xs_html *lol = xs_html_tag("ul",
2343            xs_html_attr("class", "snac-list-of-lists"));
2344        xs_html_add(body, lol);
2345
2346        xs *lists = list_maint(user, NULL, 0); /* get list of lists */
2347
2348        int ct = 0;
2349        const char *v;
2350
2351        while (xs_list_next(lists, &v, &ct)) {
2352            const char *lname = xs_list_get(v, 1);
2353            xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
2354            xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
2355
2356            xs_html_add(lol,
2357                xs_html_tag("li",
2358                    xs_html_tag("a",
2359                        xs_html_attr("href", url),
2360                        xs_html_attr("class", "snac-list-link"),
2361                        xs_html_attr("title", ttl),
2362                        xs_html_text(lname))));
2363        }
2364
2365        {
2366            /* show the list of pinned posts */
2367            xs *url = xs_fmt("%s/pinned", user->actor);
2368            xs_html_add(lol,
2369                xs_html_tag("li",
2370                    xs_html_tag("a",
2371                        xs_html_attr("href", url),
2372                        xs_html_attr("class", "snac-list-link"),
2373                        xs_html_attr("title", L("Pinned posts")),
2374                        xs_html_text("pinned"))));
2375        }
2376
2377        {
2378            /* show the list of bookmarked posts */
2379            xs *url = xs_fmt("%s/bookmarks", user->actor);
2380            xs_html_add(lol,
2381                xs_html_tag("li",
2382                    xs_html_tag("a",
2383                        xs_html_attr("href", url),
2384                        xs_html_attr("class", "snac-list-link"),
2385                        xs_html_attr("title", L("Bookmarked posts")),
2386                        xs_html_text("bookmarks"))));
2387        }
2388
2389        {
2390            /* show the list of drafts */
2391            xs *url = xs_fmt("%s/drafts", user->actor);
2392            xs_html_add(lol,
2393                xs_html_tag("li",
2394                    xs_html_tag("a",
2395                        xs_html_attr("href", url),
2396                        xs_html_attr("class", "snac-list-link"),
2397                        xs_html_attr("title", L("Post drafts")),
2398                        xs_html_text("drafts"))));
2399        }
2400    }
2401
2402    xs_html_add(body,
2403        xs_html_tag("a",
2404            xs_html_attr("name", "snac-posts")));
2405
2406    xs_html *posts = xs_html_tag("div",
2407        xs_html_attr("class", "snac-posts"));
2408
2409    if (title) {
2410        xs_html_add(posts,
2411            xs_html_tag("h2",
2412                xs_html_attr("class", "snac-header"),
2413                xs_html_text(title)));
2414    }
2415
2416    xs_html_add(body,
2417        posts);
2418
2419    while (xs_list_iter(&p, &v)) {
2420        xs *msg = NULL;
2421        int status;
2422
2423        if (utl && user && !is_pinned_by_md5(user, v))
2424            status = timeline_get_by_md5(user, v, &msg);
2425        else
2426            status = object_get_by_md5(v, &msg);
2427
2428        if (!valid_status(status))
2429            continue;
2430
2431        /* if it's an instance page, discard messages from private users */
2432        if (user == NULL && is_msg_from_private_user(msg))
2433            continue;
2434
2435        /* is this message a non-public reply? */
2436        if (user != NULL && !is_msg_public(msg)) {
2437            const char *irt = get_in_reply_to(msg);
2438
2439            /* is it a reply to something not in the storage? */
2440            if (!xs_is_null(irt) && !object_here(irt)) {
2441                /* is it for me? */
2442                const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
2443                const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
2444
2445                if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
2446                    snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
2447                    continue;
2448                }
2449            }
2450        }
2451
2452        xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
2453
2454        if (entry != NULL)
2455            xs_html_add(posts,
2456                entry);
2457    }
2458
2459    if (list && user && read_only) {
2460        /** history **/
2461        if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE) {
2462            xs_html *ul = xs_html_tag("ul", NULL);
2463
2464            xs_html *history = xs_html_tag("div",
2465                xs_html_attr("class", "snac-history"),
2466                xs_html_tag("p",
2467                    xs_html_attr("class", "snac-history-title"),
2468                    xs_html_text(L("History"))),
2469                    ul);
2470
2471            xs *list = history_list(user);
2472            xs_list *p = list;
2473            const char *v;
2474
2475            while (xs_list_iter(&p, &v)) {
2476                xs *fn  = xs_replace(v, ".html", "");
2477                xs *url = xs_fmt("%s/h/%s", user->actor, v);
2478
2479                xs_html_add(ul,
2480                    xs_html_tag("li",
2481                        xs_html_tag("a",
2482                            xs_html_attr("href", url),
2483                            xs_html_text(fn))));
2484            }
2485
2486            xs_html_add(body,
2487                history);
2488        }
2489    }
2490
2491    {
2492        xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
2493        xs_html_add(body,
2494            xs_html_raw(s1));
2495    }
2496
2497    if (show_more) {
2498        xs *m  = NULL;
2499        xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
2500
2501        xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
2502
2503        if (page != NULL)
2504            url = xs_str_cat(url, page);
2505
2506        if (xs_str_in(url, "?") != -1)
2507            m = xs_fmt("%s&%s", url, ss);
2508        else
2509            m = xs_fmt("%s?%s", url, ss);
2510
2511        xs_html *more_links = xs_html_tag("p",
2512            xs_html_tag("a",
2513                xs_html_attr("href", url),
2514                xs_html_attr("name", "snac-more"),
2515                xs_html_text(L("Back to top"))),
2516            xs_html_text(" - "),
2517            xs_html_tag("a",
2518                xs_html_attr("href", m),
2519                xs_html_attr("name", "snac-more"),
2520                xs_html_text(L("More..."))));
2521
2522        xs_html_add(body,
2523            more_links);
2524    }
2525
2526    xs_html_add(body,
2527        html_footer());
2528
2529    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2530}
2531
2532
2533xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, const char *proxy)
2534{
2535    xs_html *snac_posts;
2536    xs_html *people = xs_html_tag("div",
2537        xs_html_tag("h2",
2538            xs_html_attr("class", "snac-header"),
2539            xs_html_text(header)),
2540        snac_posts = xs_html_tag("details",
2541                xs_html_attr("open", NULL),
2542                xs_html_tag("summary",
2543                    xs_html_text("..."))));
2544
2545    const char *actor_id;
2546
2547    xs_list_foreach(list, actor_id) {
2548        xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
2549        xs *actor = NULL;
2550
2551        if (valid_status(actor_get(actor_id, &actor))) {
2552            xs_html *snac_post = xs_html_tag("div",
2553                xs_html_attr("class", "snac-post"),
2554                xs_html_tag("a",
2555                    xs_html_attr("name", md5)),
2556                xs_html_tag("div",
2557                    xs_html_attr("class", "snac-post-header"),
2558                    html_actor_icon(snac, actor, xs_dict_get(actor, "published"),
2559                                    NULL, NULL, 0, 1, proxy)));
2560
2561            /* content (user bio) */
2562            const char *c = xs_dict_get(actor, "summary");
2563
2564            if (!xs_is_null(c)) {
2565                xs *sc = sanitize(c);
2566
2567                xs_html *snac_content = xs_html_tag("div",
2568                    xs_html_attr("class", "snac-content"));
2569
2570                if (xs_startswith(sc, "<p>"))
2571                    xs_html_add(snac_content,
2572                        xs_html_raw(sc)); /* already sanitized */
2573                else
2574                    xs_html_add(snac_content,
2575                        xs_html_tag("p",
2576                            xs_html_raw(sc))); /* already sanitized */
2577
2578                xs_html_add(snac_post, snac_content);
2579            }
2580
2581            /* buttons */
2582            xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor);
2583
2584            xs_html *snac_controls = xs_html_tag("div",
2585                xs_html_attr("class",   "snac-controls"));
2586
2587            xs_html *form = xs_html_tag("form",
2588                xs_html_attr("autocomplete",    "off"),
2589                xs_html_attr("method",          "post"),
2590                xs_html_attr("action",          btn_form_action),
2591                xs_html_sctag("input",
2592                    xs_html_attr("type",    "hidden"),
2593                    xs_html_attr("name",    "actor"),
2594                    xs_html_attr("value",   actor_id)),
2595                xs_html_sctag("input",
2596                    xs_html_attr("type",    "hidden"),
2597                    xs_html_attr("name",    "actor-form"),
2598                    xs_html_attr("value",   "yes")));
2599
2600            xs_html_add(snac_controls, form);
2601
2602            if (following_check(snac, actor_id)) {
2603                xs_html_add(form,
2604                    html_button("unfollow", L("Unfollow"),
2605                                L("Stop following this user's activity")));
2606
2607                if (is_limited(snac, actor_id))
2608                    xs_html_add(form,
2609                        html_button("unlimit", L("Unlimit"),
2610                                L("Allow announces (boosts) from this user")));
2611                else
2612                    xs_html_add(form,
2613                        html_button("limit", L("Limit"),
2614                                L("Block announces (boosts) from this user")));
2615            }
2616            else {
2617                xs_html_add(form,
2618                    html_button("follow", L("Follow"),
2619                                L("Start following this user's activity")));
2620
2621                if (follower_check(snac, actor_id))
2622                    xs_html_add(form,
2623                        html_button("delete", L("Delete"), L("Delete this user")));
2624            }
2625
2626            if (pending_check(snac, actor_id)) {
2627                xs_html_add(form,
2628                    html_button("approve", L("Approve"),
2629                                L("Approve this follow request")));
2630
2631                xs_html_add(form,
2632                    html_button("discard", L("Discard"), L("Discard this follow request")));
2633            }
2634
2635            if (is_muted(snac, actor_id))
2636                xs_html_add(form,
2637                    html_button("unmute", L("Unmute"),
2638                                L("Stop blocking activities from this user")));
2639            else
2640                xs_html_add(form,
2641                    html_button("mute", L("MUTE"),
2642                                L("Block any activity from this user")));
2643
2644            /* the post textarea */
2645            xs *dm_div_id  = xs_fmt("%s_%s_dm", md5, t);
2646            xs *dm_form_id = xs_fmt("%s_reply_form", md5);
2647
2648            xs_html_add(snac_controls,
2649                xs_html_tag("p", NULL),
2650                html_note(snac, L("Direct Message..."),
2651                    dm_div_id, dm_form_id,
2652                    "", "",
2653                    NULL, actor_id,
2654                    xs_stock(XSTYPE_FALSE), "",
2655                    xs_stock(XSTYPE_FALSE), NULL,
2656                    NULL, 0, "", "", 0),
2657                xs_html_tag("p", NULL));
2658
2659            xs_html_add(snac_post, snac_controls);
2660
2661            xs_html_add(snac_posts, snac_post);
2662        }
2663    }
2664
2665    return people;
2666}
2667
2668
2669xs_str *html_people(snac *user)
2670{
2671    const char *proxy = NULL;
2672
2673    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2674        proxy = user->actor;
2675
2676    xs *wing = following_list(user);
2677    xs *wers = follower_list(user);
2678
2679    xs_html *lists = xs_html_tag("div",
2680        xs_html_attr("class", "snac-posts"));
2681
2682    if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
2683        xs *pending = pending_list(user);
2684        xs_html_add(lists,
2685            html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
2686    }
2687
2688    xs_html_add(lists,
2689        html_people_list(user, wing, L("People you follow"), "i", proxy),
2690        html_people_list(user, wers, L("People that follow you"), "e", proxy));
2691
2692    xs_html *html = xs_html_tag("html",
2693        html_user_head(user, NULL, NULL),
2694        xs_html_add(html_user_body(user, 0),
2695            lists,
2696            html_footer()));
2697
2698    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2699}
2700
2701
2702xs_str *html_notifications(snac *user, int skip, int show)
2703{
2704    const char *proxy = NULL;
2705
2706    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2707        proxy = user->actor;
2708
2709    xs *n_list = notify_list(user, skip, show);
2710    xs *n_time = notify_check_time(user, 0);
2711
2712    xs_html *body = html_user_body(user, 0);
2713
2714    xs_html *html = xs_html_tag("html",
2715        html_user_head(user, NULL, NULL),
2716        body);
2717
2718    xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
2719
2720    xs_html_add(body,
2721        xs_html_tag("form",
2722            xs_html_attr("autocomplete", "off"),
2723            xs_html_attr("method",       "post"),
2724            xs_html_attr("action",       clear_all_action),
2725            xs_html_attr("id",           "clear"),
2726            xs_html_sctag("input",
2727                xs_html_attr("type",     "submit"),
2728                xs_html_attr("class",    "snac-btn-like"),
2729                xs_html_attr("value",    L("Clear all")))));
2730
2731    xs_html *noti_new = NULL;
2732    xs_html *noti_seen = NULL;
2733
2734    xs_html *posts = xs_html_tag("div",
2735        xs_html_attr("class", "snac-posts"));
2736    xs_html_add(body, posts);
2737
2738    xs_list *p = n_list;
2739    const xs_str *v;
2740    while (xs_list_iter(&p, &v)) {
2741        xs *noti = notify_get(user, v);
2742
2743        if (noti == NULL)
2744            continue;
2745
2746        xs *obj = NULL;
2747        const char *type  = xs_dict_get(noti, "type");
2748        const char *utype = xs_dict_get(noti, "utype");
2749        const char *id    = xs_dict_get(noti, "objid");
2750        const char *date  = xs_dict_get(noti, "date");
2751        xs *wrk = NULL;
2752
2753        if (xs_is_null(id))
2754            continue;
2755
2756        if (is_hidden(user, id))
2757            continue;
2758
2759        object_get(id, &obj);
2760
2761        const char *actor_id = xs_dict_get(noti, "actor");
2762        xs *actor = NULL;
2763
2764        if (!valid_status(actor_get(actor_id, &actor)))
2765            continue;
2766
2767        xs *a_name = actor_name(actor, proxy);
2768        const char *label = type;
2769
2770        if (strcmp(type, "Create") == 0)
2771            label = L("Mention");
2772        else
2773        if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
2774            label = L("Finished poll");
2775        else
2776        if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
2777            label = L("Unfollow");
2778        else
2779        if (strcmp(type, "EmojiReact") == 0) {
2780            const char *content = xs_dict_get_path(noti, "msg.content");
2781
2782            if (xs_type(content) == XSTYPE_STRING) {
2783                wrk = xs_fmt("%s (%s)", type, content);
2784                label = wrk;
2785            }
2786        }
2787        else
2788        if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
2789            label = L("Follow Request");
2790
2791        xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
2792
2793        xs_html *entry = xs_html_tag("div",
2794            xs_html_attr("class", "snac-post-with-desc"),
2795            xs_html_tag("p",
2796                xs_html_tag("b",
2797                    xs_html_text(label),
2798                    xs_html_text(" by "),
2799                    xs_html_tag("a",
2800                        xs_html_attr("href", actor_id),
2801                        xs_html_raw(a_name))), /* a_name is already sanitized */
2802                xs_html_text(" "),
2803                xs_html_tag("time",
2804                    xs_html_attr("class", "dt-published snac-pubdate"),
2805                    xs_html_attr("title", date),
2806                    xs_html_text(s_date))));
2807
2808        if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) {
2809            xs_html_add(entry,
2810                xs_html_tag("div",
2811                    xs_html_attr("class", "snac-post"),
2812                    html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy)));
2813        }
2814        else
2815        if (strcmp(type, "Move") == 0) {
2816            const xs_dict *o_msg = xs_dict_get(noti, "msg");
2817            const char *target;
2818
2819            if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
2820                xs *old_actor = NULL;
2821
2822                if (valid_status(actor_get(target, &old_actor))) {
2823                    xs_html_add(entry,
2824                        xs_html_tag("div",
2825                            xs_html_attr("class", "snac-post"),
2826                            html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy)));
2827                }
2828            }
2829        }
2830        else
2831        if (obj != NULL) {
2832            xs *md5 = xs_md5_hex(id, strlen(id));
2833
2834            xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
2835
2836            if (h != NULL) {
2837                xs_html_add(entry,
2838                    h);
2839            }
2840        }
2841
2842        if (strcmp(v, n_time) > 0) {
2843            /* unseen notification */
2844            if (noti_new == NULL) {
2845                noti_new = xs_html_tag("div",
2846                    xs_html_tag("h2",
2847                        xs_html_attr("class", "snac-header"),
2848                        xs_html_text(L("New"))));
2849
2850                xs_html_add(posts,
2851                    noti_new);
2852            }
2853
2854            xs_html_add(noti_new,
2855                entry);
2856        }
2857        else {
2858            /* already seen notification */
2859            if (noti_seen == NULL) {
2860                noti_seen = xs_html_tag("div",
2861                    xs_html_tag("h2",
2862                        xs_html_attr("class", "snac-header"),
2863                        xs_html_text(L("Already seen"))));
2864
2865                xs_html_add(posts,
2866                    noti_seen);
2867            }
2868
2869            xs_html_add(noti_seen,
2870                entry);
2871        }
2872    }
2873
2874    if (noti_new == NULL && noti_seen == NULL)
2875        xs_html_add(body,
2876            xs_html_tag("h2",
2877                xs_html_attr("class", "snac-header"),
2878                xs_html_text(L("None"))));
2879
2880    /* add the navigation footer */
2881    xs *next_p = notify_list(user, skip + show, 1);
2882    if (xs_list_len(next_p)) {
2883        xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
2884            user->actor, skip + show, show);
2885
2886        xs_html_add(body,
2887            xs_html_tag("p",
2888                xs_html_tag("a",
2889                    xs_html_attr("href", url),
2890                    xs_html_text(L("More...")))));
2891    }
2892
2893    xs_html_add(body,
2894        html_footer());
2895
2896    /* set the check time to now */
2897    xs *dummy = notify_check_time(user, 1);
2898    dummy = xs_free(dummy);
2899
2900    timeline_touch(user);
2901
2902    return xs_html_render_s(html, "<!DOCTYPE html>\n");
2903}
2904
2905
2906int html_get_handler(const xs_dict *req, const char *q_path,
2907                     char **body, int *b_size, char **ctype,
2908                     xs_str **etag, xs_str **last_modified)
2909{
2910    const char *accept = xs_dict_get(req, "accept");
2911    int status = HTTP_STATUS_NOT_FOUND;
2912    snac snac;
2913    xs *uid = NULL;
2914    const char *p_path;
2915    int cache = 1;
2916    int save = 1;
2917    int proxy = 0;
2918    const char *v;
2919
2920    xs *l = xs_split_n(q_path, "/", 2);
2921    v = xs_list_get(l, 1);
2922
2923    if (xs_is_null(v)) {
2924        srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
2925        return HTTP_STATUS_NOT_FOUND;
2926    }
2927
2928    uid = xs_dup(v);
2929
2930    /* rss extension? */
2931    if (xs_endswith(uid, ".rss")) {
2932        uid = xs_crop_i(uid, 0, -4);
2933        p_path = ".rss";
2934    }
2935    else
2936        p_path = xs_list_get(l, 2);
2937
2938    if (!uid || !user_open(&snac, uid)) {
2939        /* invalid user */
2940        srv_debug(1, xs_fmt("html_get_handler bad user %s", uid));
2941        return HTTP_STATUS_NOT_FOUND;
2942    }
2943
2944    if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2945        proxy = 1;
2946
2947    /* return the RSS if requested by Accept header */
2948    if (accept != NULL) {
2949        if (xs_str_in(accept, "text/xml") != -1 ||
2950            xs_str_in(accept, "application/rss+xml") != -1)
2951            p_path = ".rss";
2952    }
2953
2954    /* check if server config variable 'disable_cache' is set */
2955    if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
2956        cache = 0;
2957
2958    int skip = 0;
2959    int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries"));
2960    int show = def_show;
2961
2962    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
2963    if ((v = xs_dict_get(q_vars, "skip")) != NULL)
2964        skip = atoi(v), cache = 0, save = 0;
2965    if ((v = xs_dict_get(q_vars, "show")) != NULL)
2966        show = atoi(v), cache = 0, save = 0;
2967    if ((v = xs_dict_get(q_vars, "da")) != NULL) {
2968        /* user dismissed an announcement */
2969        if (login(&snac, req)) {
2970            double ts = atof(v);
2971            xs *timestamp = xs_number_new(ts);
2972            srv_log(xs_fmt("user dismissed announcements until %d", ts));
2973            snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
2974            user_persist(&snac, 0);
2975        }
2976    }
2977
2978    /* get a possible error message */
2979    const char *error = xs_dict_get(q_vars, "error");
2980    if (error != NULL)
2981        cache = 0;
2982
2983    /* a show of 0 has no sense */
2984    if (show == 0)
2985        show = def_show;
2986
2987    if (p_path == NULL) { /** public timeline **/
2988        xs *h = xs_str_localtime(0, "%Y-%m.html");
2989
2990        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
2991            /** empty public timeline for private users **/
2992            *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error);
2993            *b_size = strlen(*body);
2994            status  = HTTP_STATUS_OK;
2995        }
2996        else
2997        if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
2998            snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
2999
3000            status = history_get(&snac, h, body, b_size,
3001                        xs_dict_get(req, "if-none-match"), etag);
3002        }
3003        else {
3004            xs *list = NULL;
3005            xs *next = NULL;
3006
3007            if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) {
3008                list = timeline_simple_list(&snac, "public", skip, show);
3009                next = timeline_simple_list(&snac, "public", skip + show, 1);
3010            }
3011            else {
3012                list = timeline_list(&snac, "public", skip, show);
3013                next = timeline_list(&snac, "public", skip + show, 1);
3014            }
3015
3016            xs *pins = pinned_list(&snac);
3017            pins = xs_list_cat(pins, list);
3018
3019            *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error);
3020
3021            *b_size = strlen(*body);
3022            status  = HTTP_STATUS_OK;
3023
3024            if (save)
3025                history_add(&snac, h, *body, *b_size, etag);
3026        }
3027    }
3028    else
3029    if (strcmp(p_path, "admin") == 0) { /** private timeline **/
3030        if (!login(&snac, req)) {
3031            *body  = xs_dup(uid);
3032            status = HTTP_STATUS_UNAUTHORIZED;
3033        }
3034        else {
3035            const char *q = xs_dict_get(q_vars, "q");
3036            xs *url_acct = NULL;
3037
3038            /* searching for an URL? */
3039            if (q && xs_match(q, "https://*|http://*")) {
3040                /* may by an actor; try a webfinger */
3041                xs *actor_obj = NULL;
3042
3043                if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) {
3044                    /* it's an actor; do the dirty trick of changing q to the account name */
3045                    q = url_acct;
3046                }
3047                else {
3048                    /* if it's not already here, try to bring it to the user's timeline */
3049                    xs *md5 = xs_md5_hex(q, strlen(q));
3050
3051                    if (!timeline_here(&snac, md5)) {
3052                        xs *object = NULL;
3053                        int status;
3054
3055                        status = activitypub_request(&snac, q, &object);
3056                        snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
3057
3058                        if (valid_status(status)) {
3059                            /* got it; also request the actor */
3060                            const char *attr_to = get_atto(object);
3061
3062                            if (!xs_is_null(attr_to)) {
3063                                status = actor_request(&snac, attr_to, &actor_obj);
3064
3065                                snac_debug(&snac, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status));
3066
3067                                if (valid_status(status)) {
3068                                    /* add the actor */
3069                                    actor_add(attr_to, actor_obj);
3070
3071                                    /* add the post to the timeline */
3072                                    timeline_add(&snac, q, object);
3073                                }
3074                            }
3075                        }
3076                    }
3077                }
3078
3079                /* fall through */
3080            }
3081
3082            if (q && *q) {
3083                if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
3084                    /** search account **/
3085                    xs *actor = NULL;
3086                    xs *acct = NULL;
3087                    xs *l = xs_list_new();
3088                    xs_html *page = NULL;
3089
3090                    if (valid_status(webfinger_request(q, &actor, &acct))) {
3091                        xs *actor_obj = NULL;
3092
3093                        if (valid_status(actor_request(&snac, actor, &actor_obj))) {
3094                            actor_add(actor, actor_obj);
3095
3096                            /* create a people list with only one element */
3097                            l = xs_list_append(xs_list_new(), actor);
3098
3099                            xs *title = xs_fmt(L("Search results for account %s"), q);
3100
3101                            page = html_people_list(&snac, l, title, "wf", NULL);
3102                        }
3103                    }
3104
3105                    if (page == NULL) {
3106                        xs *title = xs_fmt(L("Account %s not found"), q);
3107
3108                        page = xs_html_tag("div",
3109                            xs_html_tag("h2",
3110                                xs_html_attr("class", "snac-header"),
3111                                xs_html_text(title)));
3112                    }
3113
3114                    xs_html *html = xs_html_tag("html",
3115                        html_user_head(&snac, NULL, NULL),
3116                        xs_html_add(html_user_body(&snac, 0),
3117                        page,
3118                        html_footer()));
3119
3120                    *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
3121                    *b_size = strlen(*body);
3122                    status = HTTP_STATUS_OK;
3123                }
3124                else
3125                if (*q == '#') {
3126                    /** search by tag **/
3127                    xs *tl = tag_search(q, skip, show + 1);
3128                    int more = 0;
3129                    if (xs_list_len(tl) >= show + 1) {
3130                        /* drop the last one */
3131                        tl = xs_list_del(tl, -1);
3132                        more = 1;
3133                    }
3134
3135                    xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
3136                    xs *title = xs_fmt(xs_list_len(tl) ?
3137                        L("Search results for tag %s") : L("Nothing found for tag %s"), q);
3138
3139                    *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error);
3140                    *b_size = strlen(*body);
3141                    status  = HTTP_STATUS_OK;
3142                }
3143                else {
3144                    /** search by content **/
3145                    int to = 0;
3146                    int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
3147                    xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
3148                    xs *title = NULL;
3149                    xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
3150                    int tl_len = xs_list_len(tl);
3151
3152                    if (to)
3153                        title = xs_fmt(L("Search results for '%s' (may be more)"), q);
3154                    else
3155                    if (tl_len)
3156                        title = xs_fmt(L("Search results for '%s'"), q);
3157                    else
3158                    if (skip)
3159                        title = xs_fmt(L("No more matches for '%s'"), q);
3160                    else
3161                        title = xs_fmt(L("Nothing found for '%s'"), q);
3162
3163                    *body   = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
3164                                            title, page, 0, error);
3165                    *b_size = strlen(*body);
3166                    status  = HTTP_STATUS_OK;
3167                }
3168            }
3169            else {
3170                double t = history_mtime(&snac, "timeline.html_");
3171
3172                /* if enabled by admin, return a cached page if its timestamp is:
3173                   a) newer than the timeline timestamp
3174                   b) newer than the start time of the server
3175                */
3176                if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
3177                    snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
3178
3179                    status = history_get(&snac, "timeline.html_", body, b_size,
3180                                xs_dict_get(req, "if-none-match"), etag);
3181                }
3182                else {
3183                    snac_debug(&snac, 1, xs_fmt("building timeline"));
3184
3185                    xs *list = timeline_list(&snac, "private", skip, show);
3186                    xs *next = timeline_list(&snac, "private", skip + show, 1);
3187
3188                    *body = html_timeline(&snac, list, 0, skip, show,
3189                            xs_list_len(next), NULL, "/admin", 1, error);
3190
3191                    *b_size = strlen(*body);
3192                    status  = HTTP_STATUS_OK;
3193
3194                    if (save)
3195                        history_add(&snac, "timeline.html_", *body, *b_size, etag);
3196                }
3197            }
3198        }
3199    }
3200    else
3201    if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
3202        if (!login(&snac, req)) {
3203            *body  = xs_dup(uid);
3204            status = HTTP_STATUS_UNAUTHORIZED;
3205        }
3206        else {
3207            xs *l = xs_split(p_path, "/");
3208            const char *md5 = xs_list_get(l, -1);
3209
3210            if (md5 && *md5 && timeline_here(&snac, md5)) {
3211                xs *list = xs_list_append(xs_list_new(), md5);
3212
3213                *body   = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error);
3214                *b_size = strlen(*body);
3215                status  = HTTP_STATUS_OK;
3216            }
3217        }
3218    }
3219    else
3220    if (strcmp(p_path, "people") == 0) { /** the list of people **/
3221        if (!login(&snac, req)) {
3222            *body  = xs_dup(uid);
3223            status = HTTP_STATUS_UNAUTHORIZED;
3224        }
3225        else {
3226            *body   = html_people(&snac);
3227            *b_size = strlen(*body);
3228            status  = HTTP_STATUS_OK;
3229        }
3230    }
3231    else
3232    if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
3233        if (!login(&snac, req)) {
3234            *body  = xs_dup(uid);
3235            status = HTTP_STATUS_UNAUTHORIZED;
3236        }
3237        else {
3238            *body   = html_notifications(&snac, skip, show);
3239            *b_size = strlen(*body);
3240            status  = HTTP_STATUS_OK;
3241        }
3242    }
3243    else
3244    if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
3245        if (!login(&snac, req)) {
3246            *body  = xs_dup(uid);
3247            status = HTTP_STATUS_UNAUTHORIZED;
3248        }
3249        else {
3250            xs *list = timeline_instance_list(skip, show);
3251            xs *next = timeline_instance_list(skip + show, 1);
3252
3253            *body = html_timeline(&snac, list, 0, skip, show,
3254                xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error);
3255            *b_size = strlen(*body);
3256            status  = HTTP_STATUS_OK;
3257        }
3258    }
3259    else
3260    if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
3261        if (!login(&snac, req)) {
3262            *body  = xs_dup(uid);
3263            status = HTTP_STATUS_UNAUTHORIZED;
3264        }
3265        else {
3266            xs *list = pinned_list(&snac);
3267
3268            *body = html_timeline(&snac, list, 0, skip, show,
3269                0, L("Pinned posts"), "", 0, error);
3270            *b_size = strlen(*body);
3271            status  = HTTP_STATUS_OK;
3272        }
3273    }
3274    else
3275    if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
3276        if (!login(&snac, req)) {
3277            *body  = xs_dup(uid);
3278            status = HTTP_STATUS_UNAUTHORIZED;
3279        }
3280        else {
3281            xs *list = bookmark_list(&snac);
3282
3283            *body = html_timeline(&snac, list, 0, skip, show,
3284                0, L("Bookmarked posts"), "", 0, error);
3285            *b_size = strlen(*body);
3286            status  = HTTP_STATUS_OK;
3287        }
3288    }
3289    else
3290    if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
3291        if (!login(&snac, req)) {
3292            *body  = xs_dup(uid);
3293            status = HTTP_STATUS_UNAUTHORIZED;
3294        }
3295        else {
3296            xs *list = draft_list(&snac);
3297
3298            *body = html_timeline(&snac, list, 0, skip, show,
3299                0, L("Post drafts"), "", 0, error);
3300            *b_size = strlen(*body);
3301            status  = HTTP_STATUS_OK;
3302        }
3303    }
3304    else
3305    if (xs_startswith(p_path, "list/")) { /** list timelines **/
3306        if (!login(&snac, req)) {
3307            *body  = xs_dup(uid);
3308            status = HTTP_STATUS_UNAUTHORIZED;
3309        }
3310        else {
3311            xs *l = xs_split(p_path, "/");
3312            const char *lid = xs_list_get(l, -1);
3313
3314            xs *list = list_timeline(&snac, lid, skip, show);
3315            xs *next = list_timeline(&snac, lid, skip + show, 1);
3316
3317            if (list != NULL) {
3318                xs *ttl = timeline_top_level(&snac, list);
3319
3320                xs *base = xs_fmt("/list/%s", lid);
3321                xs *name = list_maint(&snac, lid, 3);
3322                xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
3323
3324                *body = html_timeline(&snac, ttl, 0, skip, show,
3325                    xs_list_len(next), title, base, 1, error);
3326                *b_size = strlen(*body);
3327                status  = HTTP_STATUS_OK;
3328            }
3329        }
3330    }
3331    else
3332    if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
3333        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3334            return HTTP_STATUS_FORBIDDEN;
3335
3336        xs *id  = xs_fmt("%s/%s", snac.actor, p_path);
3337        xs *msg = NULL;
3338
3339        if (valid_status(object_get(id, &msg))) {
3340            xs *md5  = xs_md5_hex(id, strlen(id));
3341            xs *list = xs_list_new();
3342
3343            list = xs_list_append(list, md5);
3344
3345            *body   = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error);
3346            *b_size = strlen(*body);
3347            status  = HTTP_STATUS_OK;
3348        }
3349    }
3350    else
3351    if (xs_startswith(p_path, "s/")) { /** a static file **/
3352        xs *l    = xs_split(p_path, "/");
3353        const char *id = xs_list_get(l, 1);
3354        int sz;
3355
3356        if (id && *id) {
3357            status = static_get(&snac, id, body, &sz,
3358                        xs_dict_get(req, "if-none-match"), etag);
3359
3360            if (valid_status(status)) {
3361                *b_size = sz;
3362                *ctype  = (char *)xs_mime_by_ext(id);
3363            }
3364        }
3365    }
3366    else
3367    if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
3368        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3369            return HTTP_STATUS_FORBIDDEN;
3370
3371        if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
3372            return HTTP_STATUS_FORBIDDEN;
3373
3374        xs *l = xs_split(p_path, "/");
3375        const char *id = xs_list_get(l, 1);
3376
3377        if (id && *id) {
3378            if (xs_endswith(id, "timeline.html_")) {
3379                /* Don't let them in */
3380                *b_size = 0;
3381                status = HTTP_STATUS_NOT_FOUND;
3382            }
3383            else
3384                status = history_get(&snac, id, body, b_size,
3385                            xs_dict_get(req, "if-none-match"), etag);
3386        }
3387    }
3388    else
3389    if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
3390        if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
3391            return HTTP_STATUS_FORBIDDEN;
3392
3393        xs *elems = timeline_simple_list(&snac, "public", 0, 20);
3394        xs *bio   = xs_dup(xs_dict_get(snac.config, "bio"));
3395
3396        xs *rss_title = xs_fmt("%s (@%s@%s)",
3397            xs_dict_get(snac.config, "name"),
3398            snac.uid,
3399            xs_dict_get(srv_config, "host"));
3400        xs *rss_link = xs_fmt("%s.rss", snac.actor);
3401
3402        *body   = timeline_to_rss(&snac, elems, rss_title, rss_link, bio);
3403        *b_size = strlen(*body);
3404        *ctype  = "application/rss+xml; charset=utf-8";
3405        status  = HTTP_STATUS_OK;
3406
3407        snac_debug(&snac, 1, xs_fmt("serving RSS"));
3408    }
3409    else
3410    if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
3411        xs *proxy_prefix = NULL;
3412
3413        if (xs_startswith(p_path, "x/")) {
3414            /* proxy usage authorized by http basic auth */
3415            if (login(&snac, req))
3416                proxy_prefix = xs_str_new("x/");
3417            else {
3418                *body  = xs_dup(uid);
3419                status = HTTP_STATUS_UNAUTHORIZED;
3420            }
3421        }
3422        else {
3423            /* proxy usage authorized by proxy_token */
3424            xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
3425            xs *tk = xs_md5_hex(tks, strlen(tks));
3426            xs *p = xs_fmt("y/%s/", tk);
3427
3428            if (xs_startswith(p_path, p))
3429                proxy_prefix = xs_dup(p);
3430        }
3431
3432        if (proxy_prefix) {
3433            /* pick the raw path (including optional ? arguments) */
3434            const char *raw_path = xs_dict_get(req, "raw_path");
3435
3436            /* skip to where the proxy/ string starts */
3437            raw_path += xs_str_in(raw_path, proxy_prefix);
3438
3439            xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
3440            xs *hdrs = xs_dict_new();
3441
3442            hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
3443
3444            const char *ims = xs_dict_get(req, "if-modified-since");
3445            const char *inm = xs_dict_get(req, "if-none-match");
3446
3447            if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
3448            if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
3449
3450            xs *rsp = xs_http_request("GET", url, hdrs,
3451                        NULL, 0, &status, body, b_size, 0);
3452
3453            if (valid_status(status)) {
3454                const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
3455                const char *lm = xs_dict_get(rsp, "last-modified");
3456                const char *et = xs_dict_get(rsp, "etag");
3457
3458                if (lm) *last_modified = xs_dup(lm);
3459                if (et) *etag = xs_dup(et);
3460
3461                /* find the content-type in the static mime types,
3462                   and return that value instead of ct, which will
3463                   be destroyed when out of scope */
3464                for (int n = 0; xs_mime_types[n]; n += 2) {
3465                    if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
3466                        *ctype = (char *)xs_mime_types[n + 1];
3467                        break;
3468                    }
3469                }
3470            }
3471
3472            snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
3473        }
3474    }
3475    else
3476        status = HTTP_STATUS_NOT_FOUND;
3477
3478    user_free(&snac);
3479
3480    if (valid_status(status) && *ctype == NULL) {
3481        *ctype = "text/html; charset=utf-8";
3482    }
3483
3484    return status;
3485}
3486
3487
3488int html_post_handler(const xs_dict *req, const char *q_path,
3489                      char *payload, int p_size,
3490                      char **body, int *b_size, char **ctype)
3491{
3492    (void)p_size;
3493    (void)ctype;
3494
3495    int status = 0;
3496    snac snac;
3497    const char *uid;
3498    const char *p_path;
3499    const xs_dict *p_vars;
3500
3501    xs *l = xs_split_n(q_path, "/", 2);
3502
3503    uid = xs_list_get(l, 1);
3504    if (!uid || !user_open(&snac, uid)) {
3505        /* invalid user */
3506        srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
3507        return HTTP_STATUS_NOT_FOUND;
3508    }
3509
3510    p_path = xs_list_get(l, 2);
3511
3512    /* all posts must be authenticated */
3513    if (!login(&snac, req)) {
3514        user_free(&snac);
3515        *body  = xs_dup(uid);
3516        return HTTP_STATUS_UNAUTHORIZED;
3517    }
3518
3519    p_vars = xs_dict_get(req, "p_vars");
3520
3521    if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
3522        snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
3523
3524        /* post note */
3525        const xs_str *content      = xs_dict_get(p_vars, "content");
3526        const xs_str *in_reply_to  = xs_dict_get(p_vars, "in_reply_to");
3527        const xs_str *attach_url   = xs_dict_get(p_vars, "attach_url");
3528        const xs_list *attach_file = xs_dict_get(p_vars, "attach");
3529        const xs_str *to           = xs_dict_get(p_vars, "to");
3530        const xs_str *sensitive    = xs_dict_get(p_vars, "sensitive");
3531        const xs_str *summary      = xs_dict_get(p_vars, "summary");
3532        const xs_str *edit_id      = xs_dict_get(p_vars, "edit_id");
3533        const xs_str *alt_text     = xs_dict_get(p_vars, "alt_text");
3534        int priv             = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
3535        int store_as_draft   = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
3536        xs *attach_list      = xs_list_new();
3537
3538        /* default alt text */
3539        if (xs_is_null(alt_text))
3540            alt_text = "";
3541
3542        /* is attach_url set? */
3543        if (!xs_is_null(attach_url) && *attach_url != '\0') {
3544            xs *l = xs_list_new();
3545
3546            l = xs_list_append(l, attach_url);
3547            l = xs_list_append(l, alt_text);
3548
3549            attach_list = xs_list_append(attach_list, l);
3550        }
3551
3552        /* is attach_file set? */
3553        if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) {
3554            const char *fn = xs_list_get(attach_file, 0);
3555
3556            if (*fn != '\0') {
3557                char *ext = strrchr(fn, '.');
3558                xs *hash  = xs_md5_hex(fn, strlen(fn));
3559                xs *id    = xs_fmt("%s%s", hash, ext);
3560                xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
3561                int fo    = xs_number_get(xs_list_get(attach_file, 1));
3562                int fs    = xs_number_get(xs_list_get(attach_file, 2));
3563
3564                /* store */
3565                static_put(&snac, id, payload + fo, fs);
3566
3567                xs *l = xs_list_new();
3568
3569                l = xs_list_append(l, url);
3570                l = xs_list_append(l, alt_text);
3571
3572                attach_list = xs_list_append(attach_list, l);
3573            }
3574        }
3575
3576        if (content != NULL) {
3577            xs *msg       = NULL;
3578            xs *c_msg     = NULL;
3579            xs *content_2 = xs_replace(content, "\r", "");
3580            xs *poll_opts = NULL;
3581
3582            /* is there a valid set of poll options? */
3583            const char *v = xs_dict_get(p_vars, "poll_options");
3584            if (!xs_is_null(v) && *v) {
3585                xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
3586
3587                poll_opts = xs_split(v2, "\n");
3588            }
3589
3590            if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
3591                /* get the rest of poll configuration */
3592                const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
3593                const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
3594                int multiple = 0;
3595
3596                int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
3597
3598                if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
3599                    multiple = 1;
3600
3601                msg = msg_question(&snac, content_2, attach_list,
3602                                   poll_opts, multiple, end_secs);
3603
3604                enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
3605            }
3606            else
3607                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv);
3608
3609            if (sensitive != NULL) {
3610                msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
3611                msg = xs_dict_set(msg, "summary",   xs_is_null(summary) ? "..." : summary);
3612            }
3613
3614            if (xs_is_null(edit_id)) {
3615                /* new message */
3616                const char *id = xs_dict_get(msg, "id");
3617
3618                if (store_as_draft) {
3619                    draft_add(&snac, id, msg);
3620                }
3621                else {
3622                    c_msg = msg_create(&snac, msg);
3623                    timeline_add(&snac, id, msg);
3624                }
3625            }
3626            else {
3627                /* an edition of a previous message */
3628                xs *p_msg = NULL;
3629
3630                if (valid_status(object_get(edit_id, &p_msg))) {
3631                    /* copy relevant fields from previous version */
3632                    char *fields[] = { "id", "context", "url", "published",
3633                                       "to", "inReplyTo", NULL };
3634                    int n;
3635
3636                    for (n = 0; fields[n]; n++) {
3637                        const char *v = xs_dict_get(p_msg, fields[n]);
3638                        msg = xs_dict_set(msg, fields[n], v);
3639                    }
3640
3641                    if (store_as_draft) {
3642                        draft_add(&snac, edit_id, msg);
3643                    }
3644                    else
3645                    if (is_draft(&snac, edit_id)) {
3646                        /* message was previously a draft; it's a create activity */
3647
3648                        /* set the published field to now */
3649                        xs *published = xs_str_utctime(0, ISO_DATE_SPEC);
3650                        msg = xs_dict_set(msg, "published", published);
3651
3652                        /* overwrite object */
3653                        object_add_ow(edit_id, msg);
3654
3655                        c_msg = msg_create(&snac, msg);
3656                        timeline_add(&snac, edit_id, msg);
3657                        draft_del(&snac, edit_id);
3658                    }
3659                    else {
3660                        /* set the updated field */
3661                        xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
3662                        msg = xs_dict_set(msg, "updated", updated);
3663
3664                        /* overwrite object, not updating the indexes */
3665                        object_add_ow(edit_id, msg);
3666
3667                        /* update message */
3668                        c_msg = msg_update(&snac, msg);
3669                    }
3670                }
3671                else
3672                    snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
3673            }
3674
3675            if (c_msg != NULL)
3676                enqueue_message(&snac, c_msg);
3677
3678            history_del(&snac, "timeline.html_");
3679        }
3680
3681        status = HTTP_STATUS_SEE_OTHER;
3682    }
3683    else
3684    if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
3685        /* action on an entry */
3686        const char *id     = xs_dict_get(p_vars, "id");
3687        const char *actor  = xs_dict_get(p_vars, "actor");
3688        const char *action = xs_dict_get(p_vars, "action");
3689        const char *group  = xs_dict_get(p_vars, "group");
3690
3691        if (action == NULL)
3692            return HTTP_STATUS_NOT_FOUND;
3693
3694        snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
3695
3696        status = HTTP_STATUS_SEE_OTHER;
3697
3698        if (strcmp(action, L("Like")) == 0) { /** **/
3699            xs *msg = msg_admiration(&snac, id, "Like");
3700
3701            if (msg != NULL) {
3702                enqueue_message(&snac, msg);
3703                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1);
3704            }
3705        }
3706        else
3707        if (strcmp(action, L("Boost")) == 0) { /** **/
3708            xs *msg = msg_admiration(&snac, id, "Announce");
3709
3710            if (msg != NULL) {
3711                enqueue_message(&snac, msg);
3712                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
3713            }
3714        }
3715        else
3716        if (strcmp(action, L("Unlike")) == 0) { /** **/
3717            xs *msg = msg_repulsion(&snac, id, "Like");
3718
3719            if (msg != NULL) {
3720                enqueue_message(&snac, msg);
3721            }
3722        }
3723        else
3724        if (strcmp(action, L("Unboost")) == 0) { /** **/
3725            xs *msg = msg_repulsion(&snac, id, "Announce");
3726
3727            if (msg != NULL) {
3728                enqueue_message(&snac, msg);
3729            }
3730        }
3731        else
3732        if (strcmp(action, L("MUTE")) == 0) { /** **/
3733            mute(&snac, actor);
3734        }
3735        else
3736        if (strcmp(action, L("Unmute")) == 0) { /** **/
3737            unmute(&snac, actor);
3738        }
3739        else
3740        if (strcmp(action, L("Hide")) == 0) { /** **/
3741            if (is_draft(&snac, id))
3742                draft_del(&snac, id);
3743            else
3744                hide(&snac, id);
3745        }
3746        else
3747        if (strcmp(action, L("Limit")) == 0) { /** **/
3748            limit(&snac, actor);
3749        }
3750        else
3751        if (strcmp(action, L("Unlimit")) == 0) { /** **/
3752            unlimit(&snac, actor);
3753        }
3754        else
3755        if (strcmp(action, L("Follow")) == 0) { /** **/
3756            xs *msg = msg_follow(&snac, actor);
3757
3758            if (msg != NULL) {
3759                /* reload the actor from the message, in may be different */
3760                actor = xs_dict_get(msg, "object");
3761
3762                following_add(&snac, actor, msg);
3763
3764                enqueue_output_by_actor(&snac, msg, actor, 0);
3765            }
3766        }
3767        else
3768        if (strcmp(action, L("Unfollow")) == 0) { /** **/
3769            /* get the following object */
3770            xs *object = NULL;
3771
3772            if (valid_status(following_get(&snac, actor, &object))) {
3773                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3774
3775                following_del(&snac, actor);
3776
3777                enqueue_output_by_actor(&snac, msg, actor, 0);
3778
3779                snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
3780            }
3781            else
3782                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
3783        }
3784        else
3785        if (strcmp(action, L("Follow Group")) == 0) { /** **/
3786            xs *msg = msg_follow(&snac, group);
3787
3788            if (msg != NULL) {
3789                /* reload the group from the message, in may be different */
3790                group = xs_dict_get(msg, "object");
3791
3792                following_add(&snac, group, msg);
3793
3794                enqueue_output_by_actor(&snac, msg, group, 0);
3795            }
3796        }
3797        else
3798        if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
3799            /* get the following object */
3800            xs *object = NULL;
3801
3802            if (valid_status(following_get(&snac, group, &object))) {
3803                xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
3804
3805                following_del(&snac, group);
3806
3807                enqueue_output_by_actor(&snac, msg, group, 0);
3808
3809                snac_log(&snac, xs_fmt("unfollowed group %s", group));
3810            }
3811            else
3812                snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
3813        }
3814        else
3815        if (strcmp(action, L("Delete")) == 0) { /** **/
3816            const char *actor_form = xs_dict_get(p_vars, "actor-form");
3817            if (actor_form != NULL) {
3818                /* delete follower */
3819                if (valid_status(follower_del(&snac, actor)))
3820                    snac_log(&snac, xs_fmt("deleted follower %s", actor));
3821                else
3822                    snac_log(&snac, xs_fmt("error deleting follower %s", actor));
3823            }
3824            else {
3825                /* delete an entry */
3826                if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) {
3827                    /* it's a post by us: generate a delete */
3828                    xs *msg = msg_delete(&snac, id);
3829
3830                    enqueue_message(&snac, msg);
3831
3832                    snac_log(&snac, xs_fmt("posted tombstone for %s", id));
3833                }
3834
3835                timeline_del(&snac, id);
3836
3837                draft_del(&snac, id);
3838
3839                snac_log(&snac, xs_fmt("deleted entry %s", id));
3840            }
3841        }
3842        else
3843        if (strcmp(action, L("Pin")) == 0) { /** **/
3844            pin(&snac, id);
3845            timeline_touch(&snac);
3846        }
3847        else
3848        if (strcmp(action, L("Unpin")) == 0) { /** **/
3849            unpin(&snac, id);
3850            timeline_touch(&snac);
3851        }
3852        else
3853        if (strcmp(action, L("Bookmark")) == 0) { /** **/
3854            bookmark(&snac, id);
3855            timeline_touch(&snac);
3856        }
3857        else
3858        if (strcmp(action, L("Unbookmark")) == 0) { /** **/
3859            unbookmark(&snac, id);
3860            timeline_touch(&snac);
3861        }
3862        else
3863        if (strcmp(action, L("Approve")) == 0) { /** **/
3864            xs *fwreq = pending_get(&snac, actor);
3865
3866            if (fwreq != NULL) {
3867                xs *reply = msg_accept(&snac, fwreq, actor);
3868
3869                enqueue_message(&snac, reply);
3870
3871                if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3872                    /* add a date if it doesn't include one (Mastodon) */
3873                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3874                    fwreq = xs_dict_set(fwreq, "published", date);
3875                }
3876
3877                timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3878
3879                follower_add(&snac, actor);
3880
3881                pending_del(&snac, actor);
3882
3883                snac_log(&snac, xs_fmt("new follower %s", actor));
3884            }
3885        }
3886        else
3887        if (strcmp(action, L("Discard")) == 0) { /** **/
3888            pending_del(&snac, actor);
3889        }
3890        else
3891            status = HTTP_STATUS_NOT_FOUND;
3892
3893        /* delete the cached timeline */
3894        if (status == HTTP_STATUS_SEE_OTHER)
3895            history_del(&snac, "timeline.html_");
3896    }
3897    else
3898    if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
3899        /* change of user data */
3900        const char *v;
3901        const char *p1, *p2;
3902
3903        if ((v = xs_dict_get(p_vars, "name")) != NULL)
3904            snac.config = xs_dict_set(snac.config, "name", v);
3905        if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
3906            snac.config = xs_dict_set(snac.config, "avatar", v);
3907        if ((v = xs_dict_get(p_vars, "bio")) != NULL)
3908            snac.config = xs_dict_set(snac.config, "bio", v);
3909        if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
3910            strcmp(v, "on") == 0) {
3911            snac.config = xs_dict_set(snac.config, "cw", "open");
3912        } else { /* if the checkbox is not set, the parameter is missing */
3913            snac.config = xs_dict_set(snac.config, "cw", "");
3914        }
3915        if ((v = xs_dict_get(p_vars, "email")) != NULL)
3916            snac.config = xs_dict_set(snac.config, "email", v);
3917        if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
3918            snac.config = xs_dict_set(snac.config, "telegram_bot", v);
3919        if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
3920            snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
3921        if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
3922            snac.config = xs_dict_set(snac.config, "ntfy_server", v);
3923        if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
3924            snac.config = xs_dict_set(snac.config, "ntfy_token", v);
3925        if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
3926            xs *days    = xs_number_new(atof(v));
3927            snac.config = xs_dict_set(snac.config, "purge_days", days);
3928        }
3929        if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
3930            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
3931        else
3932            snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
3933        if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
3934            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
3935        else
3936            snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
3937        if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
3938            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
3939        else
3940            snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
3941        if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
3942            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
3943        else
3944            snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
3945        if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
3946            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
3947        else
3948            snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
3949        if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
3950            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
3951        else
3952            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
3953        if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
3954            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
3955        else
3956            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
3957
3958        if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
3959            snac.config = xs_dict_set(snac.config, "metadata", v);
3960
3961        /* uploads */
3962        const char *uploads[] = { "avatar", "header", NULL };
3963        int n;
3964        for (n = 0; uploads[n]; n++) {
3965            xs *var_name = xs_fmt("%s_file", uploads[n]);
3966
3967            const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
3968            if (xs_type(uploaded_file) == XSTYPE_LIST) {
3969                const char *fn = xs_list_get(uploaded_file, 0);
3970
3971                if (fn && *fn) {
3972                    const char *mimetype = xs_mime_by_ext(fn);
3973
3974                    if (xs_startswith(mimetype, "image/")) {
3975                        const char *ext = strrchr(fn, '.');
3976                        xs *hash        = xs_md5_hex(fn, strlen(fn));
3977                        xs *id          = xs_fmt("%s%s", hash, ext);
3978                        xs *url         = xs_fmt("%s/s/%s", snac.actor, id);
3979                        int fo          = xs_number_get(xs_list_get(uploaded_file, 1));
3980                        int fs          = xs_number_get(xs_list_get(uploaded_file, 2));
3981
3982                        /* store */
3983                        static_put(&snac, id, payload + fo, fs);
3984
3985                        snac.config = xs_dict_set(snac.config, uploads[n], url);
3986                    }
3987                }
3988            }
3989        }
3990
3991        /* delete images by removing url from user.json */
3992        for (n = 0; uploads[n]; n++) {
3993            xs *var_name = xs_fmt("%s_delete", uploads[n]);
3994            const char *delete_var = xs_dict_get(p_vars, var_name);
3995
3996            if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
3997                    snac.config = xs_dict_set(snac.config, uploads[n], "");
3998            }
3999        }
4000
4001        /* password change? */
4002        if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
4003            (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
4004            *p1 && strcmp(p1, p2) == 0) {
4005            xs *pw = hash_password(snac.uid, p1, NULL);
4006            snac.config = xs_dict_set(snac.config, "passwd", pw);
4007        }
4008
4009        user_persist(&snac, 1);
4010
4011        status = HTTP_STATUS_SEE_OTHER;
4012    }
4013    else
4014    if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
4015        notify_clear(&snac);
4016        timeline_touch(&snac);
4017
4018        status = HTTP_STATUS_SEE_OTHER;
4019    }
4020    else
4021    if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
4022        const char *irt   = xs_dict_get(p_vars, "irt");
4023        const char *opt   = xs_dict_get(p_vars, "question");
4024        const char *actor = xs_dict_get(p_vars, "actor");
4025
4026        xs *ls = NULL;
4027
4028        /* multiple choices? */
4029        if (xs_type(opt) == XSTYPE_LIST)
4030            ls = xs_dup(opt);
4031        else
4032        if (xs_type(opt) == XSTYPE_STRING) {
4033            ls = xs_list_new();
4034            ls = xs_list_append(ls, opt);
4035        }
4036
4037        const xs_str *v;
4038        int c = 0;
4039
4040        while (xs_list_next(ls, &v, &c)) {
4041            xs *msg = msg_note(&snac, "", actor, irt, NULL, 1);
4042
4043            /* set the option */
4044            msg = xs_dict_append(msg, "name", v);
4045
4046            xs *c_msg = msg_create(&snac, msg);
4047
4048            enqueue_message(&snac, c_msg);
4049
4050            timeline_add(&snac, xs_dict_get(msg, "id"), msg);
4051        }
4052
4053        if (ls != NULL) {
4054            /* get the poll object */
4055            xs *poll = NULL;
4056
4057            if (valid_status(object_get(irt, &poll))) {
4058                const char *date = xs_dict_get(poll, "endTime");
4059                if (xs_is_null(date))
4060                    date = xs_dict_get(poll, "closed");
4061
4062                if (!xs_is_null(date)) {
4063                    time_t t = xs_parse_iso_date(date, 0) - time(NULL);
4064
4065                    /* request the poll when it's closed;
4066                       Pleroma does not send and update when the poll closes */
4067                    enqueue_object_request(&snac, irt, t + 2);
4068                }
4069            }
4070        }
4071
4072        status = HTTP_STATUS_SEE_OTHER;
4073    }
4074
4075    if (status == HTTP_STATUS_SEE_OTHER) {
4076        const char *redir = xs_dict_get(p_vars, "redir");
4077
4078        if (xs_is_null(redir))
4079            redir = "top";
4080
4081        *body   = xs_fmt("%s/admin#%s", snac.actor, redir);
4082        *b_size = strlen(*body);
4083    }
4084
4085    user_free(&snac);
4086
4087    return status;
4088}
4089
4090
4091xs_str *timeline_to_rss(snac *user, const xs_list *timeline,
4092                        const char *title, const char *link, const char *desc)
4093/* converts a timeline to rss */
4094{
4095    xs_html *rss = xs_html_tag("rss",
4096        xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"),
4097        xs_html_attr("version",       "2.0"),
4098        xs_html_attr("xmlns:atom",    "http:/" "/www.w3.org/2005/Atom"));
4099
4100    xs_html *channel = xs_html_tag("channel",
4101        xs_html_tag("title",
4102            xs_html_text(title)),
4103        xs_html_tag("language",
4104            xs_html_text("en")),
4105        xs_html_tag("link",
4106            xs_html_text(link)),
4107        xs_html_sctag("atom:link",
4108            xs_html_attr("href", link),
4109            xs_html_attr("rel", "self"),
4110            xs_html_attr("type", "application/rss+xml")),
4111        xs_html_tag("generator",
4112            xs_html_text(USER_AGENT)),
4113        xs_html_tag("description",
4114            xs_html_text(desc)));
4115
4116    xs_html_add(rss, channel);
4117
4118    int cnt = 0;
4119    const char *v;
4120
4121    xs_list_foreach(timeline, v) {
4122        xs *msg = NULL;
4123
4124        if (user) {
4125            if (!valid_status(timeline_get_by_md5(user, v, &msg)))
4126                continue;
4127        }
4128        else {
4129            if (!valid_status(object_get_by_md5(v, &msg)))
4130                continue;
4131        }
4132
4133        const char *id = xs_dict_get(msg, "id");
4134        const char *content = xs_dict_get(msg, "content");
4135        const char *published = xs_dict_get(msg, "published");
4136
4137        if (user && !xs_startswith(id, user->actor))
4138            continue;
4139
4140        if (!id || !content || !published)
4141            continue;
4142
4143        /* create a title with the first line of the content */
4144        xs *title = xs_replace(content, "<br>", "\n");
4145        title = xs_regex_replace_i(title, "<[^>]+>", " ");
4146        title = xs_regex_replace_i(title, "&[^;]+;", " ");
4147        int i;
4148
4149        for (i = 0; title[i] && title[i] != '\n' && i < 50; i++);
4150
4151        if (title[i] != '\0') {
4152            title[i] = '\0';
4153            title = xs_str_cat(title, "...");
4154        }
4155
4156        title = xs_strip_i(title);
4157
4158        /* convert the date */
4159        time_t t = xs_parse_iso_date(published, 0);
4160        xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000");
4161
4162        /* if it's the first one, add it to the header */
4163        if (cnt == 0)
4164            xs_html_add(channel,
4165                xs_html_tag("lastBuildDate",
4166                    xs_html_text(rss_date)));
4167
4168        xs_html_add(channel,
4169            xs_html_tag("item",
4170                xs_html_tag("title",
4171                    xs_html_text(title)),
4172                xs_html_tag("link",
4173                    xs_html_text(id)),
4174                xs_html_tag("guid",
4175                    xs_html_text(id)),
4176                xs_html_tag("pubDate",
4177                    xs_html_text(rss_date)),
4178                xs_html_tag("description",
4179                    xs_html_text(content))));
4180
4181        cnt++;
4182    }
4183
4184    return xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
4185}