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