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