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