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