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