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