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