main
html.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2026 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_bearssl.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#include "xs_http.h"
19#include "xs_list_tools.h"
20
21#include "snac.h"
22
23int login(snac *user, const xs_dict *headers)
24/* tries a login */
25{
26 int logged_in = 0;
27 const char *auth = xs_dict_get(headers, "authorization");
28
29 if (auth && xs_startswith(auth, "Basic ")) {
30 int sz;
31 xs *s1 = xs_crop_i(xs_dup(auth), 6, 0);
32 xs *s2 = xs_base64_dec(s1, &sz);
33
34 xs *l1 = xs_split_n(s2, ":", 1);
35
36 if (xs_list_len(l1) == 2) {
37 const char *uid = xs_list_get(l1, 0);
38 const char *pwd = xs_list_get(l1, 1);
39 const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
40 xs_dict_get(headers, "x-forwarded-for"));
41
42 if (badlogin_check(uid, addr)) {
43 logged_in = check_password(uid, pwd,
44 xs_dict_get(user->config, "passwd"));
45
46 if (!logged_in)
47 badlogin_inc(uid, addr);
48 }
49 }
50 }
51
52 if (logged_in)
53 lastlog_write(user, "web");
54
55 return logged_in;
56}
57
58xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems,
59 const char *proxy, const xs_list *cl, const char *act_o)
60/* replace but also adds a class list and an actor in its alt text.
61 * Used for emoji reactions */
62{
63 if (!xs_is_null(tag)) {
64 xs *tag_list = NULL;
65 if (xs_type(tag) == XSTYPE_DICT) {
66 /* not a list */
67 tag_list = xs_list_new();
68 tag_list = xs_list_append(tag_list, tag);
69 } else {
70 /* is a list */
71 tag_list = xs_dup(tag);
72 }
73
74 xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems);
75 xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
76 if (cl) {
77 xs *l = xs_join(cl, " ");
78 class = xs_str_cat(class, " ", l);
79 }
80
81 int c = 0;
82 const xs_val *v;
83
84 c = 0;
85
86 xs_set rep_emoji;
87 xs_set_init(&rep_emoji);
88
89 while (xs_list_next(tag_list, &v, &c)) {
90 const char *t = xs_dict_get(v, "type");
91
92 if (t && strcmp(t, "Emoji") == 0) {
93 const char *n = xs_dict_get(v, "name");
94 const xs_dict *i = xs_dict_get(v, "icon");
95
96 /* avoid repeated emojis (Misskey seems to return this) */
97 if (xs_set_add(&rep_emoji, n) == 0)
98 continue;
99
100 if (xs_is_string(n) && xs_is_dict(i)) {
101 const char *u = xs_dict_get(i, "url");
102 const char *mt = xs_dict_get(i, "mediaType");
103
104 if (xs_is_string(u)) {
105 // on akkoma instances mediaType is not present.
106 // but we need to to know if the image is an svg or not.
107 // for now, i just use the file extention, which may not be the most reliable...
108 if (!xs_is_string(mt))
109 mt = xs_mime_by_ext(u);
110
111 xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n);
112
113 if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
114 s = xs_replace_i(s, n, "");
115 else {
116 xs *url = make_url(u, proxy, 0);
117
118 xs_html *img = xs_html_sctag("img",
119 xs_html_attr("loading", "lazy"),
120 xs_html_attr("src", url),
121 xs_html_attr("alt", act),
122 xs_html_attr("title", act),
123 xs_html_attr("class", class),
124 xs_html_attr("style", style));
125
126 xs *s1 = xs_html_render(img);
127 s = xs_replace_i(s, n, s1);
128 }
129 }
130 else
131 s = xs_replace_i(s, n, "");
132 }
133 }
134 }
135
136 xs_set_free(&rep_emoji);
137 }
138
139 return s;
140}
141
142
143xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
144/* replaces all the :shortnames: with the emojis in tag */
145{
146 return _replace_shortnames(s, tag, ems, proxy, NULL, NULL);
147}
148
149
150xs_str *actor_name(xs_dict *actor, const char *proxy)
151/* gets the actor name */
152{
153 const char *v;
154
155 if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') {
156 if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') {
157 v = "anonymous";
158 }
159 }
160
161 return replace_shortnames(xs_html_encode(v), xs_dict_get(actor, "tag"), 1, proxy);
162}
163
164
165xs_str *actor_pronouns(xs_dict *actor)
166/* gets the actor name */
167{
168 const xs_list *attachment;
169 const xs_dict *d;
170 const char *v;
171 const char *pronouns = "";
172 xs_str *ret;
173
174 if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) {
175 xs_list_foreach(attachment, d) {
176 xs *prop = xs_utf8_tolower(xs_dict_get(d, "name"));
177 /* make sure that we are reading the correct metadata */
178 if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) {
179 /* safeguard from NULL values */
180 v = xs_dict_get(d, "value");
181 pronouns = v ? v : pronouns;
182 break;
183 }
184 }
185 }
186
187 /* strip all HTML tags */
188 ret = xs_regex_replace(pronouns, "</?[^>]+>", "");
189 return ret;
190}
191
192
193xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
194/* needed when we have local text with no tags attached */
195{
196 xs *tags = xs_list_new();
197 xs *name1 = not_really_markdown(text, NULL, &tags);
198
199 xs_str *name3;
200 if (user) {
201 xs *name2 = process_tags(user, name1, &tags);
202 name3 = sanitize(name2);
203 }
204 else {
205 name3 = sanitize(name1);
206 name3 = xs_replace_i(name3, "<br>", "");
207 }
208
209 return replace_shortnames(name3, tags, ems, proxy);
210}
211
212
213xs_str *html_date_label(snac *user, const char *date)
214{
215 time_t t;
216
217 /* check if a user has actually set a timezone */
218 if (user != NULL && xs_dict_get(user->config, "tz") != NULL &&
219 xs_endswith(date, "Z") && (t = xs_parse_iso_date(date, 0)) != 0) {
220 t += xs_tz_offset(user->tz);
221
222 time_t today = time(NULL);
223 if (today - t >= 3600 * 24 * 30 * 6) {
224 /* more than half a year ago */
225 return xs_str_utctime(t, "%Y-%m-%d %H:%M");
226 }
227
228 xs_str *date = xs_str_utctime(t, "%b %d %H:%M");
229
230 struct tm tm;
231 localtime_r(&t, &tm);
232
233 if (tm.tm_mon >= 0 && tm.tm_mon <= 11) {
234 /* copy the 3letter translated month name */
235 const char *m = L(months[tm.tm_mon]);
236
237 if (xs_is_string(m))
238 date = xs_replace_i(date, months[tm.tm_mon], m);
239 }
240
241 return date;
242 }
243
244 return xs_crop_i(xs_dup(date), 0, 10);
245}
246
247
248xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
249 const char *udate, const char *url, int scope,
250 int in_people, const char *proxy, const char *lang,
251 const char *md5)
252{
253 xs_html *actor_icon = xs_html_tag("p", NULL);
254
255 xs *avatar = NULL;
256 const char *v;
257 int fwing = 0;
258 int fwer = 0;
259
260 xs *name = actor_name(actor, proxy);
261 xs *pronouns = actor_pronouns(actor);
262
263 /* get the avatar */
264 if ((v = xs_dict_get(actor, "icon")) != NULL) {
265 /* if it's a list (Peertube), get the first one */
266 if (xs_type(v) == XSTYPE_LIST)
267 v = xs_list_get(v, 0);
268
269 if ((v = xs_dict_get(v, "url")) != NULL)
270 avatar = make_url(v, proxy, 0);
271 }
272
273 if (avatar == NULL)
274 avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
275
276 const char *actor_id = xs_dict_get(actor, "id");
277 const char *html_url = xs_dict_get_def(actor, "url", actor_id);
278
279 if (xs_is_list(html_url))
280 html_url = xs_list_get(html_url, 0);
281
282 if (!xs_is_string(html_url))
283 html_url = actor_id;
284
285 xs *href = NULL;
286
287 if (user) {
288 fwer = follower_check(user, actor_id);
289 fwing = following_check(user, actor_id);
290 }
291
292 if (user && !in_people) {
293 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
294 href = xs_fmt("%s/people/%s", user->actor, md5);
295 }
296
297 if (href == NULL)
298 href = xs_dup(html_url);
299
300 xs_html *name_link = xs_html_tag("a",
301 xs_html_attr("href", href),
302 xs_html_attr("class", "p-author h-card snac-author"),
303 xs_html_raw(name)); /* name is already html-escaped */
304
305 if (*pronouns) {
306 xs_html_add(name_link,
307 xs_html_text(" ["),
308 xs_html_tag("span",
309 xs_html_attr("class", "snac-pronouns"),
310 xs_html_attr("title", "user's pronouns"),
311 xs_html_raw(pronouns)),
312 xs_html_text("]"));
313 }
314
315 xs_html_add(actor_icon,
316 xs_html_sctag("img",
317 xs_html_attr("loading", "lazy"),
318 xs_html_attr("class", "snac-avatar"),
319 xs_html_attr("src", avatar),
320 xs_html_attr("alt", "[?]")),
321 name_link);
322
323 if (!xs_is_null(url)) {
324 xs_html_add(actor_icon,
325 xs_html_text(" "),
326 xs_html_tag("a",
327 xs_html_attr("href", (char *)url),
328 xs_html_attr("title", xs_is_string(md5) ? md5 : ""),
329 xs_html_text("»")));
330 }
331
332
333 if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
334 xs_html_add(actor_icon,
335 xs_html_text(" "),
336 xs_html_tag("span",
337 xs_html_attr("title", "bot"),
338 xs_html_raw("🤖")));
339 }
340
341 if (fwing && fwer) {
342 xs_html_add(actor_icon,
343 xs_html_text(" "),
344 xs_html_tag("span",
345 xs_html_attr("title", "mutual relation"),
346 xs_html_raw("🤝")));
347 }
348 else
349 if (fwer) {
350 xs_html_add(actor_icon,
351 xs_html_text(" "),
352 xs_html_tag("span",
353 xs_html_attr("title", "follows you"),
354 xs_html_raw("👍")));
355 }
356
357 if (instance_failure(actor_id, 0) == -1) {
358 xs_html_add(actor_icon,
359 xs_html_text(" "),
360 xs_html_tag("span",
361 xs_html_attr("title", "broken instance"),
362 xs_html_raw("💔")));
363 }
364
365 if (actor_failure(actor_id, OP_CHECK) == -1) {
366 xs_html_add(actor_icon,
367 xs_html_text(" "),
368 xs_html_tag("span",
369 xs_html_attr("title", "account no longer exists"),
370 xs_html_raw("💀")));
371 }
372
373 if (scope != -1) {
374 if (scope == SCOPE_FOLLOWERS) {
375 xs_html_add(actor_icon,
376 xs_html_text(" "),
377 xs_html_tag("span",
378 xs_html_attr("title", "followers"),
379 xs_html_raw("🔒"))); // emoji of a lock
380 }
381 else if (scope == SCOPE_PUBLIC) {
382 xs_html_add(actor_icon,
383 xs_html_text(" "),
384 xs_html_tag("span",
385 xs_html_attr("title", "public"),
386 xs_html_raw("🌐"))); // emoji of a globe
387 }
388 else if (scope == SCOPE_UNLISTED) {
389 xs_html_add(actor_icon,
390 xs_html_text(" "),
391 xs_html_tag("span",
392 xs_html_attr("title", "unlisted"),
393 xs_html_raw("🔓"))); // emoji of an unlocked lock
394 }
395 else if (scope == SCOPE_MENTIONED) {
396 xs_html_add(actor_icon,
397 xs_html_text(" "),
398 xs_html_tag("span",
399 xs_html_attr("title", "mentioned"),
400 xs_html_raw("✉️"))); // emoji of a mail
401 }
402 }
403
404 if (xs_is_null(date)) {
405 xs_html_add(actor_icon,
406 xs_html_raw(" "));
407 }
408 else {
409 xs *date_label = html_date_label(user, date);
410 xs *date_title = xs_dup(date);
411
412 if (!xs_is_null(udate)) {
413 xs *sd = html_date_label(user, udate);
414
415 date_label = xs_str_cat(date_label, " / ", sd);
416
417 date_title = xs_str_cat(date_title, " / ", udate);
418 }
419
420 if (xs_is_string(lang))
421 date_title = xs_str_cat(date_title, " (", lang, ")");
422
423 xs_html *date_text = xs_html_text(date_label);
424
425 if (user && md5) {
426 xs *lpost_url = xs_fmt("%s/admin/p/%s#%s_entry",
427 user->actor, md5, md5);
428 date_text = xs_html_tag("a",
429 xs_html_attr("href", lpost_url),
430 xs_html_attr("class", "snac-pubdate"),
431 date_text);
432 }
433 else if (user && url) {
434 xs *lpost_url = xs_fmt("%s/admin?q=%s",
435 user->actor, xs_url_enc(url));
436 date_text = xs_html_tag("a",
437 xs_html_attr("href", lpost_url),
438 xs_html_attr("class", "snac-pubdate"),
439 date_text);
440 }
441
442 xs_html_add(actor_icon,
443 xs_html_text(" "),
444 xs_html_tag("time",
445 xs_html_attr("class", "dt-published snac-pubdate"),
446 xs_html_attr("title", date_title),
447 date_text));
448 }
449
450 {
451 const char *username, *id;
452
453 if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') {
454 /* This should never be reached */
455 username = "anonymous";
456 }
457
458 if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') {
459 /* This should never be reached */
460 id = "https://social.example.org/anonymous";
461 }
462
463 /* "LIKE AN ANIMAL" */
464 xs *domain = xs_split(id, "/");
465 xs *user = xs_fmt("@%s@%s", username, xs_list_get(domain, 2));
466
467 xs_html_add(actor_icon,
468 xs_html_sctag("br", NULL),
469 xs_html_tag("a",
470 xs_html_attr("href", html_url),
471 xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
472 xs_html_text(user)));
473 }
474
475 return actor_icon;
476}
477
478
479xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
480 const char *proxy, const char *md5, const char *lang)
481{
482 xs *actor = NULL;
483 xs_html *actor_icon = NULL;
484
485 if (actor_id && valid_status(actor_get_refresh(user, actor_id, &actor))) {
486 const char *date = NULL;
487 const char *udate = NULL;
488 const char *url = NULL;
489 const char *type = xs_dict_get(msg, "type");
490 const int scope = get_msg_visibility(msg);
491
492 if (xs_match(type, POSTLIKE_OBJECT_TYPE)) {
493 url = xs_dict_get(msg, "url");
494 if (xs_is_list(url))
495 url = xs_list_get(url, 0);
496
497 if (!xs_is_string(url))
498 url = xs_dict_get(msg, "id");
499 }
500
501 date = xs_dict_get(msg, "published");
502 udate = xs_dict_get(msg, "updated");
503
504 actor_icon = html_actor_icon(user, actor, date, udate, url, scope, 0, proxy, lang, md5);
505 }
506
507 return actor_icon;
508}
509
510void html_note_render_visibility(snac* user, xs_html *form, const int scope)
511{
512 // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here
513 static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
514 static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
515 static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
516 static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1};
517 static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes};
518 static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"};
519 static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"};
520
521 xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
522 const int* to_render = scopes[scope];
523 for( int i = 0; to_render[i] != -1; i++ ) {
524 const int scope_i = to_render[i];
525 const char* value = scopes_tags[scope_i];
526 const char* name = scopes_names[scope_i];
527 xs_html_add(paragraph,
528 xs_html_tag("label",
529 xs_html_sctag("input",
530 xs_html_attr("type", "radio"),
531 xs_html_attr("name", "visibility"),
532 xs_html_attr("value", value),
533 xs_html_attr(scope == scope_i ? "checked" : "", NULL)),
534 xs_html_text(" "),
535 xs_html_text(L(name)),
536 xs_html_text(" "))
537 );
538 }
539 xs_html_add(form, paragraph);
540}
541
542/* html_note but moddled for emoji's needs. here and not bellow, since the
543 * other one is already so complex. */
544xs_html *html_emoji(snac *user, const char *summary,
545 const char *div_id, const char *form_id,
546 const char* placeholder, const char *post_id,
547 const char* eid)
548{
549 xs *action = xs_fmt("%s/admin/action", user->actor);
550
551 xs_html *form;
552 const int react = eid == NULL ? 0 : 1;
553
554 xs_html *note = xs_html_tag("div",
555 xs_html_tag("details",
556 xs_html_tag("summary",
557 xs_html_text(summary)),
558 xs_html_tag("p", NULL),
559 xs_html_tag("div",
560 xs_html_attr("class", "snac-note"),
561 xs_html_attr("id", div_id),
562 form = xs_html_tag("form",
563 xs_html_attr("autocomplete", "off"),
564 xs_html_attr("method", "post"),
565 xs_html_attr("action", action),
566 xs_html_attr("enctype", "multipart/form-data"),
567 xs_html_attr("id", form_id),
568 xs_html_sctag("input",
569 xs_html_attr("type", "hidden"),
570 xs_html_attr("name", "id"),
571 xs_html_attr("value", post_id)),
572 xs_html_sctag("input",
573 xs_html_attr("type", react ? "hidden" : "text"),
574 xs_html_attr("name", "eid"),
575 xs_html_attr("required", "required"),
576 xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)),
577 xs_html_text(" "),
578 xs_html_sctag("input",
579 xs_html_attr("type", "submit"),
580 xs_html_attr("name", "action"),
581 xs_html_attr("eid", "action"),
582 xs_html_attr("value", react ? L("EmojiUnreact") : L("EmojiReact"))),
583 xs_html_text(" "),
584 xs_html_tag("p", NULL)))));
585
586 return note;
587}
588
589xs_html *html_note(snac *user, const char *summary,
590 const char *div_id, const char *form_id,
591 const char *ta_plh, const char *ta_content,
592 const char *edit_id, const char *actor_id,
593 const xs_val *cw_yn, const char *cw_text,
594 const int scope, const char *redir,
595 const char *in_reply_to, int poll,
596 const xs_list *att_files, const xs_list *att_alt_texts,
597 int is_draft, const char *published,
598 const char *note_lang)
599/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
600{
601 xs *action = xs_fmt("%s/admin/note", user->actor);
602
603 xs_html *form;
604
605 xs_html *note = xs_html_tag("div",
606 xs_html_tag("details",
607 xs_html_tag("summary",
608 xs_html_text(summary)),
609 xs_html_tag("p", NULL),
610 xs_html_tag("div",
611 xs_html_attr("class", "snac-note"),
612 xs_html_attr("id", div_id),
613 form = xs_html_tag("form",
614 xs_html_attr("autocomplete", "off"),
615 xs_html_attr("method", "post"),
616 xs_html_attr("action", action),
617 xs_html_attr("enctype", "multipart/form-data"),
618 xs_html_attr("id", form_id),
619 xs_html_tag("textarea",
620 xs_html_attr("class", "snac-textarea"),
621 xs_html_attr("name", "content"),
622 xs_html_attr("rows", "4"),
623 xs_html_attr("wrap", "virtual"),
624 xs_html_attr("required", "required"),
625 xs_html_attr("placeholder", ta_plh),
626 xs_html_text(ta_content)),
627 xs_html_tag("p", NULL),
628 xs_html_text(L("Sensitive content: ")),
629 xs_html_sctag("input",
630 xs_html_attr("type", "checkbox"),
631 xs_html_attr("name", "sensitive"),
632 xs_html_attr(xs_type(cw_yn) == XSTYPE_TRUE ? "checked" : "", NULL)),
633 xs_html_sctag("input",
634 xs_html_attr("type", "text"),
635 xs_html_attr("name", "summary"),
636 xs_html_attr("placeholder", L("Sensitive content description")),
637 xs_html_attr("value", xs_is_null(cw_text) ? "" : cw_text))))));
638
639 if (actor_id)
640 xs_html_add(form,
641 xs_html_sctag("input",
642 xs_html_attr("type", "hidden"),
643 xs_html_attr("name", "to"),
644 xs_html_attr("value", actor_id)));
645 if (edit_id == NULL)
646 html_note_render_visibility(user, form, scope);
647
648 if (redir)
649 xs_html_add(form,
650 xs_html_sctag("input",
651 xs_html_attr("type", "hidden"),
652 xs_html_attr("name", "redir"),
653 xs_html_attr("value", redir)));
654
655 if (in_reply_to)
656 xs_html_add(form,
657 xs_html_sctag("input",
658 xs_html_attr("type", "hidden"),
659 xs_html_attr("name", "in_reply_to"),
660 xs_html_attr("value", in_reply_to)));
661 else
662 xs_html_add(form,
663 xs_html_tag("p", NULL),
664 xs_html_text(L("Reply to (URL): ")),
665 xs_html_sctag("input",
666 xs_html_attr("type", "url"),
667 xs_html_attr("name", "in_reply_to"),
668 xs_html_attr("placeholder", L("Optional URL to reply to"))));
669
670 xs_html_add(form,
671 xs_html_tag("p", NULL),
672 xs_html_tag("span",
673 xs_html_attr("title", L("Don't send, but store as a draft")),
674 xs_html_text(L("Draft:")),
675 xs_html_sctag("input",
676 xs_html_attr("type", "checkbox"),
677 xs_html_attr("name", "is_draft"),
678 xs_html_attr(is_draft ? "checked" : "", NULL))));
679
680 const char *post_langs = xs_dict_get(user->config, "post_langs");
681
682 if (xs_is_string(post_langs) && *post_langs) {
683 xs *ll = xs_split(post_langs, " ");
684 const char *lang;
685
686 xs_html *post_lang = xs_html_tag("select",
687 xs_html_attr("name", "post_lang"));
688
689 xs_list_foreach(ll, lang) {
690 if (*lang) {
691 if (xs_is_string(note_lang) && strcmp(lang, note_lang) == 0) {
692 xs_html_add(post_lang,
693 xs_html_tag("option",
694 xs_html_text(lang),
695 xs_html_attr("selected", NULL),
696 xs_html_attr("value", lang)));
697 }
698 else {
699 xs_html_add(post_lang,
700 xs_html_tag("option",
701 xs_html_text(lang),
702 xs_html_attr("value", lang)));
703 }
704 }
705 }
706
707 xs_html_add(form,
708 xs_html_tag("p", NULL),
709 xs_html_text(L("Language:")),
710 xs_html_text(" "),
711 post_lang);
712 }
713
714 /* post date and time */
715 xs *post_date = NULL;
716 xs *post_time = NULL;
717
718 if (xs_is_string(published)) {
719 time_t t = xs_parse_iso_date(published, 0);
720
721 if (t > 0) {
722 post_date = xs_str_time(t, "%Y-%m-%d", 1);
723 post_time = xs_str_time(t, "%H:%M:%S", 1);
724 }
725 }
726
727 if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
728 xs *pdat = xs_fmt(L("Post date and time (timezone: %s):"), user->tz);
729
730 xs_html_add(form,
731 xs_html_tag("p",
732 xs_html_tag("details",
733 xs_html_tag("summary",
734 xs_html_text(L("Scheduled post..."))),
735 xs_html_tag("p",
736 xs_html_text(pdat),
737 xs_html_sctag("br", NULL),
738 xs_html_sctag("input",
739 xs_html_attr("type", "date"),
740 xs_html_attr("value", post_date ? post_date : ""),
741 xs_html_attr("name", "post_date")),
742 xs_html_text(" "),
743 xs_html_sctag("input",
744 xs_html_attr("type", "time"),
745 xs_html_attr("value", post_time ? post_time : ""),
746 xs_html_attr("step", "1"),
747 xs_html_attr("name", "post_time"))))));
748 }
749
750 if (edit_id)
751 xs_html_add(form,
752 xs_html_sctag("input",
753 xs_html_attr("type", "hidden"),
754 xs_html_attr("name", "edit_id"),
755 xs_html_attr("value", edit_id)));
756
757 /* attachment controls */
758 xs_html *att;
759
760 xs_html_add(form,
761 xs_html_tag("p", NULL),
762 att = xs_html_tag("details",
763 xs_html_tag("summary",
764 xs_html_text(L("Attachments..."))),
765 xs_html_tag("p", NULL)));
766
767 int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
768 int att_n = 0;
769
770 /* fields for the currently existing attachments */
771 if (xs_is_list(att_files) && xs_is_list(att_alt_texts)) {
772 while (att_n < max_attachments) {
773 const char *att_file = xs_list_get(att_files, att_n);
774 const char *att_alt_text = xs_list_get(att_alt_texts, att_n);
775
776 if (!xs_is_string(att_file) || !xs_is_string(att_alt_text))
777 break;
778
779 xs *att_lbl = xs_fmt("attach_url_%d", att_n);
780 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
781
782 if (att_n)
783 xs_html_add(att,
784 xs_html_sctag("br", NULL));
785
786 xs_html_add(att,
787 xs_html_text(L("File:")),
788 xs_html_sctag("input",
789 xs_html_attr("type", "text"),
790 xs_html_attr("name", att_lbl),
791 xs_html_attr("title", L("Clear this field to delete the attachment")),
792 xs_html_attr("value", att_file)));
793
794 xs_html_add(att,
795 xs_html_text(" "),
796 xs_html_sctag("input",
797 xs_html_attr("type", "text"),
798 xs_html_attr("name", alt_lbl),
799 xs_html_attr("value", att_alt_text),
800 xs_html_attr("placeholder", L("Attachment description"))));
801
802 att_n++;
803 }
804 }
805
806 /* the rest of possible attachments */
807 while (att_n < max_attachments) {
808 xs *att_lbl = xs_fmt("attach_%d", att_n);
809 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
810
811 if (att_n)
812 xs_html_add(att,
813 xs_html_sctag("br", NULL));
814
815 xs_html_add(att,
816 xs_html_sctag("input",
817 xs_html_attr("type", "file"),
818 xs_html_attr("name", att_lbl)));
819
820 xs_html_add(att,
821 xs_html_text(" "),
822 xs_html_sctag("input",
823 xs_html_attr("type", "text"),
824 xs_html_attr("name", alt_lbl),
825 xs_html_attr("placeholder", L("Attachment description"))));
826
827 att_n++;
828 }
829
830 /* add poll controls */
831 if (poll) {
832 const xs_number *max_options = xs_dict_get(srv_config, "max_poll_options");
833 xs *poll_limit_str = xs_dup(L("Poll options (one per line, up to 8):"));
834 if (max_options != NULL)
835 poll_limit_str = xs_replace_i(poll_limit_str, "8", xs_number_str(max_options));
836
837 xs_html_add(form,
838 xs_html_tag("p", NULL),
839 xs_html_tag("details",
840 xs_html_tag("summary",
841 xs_html_text(L("Poll..."))),
842 xs_html_tag("p",
843 xs_html_text(poll_limit_str),
844 xs_html_sctag("br", NULL),
845 xs_html_tag("textarea",
846 xs_html_attr("class", "snac-textarea"),
847 xs_html_attr("name", "poll_options"),
848 xs_html_attr("rows", "4"),
849 xs_html_attr("wrap", "virtual"),
850 xs_html_attr("placeholder", L("Option 1...\nOption 2...\nOption 3...\n...")))),
851 xs_html_tag("select",
852 xs_html_attr("name", "poll_multiple"),
853 xs_html_tag("option",
854 xs_html_attr("value", "off"),
855 xs_html_text(L("One choice"))),
856 xs_html_tag("option",
857 xs_html_attr("value", "on"),
858 xs_html_text(L("Multiple choices")))),
859 xs_html_text(" "),
860 xs_html_tag("select",
861 xs_html_attr("name", "poll_end_secs"),
862 xs_html_tag("option",
863 xs_html_attr("value", "300"),
864 xs_html_text(L("End in 5 minutes"))),
865 xs_html_tag("option",
866 xs_html_attr("value", "3600"),
867 xs_html_attr("selected", NULL),
868 xs_html_text(L("End in 1 hour"))),
869 xs_html_tag("option",
870 xs_html_attr("value", "86400"),
871 xs_html_text(L("End in 1 day"))),
872 xs_html_tag("option",
873 xs_html_attr("value", "259200"),
874 xs_html_text(L("End in 3 days"))),
875 xs_html_tag("option",
876 xs_html_attr("value", "31536000"),
877 xs_html_text(L("End in 1 year"))))));
878 }
879
880 xs_html_add(form,
881 xs_html_tag("p", NULL),
882 xs_html_sctag("input",
883 xs_html_attr("type", "submit"),
884 xs_html_attr("class", "button"),
885 xs_html_attr("value", L("Post"))),
886 xs_html_tag("p", NULL));
887
888 return note;
889}
890
891
892static xs_html *html_base_head(void)
893{
894 xs_html *head = xs_html_tag("head",
895 xs_html_sctag("meta",
896 xs_html_attr("name", "viewport"),
897 xs_html_attr("content", "width=device-width, initial-scale=1")),
898 xs_html_sctag("meta",
899 xs_html_attr("name", "generator"),
900 xs_html_attr("content", USER_AGENT)));
901
902 /* add server CSS and favicon */
903 xs *f = NULL;
904 const char *favicon = xs_dict_get(srv_config, "favicon_url");
905
906 if (xs_is_string(favicon))
907 f = xs_dup(favicon);
908 else
909 f = xs_fmt("%s/favicon.ico", srv_baseurl);
910
911 const xs_list *p = xs_dict_get(srv_config, "cssurls");
912 const char *v;
913
914 xs_list_foreach(p, v) {
915 xs_html_add(head,
916 xs_html_sctag("link",
917 xs_html_attr("rel", "stylesheet"),
918 xs_html_attr("type", "text/css"),
919 xs_html_attr("href", v)));
920 }
921
922 xs_html_add(head,
923 xs_html_sctag("link",
924 xs_html_attr("rel", "icon"),
925 xs_html_attr("type", "image/x-icon"),
926 xs_html_attr("href", f)));
927
928 return head;
929}
930
931
932xs_html *html_instance_head(void)
933{
934 xs_html *head = html_base_head();
935
936 {
937 FILE *f;
938 xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
939
940 if ((f = fopen(g_css_fn, "r")) != NULL) {
941 xs *css = xs_readall(f);
942 fclose(f);
943
944 xs_html_add(head,
945 xs_html_tag("style",
946 xs_html_raw(css)));
947 }
948 }
949
950 const char *host = xs_dict_get(srv_config, "host");
951 const char *title = xs_dict_get(srv_config, "title");
952
953 xs_html_add(head,
954 xs_html_tag("title",
955 xs_html_text(title && *title ? title : host)));
956
957 return head;
958}
959
960
961static xs_html *html_instance_body(void)
962{
963 const char *host = xs_dict_get(srv_config, "host");
964 const char *sdesc = xs_dict_get(srv_config, "short_description");
965 const char *sdescraw = xs_dict_get(srv_config, "short_description_raw");
966 const char *email = xs_dict_get(srv_config, "admin_email");
967 const char *acct = xs_dict_get(srv_config, "admin_account");
968
969 /* for L() */
970 const snac *user = NULL;
971
972 xs *blurb = xs_replace(snac_blurb, "%host%", host);
973
974 xs_html *dl;
975
976 xs_html *body = xs_html_tag("body",
977 xs_html_tag("div",
978 xs_html_attr("class", "snac-instance-blurb"),
979 xs_html_raw(blurb), /* pure html */
980 dl = xs_html_tag("dl", NULL)));
981
982 if (sdesc && *sdesc) {
983 if (!xs_is_null(sdescraw) && xs_type(sdescraw) == XSTYPE_TRUE) {
984 xs_html_add(dl,
985 xs_html_tag("di",
986 xs_html_tag("dt",
987 xs_html_text(L("Site description"))),
988 xs_html_tag("dd",
989 xs_html_raw(sdesc))));
990 } else {
991 xs_html_add(dl,
992 xs_html_tag("di",
993 xs_html_tag("dt",
994 xs_html_text(L("Site description"))),
995 xs_html_tag("dd",
996 xs_html_text(sdesc))));
997 }
998 }
999 if (email && *email) {
1000 xs *mailto = xs_fmt("mailto:%s", email);
1001
1002 xs_html_add(dl,
1003 xs_html_tag("di",
1004 xs_html_tag("dt",
1005 xs_html_text(L("Admin email"))),
1006 xs_html_tag("dd",
1007 xs_html_tag("a",
1008 xs_html_attr("href", mailto),
1009 xs_html_text(email)))));
1010 }
1011 if (acct && *acct) {
1012 xs *url = xs_fmt("%s/%s", srv_baseurl, acct);
1013 xs *handle = xs_fmt("@%s@%s", acct, host);
1014
1015 xs_html_add(dl,
1016 xs_html_tag("di",
1017 xs_html_tag("dt",
1018 xs_html_text(L("Admin account"))),
1019 xs_html_tag("dd",
1020 xs_html_tag("a",
1021 xs_html_attr("href", url),
1022 xs_html_text(handle)))));
1023 }
1024
1025 return body;
1026}
1027
1028
1029xs_html *html_user_head(snac *user, const char *desc, const char *url)
1030{
1031 xs_html *head = html_base_head();
1032
1033 /* add the user CSS */
1034 {
1035 xs *css = NULL;
1036 int size;
1037
1038 /* try to open the user css */
1039 if (!valid_status(static_get(user, "style.css", &css, &size, NULL, NULL))) {
1040 /* it's not there; try to open the server-wide css */
1041 FILE *f;
1042 xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir);
1043
1044 if ((f = fopen(g_css_fn, "r")) != NULL) {
1045 css = xs_readall(f);
1046 fclose(f);
1047 }
1048 }
1049
1050 if (css != NULL) {
1051 xs_html_add(head,
1052 xs_html_tag("style",
1053 xs_html_raw(css)));
1054 }
1055 }
1056
1057 /* title */
1058 xs *title = xs_fmt("%s (@%s@%s)", xs_dict_get(user->config, "name"),
1059 user->uid, xs_dict_get(srv_config, "host"));
1060
1061 xs_html_add(head,
1062 xs_html_tag("title",
1063 xs_html_text(title)));
1064
1065 xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
1066
1067 if (avatar == NULL || *avatar == '\0') {
1068 xs_free(avatar);
1069 avatar = xs_fmt("%s/susie.png", srv_baseurl);
1070 }
1071
1072 /* create a description field */
1073 xs *s_desc = NULL;
1074 int n;
1075
1076 if (desc == NULL)
1077 s_desc = xs_dup(xs_dict_get(user->config, "bio"));
1078 else
1079 s_desc = xs_dup(desc);
1080
1081 /* show metrics in og:description? */
1082 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
1083 xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
1084
1085 s1 = xs_str_cat(s1, " · ");
1086
1087 s_desc = xs_str_prepend_i(s_desc, s1);
1088 }
1089
1090 /* shorten desc to a reasonable size */
1091 for (n = 0; s_desc[n]; n++) {
1092 if (n > 512 && (s_desc[n] == ' ' || s_desc[n] == '\n'))
1093 break;
1094 }
1095
1096 s_desc[n] = '\0';
1097
1098 /* og properties */
1099 xs_html_add(head,
1100 xs_html_sctag("meta",
1101 xs_html_attr("property", "og:site_name"),
1102 xs_html_attr("content", xs_dict_get(srv_config, "host"))),
1103 xs_html_sctag("meta",
1104 xs_html_attr("property", "og:title"),
1105 xs_html_attr("content", title)),
1106 xs_html_sctag("meta",
1107 xs_html_attr("property", "og:description"),
1108 xs_html_attr("content", s_desc)),
1109 xs_html_sctag("meta",
1110 xs_html_attr("property", "og:image"),
1111 xs_html_attr("content", avatar)),
1112 xs_html_sctag("meta",
1113 xs_html_attr("property", "og:width"),
1114 xs_html_attr("content", "300")),
1115 xs_html_sctag("meta",
1116 xs_html_attr("property", "og:height"),
1117 xs_html_attr("content", "300")));
1118
1119 /* RSS link */
1120 xs *rss_url = xs_fmt("%s.rss", user->actor);
1121 xs_html_add(head,
1122 xs_html_sctag("link",
1123 xs_html_attr("rel", "alternate"),
1124 xs_html_attr("type", "application/rss+xml"),
1125 xs_html_attr("title", "RSS"),
1126 xs_html_attr("href", rss_url)));
1127
1128 /* ActivityPub alternate link (actor id) */
1129 xs_html_add(head,
1130 xs_html_sctag("link",
1131 xs_html_attr("rel", "alternate"),
1132 xs_html_attr("type", "application/activity+json"),
1133 xs_html_attr("href", url ? url : user->actor)));
1134
1135 /* webmention hook */
1136 xs *wbh = xs_fmt("%s/webmention-hook", srv_baseurl);
1137
1138 xs_html_add(head,
1139 xs_html_sctag("link",
1140 xs_html_attr("rel", "webmention"),
1141 xs_html_attr("href", wbh)));
1142
1143 return head;
1144}
1145
1146
1147static xs_html *html_user_body(snac *user, int read_only)
1148{
1149 const char *proxy = NULL;
1150
1151 if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
1152 proxy = user->actor;
1153
1154 xs_html *body = xs_html_tag("body",
1155 xs_html_tag("a",
1156 xs_html_attr("name", "top")));
1157
1158 /* top nav */
1159 xs_html *top_nav = xs_html_tag("nav",
1160 xs_html_attr("class", "snac-top-nav"));
1161
1162 xs *avatar = xs_dup(xs_dict_get(user->config, "avatar"));
1163
1164 if (avatar == NULL || *avatar == '\0') {
1165 xs_free(avatar);
1166 avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
1167 }
1168
1169 xs_html_add(top_nav,
1170 xs_html_sctag("img",
1171 xs_html_attr("src", avatar),
1172 xs_html_attr("class", "snac-avatar"),
1173 xs_html_attr("alt", "")));
1174
1175 if (read_only) {
1176 xs *rss_url = xs_fmt("%s.rss", user->actor);
1177 xs *admin_url = xs_fmt("%s/admin", user->actor);
1178
1179 xs_html_add(top_nav,
1180 xs_html_tag("a",
1181 xs_html_attr("href", rss_url),
1182 xs_html_text(L("RSS"))),
1183 xs_html_text(" - "),
1184 xs_html_tag("a",
1185 xs_html_attr("href", admin_url),
1186 xs_html_attr("rel", "nofollow"),
1187 xs_html_text(L("private"))));
1188 }
1189 else {
1190 int n_len = notify_new_num(user);
1191 int p_len = pending_count(user);
1192 xs_html *notify_count = NULL;
1193 xs_html *pending_follow_count = NULL;
1194
1195 /* show the number of new notifications, if there are any */
1196 if (n_len) {
1197 xs *n_len_str = xs_fmt(" %d ", n_len);
1198 notify_count = xs_html_tag("sup",
1199 xs_html_attr("style", "background-color: red; color: white;"),
1200 xs_html_text(n_len_str));
1201 }
1202 else
1203 notify_count = xs_html_text("");
1204
1205 if (p_len) {
1206 xs *s = xs_fmt(" %d ", p_len);
1207 pending_follow_count = xs_html_tag("sup",
1208 xs_html_attr("style", "background-color: red; color: white;"),
1209 xs_html_text(s));
1210 }
1211 else
1212 pending_follow_count = xs_html_text("");
1213
1214 xs *admin_url = xs_fmt("%s/admin", user->actor);
1215 xs *notify_url = xs_fmt("%s/notifications", user->actor);
1216 xs *people_url = xs_fmt("%s/people", user->actor);
1217 xs *instance_url = xs_fmt("%s/instance", user->actor);
1218
1219 xs_html_add(top_nav,
1220 xs_html_tag("a",
1221 xs_html_attr("href", user->actor),
1222 xs_html_text(L("public"))),
1223 xs_html_text(" - "),
1224 xs_html_tag("a",
1225 xs_html_attr("href", admin_url),
1226 xs_html_text(L("private"))),
1227 xs_html_text(" - "),
1228 xs_html_tag("a",
1229 xs_html_attr("href", notify_url),
1230 xs_html_text(L("notifications"))),
1231 notify_count,
1232 xs_html_text(" - "),
1233 xs_html_tag("a",
1234 xs_html_attr("href", people_url),
1235 xs_html_text(L("people"))),
1236 pending_follow_count,
1237 xs_html_text(" - "),
1238 xs_html_tag("a",
1239 xs_html_attr("href", instance_url),
1240 xs_html_text(L("instance"))),
1241 xs_html_text(" "),
1242 xs_html_tag("form",
1243 xs_html_attr("style", "display: inline!important"),
1244 xs_html_attr("class", "snac-search-box"),
1245 xs_html_attr("action", admin_url),
1246 xs_html_sctag("input",
1247 xs_html_attr("type", "text"),
1248 xs_html_attr("name", "q"),
1249 xs_html_attr("title", L("Search posts by URL or content (regular expression), @user@host accounts, or #tag")),
1250 xs_html_attr("placeholder", L("Content search")))));
1251 }
1252
1253 xs_html_add(body,
1254 top_nav);
1255
1256 /* user info */
1257 xs_html *top_user = xs_html_tag("div",
1258 xs_html_attr("class", "h-card snac-top-user"));
1259
1260 if (read_only) {
1261 const char *header = xs_dict_get(user->config, "header");
1262 if (header && *header) {
1263 xs_html_add(top_user,
1264 xs_html_tag("div",
1265 xs_html_attr("class", "snac-top-user-banner"),
1266 xs_html_attr("style", "clear: both"),
1267 xs_html_sctag("br", NULL),
1268 xs_html_sctag("img",
1269 xs_html_attr("src", header))));
1270 }
1271 }
1272
1273 xs *handle = xs_fmt("@%s@%s",
1274 xs_dict_get(user->config, "uid"),
1275 xs_dict_get(srv_config, "host"));
1276
1277 xs *display_name = format_text_with_emoji(NULL, xs_dict_get(user->config, "name"), 1, proxy);
1278
1279 xs_html_add(top_user,
1280 xs_html_tag("p",
1281 xs_html_attr("class", "p-name snac-top-user-name"),
1282 xs_html_raw(display_name)),
1283 xs_html_tag("p",
1284 xs_html_attr("class", "snac-top-user-id"),
1285 xs_html_text(handle)));
1286
1287 /** instance announcement **/
1288
1289 if (!read_only) {
1290 double la = 0.0;
1291 xs *user_la = xs_dup(xs_dict_get(user->config, "last_announcement"));
1292 if (user_la != NULL)
1293 la = xs_number_get(user_la);
1294
1295 const t_announcement *an = announcement(la);
1296 if (an != NULL && (an->text != NULL)) {
1297 xs *s = xs_fmt("?da=%.0f", an->timestamp);
1298
1299 xs_html_add(top_user, xs_html_tag("div",
1300 xs_html_attr("class", "snac-announcement"),
1301 xs_html_text(an->text),
1302 xs_html_text(" "),
1303 xs_html_tag("a",
1304 xs_html_attr("href", s),
1305 xs_html_text("Dismiss"))));
1306 }
1307 }
1308
1309 if (read_only) {
1310 xs *bio = format_text_with_emoji(user, xs_dict_get(user->config, "bio"), 2, proxy);
1311
1312 xs_html *top_user_bio = xs_html_tag("div",
1313 xs_html_attr("class", "p-note snac-top-user-bio"),
1314 xs_html_raw(bio)); /* already sanitized */
1315
1316 xs_html_add(top_user,
1317 top_user_bio);
1318
1319 xs *metadata = NULL;
1320 const xs_dict *md = xs_dict_get(user->config, "metadata");
1321
1322 if (xs_type(md) == XSTYPE_DICT)
1323 metadata = xs_dup(md);
1324 else
1325 if (xs_type(md) == XSTYPE_STRING) {
1326 /* convert to dict for easier iteration */
1327 metadata = xs_dict_new();
1328 xs *l = xs_split(md, "\n");
1329 const char *ll;
1330
1331 xs_list_foreach(l, ll) {
1332 xs *kv = xs_split_n(ll, "=", 1);
1333 const char *k = xs_list_get(kv, 0);
1334 const char *v = xs_list_get(kv, 1);
1335
1336 if (k && v) {
1337 xs *kk = xs_strip_i(xs_dup(k));
1338 xs *vv = xs_strip_i(xs_dup(v));
1339 metadata = xs_dict_set(metadata, kk, vv);
1340 }
1341 }
1342 }
1343
1344 if (xs_type(metadata) == XSTYPE_DICT) {
1345 const xs_str *k;
1346 const xs_str *v;
1347
1348 xs_dict *val_links = user->links;
1349 if (xs_is_null(val_links))
1350 val_links = xs_stock(XSTYPE_DICT);
1351
1352 xs_html *snac_metadata = xs_html_tag("div",
1353 xs_html_attr("class", "snac-metadata"));
1354
1355 int c = 0;
1356 while (xs_dict_next(metadata, &k, &v, &c)) {
1357 xs_html *value;
1358
1359 if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/") || *v == '@') {
1360 /* is this link validated? */
1361 xs *verified_link = NULL;
1362 const xs_number *val_time = xs_dict_get(val_links, v);
1363 const char *url = NULL;
1364
1365 if (xs_is_string(val_time)) {
1366 /* resolve again, as it may be an account handle */
1367 url = val_time;
1368 val_time = xs_dict_get(val_links, val_time);
1369 }
1370
1371 if (xs_type(val_time) == XSTYPE_NUMBER) {
1372 time_t t = xs_number_get(val_time);
1373
1374 if (t > 0) {
1375 xs *s1 = xs_str_utctime(t, ISO_DATE_SPEC);
1376 verified_link = xs_fmt("%s (%s)", L("verified link"), s1);
1377 }
1378 }
1379
1380 if (!xs_is_null(verified_link)) {
1381 value = xs_html_tag("span",
1382 xs_html_attr("title", verified_link),
1383 xs_html_raw("✔ "),
1384 xs_html_tag("a",
1385 xs_html_attr("rel", "me"),
1386 xs_html_attr("target", "_blank"),
1387 xs_html_attr("href", url ? url : v),
1388 xs_html_text(v)));
1389 }
1390 else {
1391 value = xs_html_tag("a",
1392 xs_html_attr("rel", "me"),
1393 xs_html_attr("href", url ? url : v),
1394 xs_html_text(v));
1395 }
1396 }
1397 else
1398 if (xs_startswith(v, "gemini:/") || xs_startswith(v, "xmpp:")) {
1399 value = xs_html_tag("a",
1400 xs_html_attr("rel", "me"),
1401 xs_html_attr("href", v),
1402 xs_html_text(v));
1403 }
1404 else
1405 value = xs_html_text(v);
1406
1407 xs_html_add(snac_metadata,
1408 xs_html_tag("span",
1409 xs_html_attr("class", "snac-property-name"),
1410 xs_html_text(k)),
1411 xs_html_text(":"),
1412 xs_html_raw(" "),
1413 xs_html_tag("span",
1414 xs_html_attr("class", "snac-property-value"),
1415 value),
1416 xs_html_sctag("br", NULL));
1417 }
1418
1419 xs_html_add(top_user,
1420 snac_metadata);
1421 }
1422
1423 const char *latitude = xs_dict_get_def(user->config, "latitude", "");
1424 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1425
1426 if (*latitude && *longitude) {
1427 xs *label = xs_fmt("%s,%s", latitude, longitude);
1428 xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s",
1429 latitude, longitude);
1430
1431 xs_html_add(top_user,
1432 xs_html_tag("p",
1433 xs_html_text(L("Location: ")),
1434 xs_html_tag("a",
1435 xs_html_attr("href", url),
1436 xs_html_attr("target", "_blank"),
1437 xs_html_text(label))));
1438 }
1439
1440 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
1441 xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
1442
1443 xs_html_add(top_user,
1444 xs_html_tag("p",
1445 xs_html_text(s1)));
1446 }
1447 }
1448
1449 xs_html_add(body,
1450 top_user);
1451
1452 return body;
1453}
1454
1455
1456xs_html *html_checkbox(const char *form_name, const char *label, int flag)
1457/* helper for checkbox rendering */
1458{
1459 return xs_html_tag("p",
1460 xs_html_sctag("input",
1461 xs_html_attr("type", "checkbox"),
1462 xs_html_attr("name", form_name),
1463 xs_html_attr("id", form_name),
1464 xs_html_attr(flag ? "checked" : "", NULL)),
1465 xs_html_tag("label",
1466 xs_html_attr("for", form_name),
1467 xs_html_text(label)));
1468}
1469
1470
1471xs_html *html_top_controls(snac *user, int tb_friendly)
1472/* generates the top controls */
1473{
1474 if (tb_friendly) {
1475 /* generate only a link to the /operations entrypoint */
1476 xs *url = xs_fmt("%s/operations", user->actor);
1477
1478 return xs_html_tag("a",
1479 xs_html_attr("href", url),
1480 xs_html_text(L("Operations...")));
1481 }
1482
1483 xs *ops_action = xs_fmt("%s/admin/action", user->actor);
1484
1485 xs_html *top_controls = xs_html_tag("div",
1486 xs_html_attr("class", "snac-top-controls"),
1487
1488 /** new post **/
1489 html_note(user, L("New Post..."),
1490 "new_post_div", "new_post_form",
1491 L("What's on your mind?"), "",
1492 NULL, NULL,
1493 xs_stock(XSTYPE_FALSE), "",
1494 SCOPE_PUBLIC, NULL,
1495 NULL, 1, NULL, NULL, 0, NULL, NULL),
1496
1497 /** operations **/
1498 xs_html_tag("details",
1499 xs_html_tag("summary",
1500 xs_html_text(L("Operations..."))),
1501 xs_html_tag("p", NULL),
1502 xs_html_tag("form",
1503 xs_html_attr("autocomplete", "off"),
1504 xs_html_attr("method", "post"),
1505 xs_html_attr("action", ops_action),
1506 xs_html_sctag("input",
1507 xs_html_attr("type", "text"),
1508 xs_html_attr("name", "actor"),
1509 xs_html_attr("required", "required"),
1510 xs_html_attr("placeholder", "bob@example.com")),
1511 xs_html_text(" "),
1512 xs_html_sctag("input",
1513 xs_html_attr("type", "submit"),
1514 xs_html_attr("name", "action"),
1515 xs_html_attr("value", L("Follow"))),
1516 xs_html_text(" "),
1517 xs_html_text(L("(by URL or user@host)"))),
1518 xs_html_tag("p", NULL),
1519 xs_html_tag("form",
1520 xs_html_attr("autocomplete", "off"),
1521 xs_html_attr("method", "post"),
1522 xs_html_attr("action", ops_action),
1523 xs_html_sctag("input",
1524 xs_html_attr("type", "url"),
1525 xs_html_attr("name", "id"),
1526 xs_html_attr("required", "required"),
1527 xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1528 xs_html_text(" "),
1529 xs_html_sctag("input",
1530 xs_html_attr("type", "submit"),
1531 xs_html_attr("name", "action"),
1532 xs_html_attr("value", L("Boost"))),
1533 xs_html_text(" "),
1534 xs_html_text(L("(by URL)"))),
1535 xs_html_tag("p", NULL),
1536 xs_html_tag("form",
1537 xs_html_attr("autocomplete", "off"),
1538 xs_html_attr("method", "post"),
1539 xs_html_attr("action", ops_action),
1540 xs_html_sctag("input",
1541 xs_html_attr("type", "text"),
1542 xs_html_attr("name", "id"),
1543 xs_html_attr("required", "required"),
1544 xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1545 xs_html_text(" "),
1546 xs_html_sctag("input",
1547 xs_html_attr("type", "submit"),
1548 xs_html_attr("name", "action"),
1549 xs_html_attr("value", L("Like"))),
1550 xs_html_text(" "),
1551 xs_html_text(L("(by URL)"))),
1552 xs_html_tag("p", NULL),
1553 xs_html_tag("form",
1554 xs_html_attr("autocomplete", "off"),
1555 xs_html_attr("method", "post"),
1556 xs_html_attr("action", ops_action),
1557 xs_html_sctag("input",
1558 xs_html_attr("type", "text"),
1559 xs_html_attr("name", "eid"),
1560 xs_html_attr("required", "required"),
1561 xs_html_attr("placeholder", ":neocat:")),
1562 xs_html_text(" "),
1563 xs_html_sctag("input",
1564 xs_html_attr("type", "text"),
1565 xs_html_attr("name", "id"),
1566 xs_html_attr("required", "required"),
1567 xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
1568 xs_html_text(" "),
1569 xs_html_sctag("input",
1570 xs_html_attr("type", "submit"),
1571 xs_html_attr("name", "action"),
1572 xs_html_attr("value", L("EmojiReact"))),
1573 xs_html_text(" "),
1574 xs_html_text(L("(by URL)"))),
1575 xs_html_tag("p", NULL)));
1576
1577 /** user settings **/
1578
1579 const char *email = "[disabled by admin]";
1580
1581 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1582 email = xs_dict_get(user->config_o, "email");
1583 if (xs_is_null(email)) {
1584 email = xs_dict_get(user->config, "email");
1585
1586 if (xs_is_null(email))
1587 email = "";
1588 }
1589 }
1590
1591 const char *cw = xs_dict_get(user->config, "cw");
1592 if (xs_is_null(cw))
1593 cw = "";
1594
1595 const char *telegram_bot = xs_dict_get(user->config, "telegram_bot");
1596 if (xs_is_null(telegram_bot))
1597 telegram_bot = "";
1598
1599 const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id");
1600 if (xs_is_null(telegram_chat_id))
1601 telegram_chat_id = "";
1602
1603 const char *ntfy_server = xs_dict_get(user->config, "ntfy_server");
1604 if (xs_is_null(ntfy_server))
1605 ntfy_server = "";
1606
1607 const char *ntfy_token = xs_dict_get(user->config, "ntfy_token");
1608 if (xs_is_null(ntfy_token))
1609 ntfy_token = "";
1610
1611 const char *purge_days = xs_dict_get(user->config, "purge_days");
1612 if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
1613 purge_days = (char *)xs_number_str(purge_days);
1614 else
1615 purge_days = "0";
1616
1617 const xs_val *d_dm_f_u = xs_dict_get(user->config, "drop_dm_from_unknown");
1618 const xs_val *bot = xs_dict_get(user->config, "bot");
1619 const xs_val *a_private = xs_dict_get(user->config, "private");
1620 const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost");
1621 const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads");
1622 const xs_val *pending = xs_dict_get(user->config, "approve_followers");
1623 const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics");
1624 const xs_val *show_unlisted = xs_dict_get(user->config, "show_unlisted");
1625 const char *latitude = xs_dict_get_def(user->config, "latitude", "");
1626 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1627 const char *webhook = xs_dict_get_def(user->config, "notify_webhook", "");
1628 const char *post_langs = xs_dict_get_def(user->config, "post_langs", "");
1629 const char *excluded_langs = xs_dict_get_def(user->config, "excluded_langs", "");
1630 const char *text_browser_uas = xs_dict_get_def(user->config, "text_browser_uas", "");
1631
1632 xs *metadata = NULL;
1633 const xs_dict *md = xs_dict_get(user->config, "metadata");
1634
1635 if (xs_type(md) == XSTYPE_DICT) {
1636 const xs_str *k;
1637 const xs_str *v;
1638
1639 metadata = xs_str_new(NULL);
1640
1641 xs_dict_foreach(md, k, v) {
1642 xs *kp = xs_fmt("%s=%s", k, v);
1643
1644 if (*metadata)
1645 metadata = xs_str_cat(metadata, "\n");
1646 metadata = xs_str_cat(metadata, kp);
1647 }
1648 }
1649 else
1650 if (xs_type(md) == XSTYPE_STRING)
1651 metadata = xs_dup(md);
1652 else
1653 metadata = xs_str_new(NULL);
1654
1655 /* ui language */
1656 xs_html *lang_select = xs_html_tag("select",
1657 xs_html_attr("name", "web_ui_lang"));
1658
1659 const char *u_lang = xs_dict_get_def(user->config, "lang", "en");
1660 const char *lang;
1661 const xs_dict *langs;
1662
1663 xs_dict_foreach(srv_langs, lang, langs) {
1664 if (strcmp(u_lang, lang) == 0)
1665 xs_html_add(lang_select,
1666 xs_html_tag("option",
1667 xs_html_text(lang),
1668 xs_html_attr("value", lang),
1669 xs_html_attr("selected", "selected")));
1670 else
1671 xs_html_add(lang_select,
1672 xs_html_tag("option",
1673 xs_html_text(lang),
1674 xs_html_attr("value", lang)));
1675 }
1676
1677 /* timezone */
1678 xs_html *tz_select = xs_html_tag("select",
1679 xs_html_attr("name", "tz"));
1680
1681 xs *tzs = xs_tz_list();
1682 const char *tz;
1683
1684 xs_list_foreach(tzs, tz) {
1685 if (strcmp(tz, user->tz) == 0)
1686 xs_html_add(tz_select,
1687 xs_html_tag("option",
1688 xs_html_text(tz),
1689 xs_html_attr("value", tz),
1690 xs_html_attr("selected", "selected")));
1691 else
1692 xs_html_add(tz_select,
1693 xs_html_tag("option",
1694 xs_html_text(tz),
1695 xs_html_attr("value", tz)));
1696 }
1697
1698 xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
1699
1700 xs_html_add(top_controls,
1701 xs_html_tag("details",
1702 xs_html_tag("summary",
1703 xs_html_text(L("User Settings..."))),
1704 xs_html_tag("div",
1705 xs_html_attr("class", "snac-user-setup"),
1706 xs_html_tag("form",
1707 xs_html_attr("autocomplete", "off"),
1708 xs_html_attr("method", "post"),
1709 xs_html_attr("action", user_setup_action),
1710 xs_html_attr("enctype", "multipart/form-data"),
1711 xs_html_tag("p",
1712 xs_html_text(L("Display name:")),
1713 xs_html_sctag("br", NULL),
1714 xs_html_sctag("input",
1715 xs_html_attr("type", "text"),
1716 xs_html_attr("name", "name"),
1717 xs_html_attr("value", xs_dict_get(user->config, "name")),
1718 xs_html_attr("placeholder", L("Your name")))),
1719 xs_html_tag("p",
1720 xs_html_text(L("Avatar: ")),
1721 xs_html_sctag("input",
1722 xs_html_attr("type", "file"),
1723 xs_html_attr("name", "avatar_file"))),
1724 xs_html_tag("p",
1725 xs_html_sctag("input",
1726 xs_html_attr("type", "checkbox"),
1727 xs_html_attr("name", "avatar_delete")),
1728 xs_html_text(L("Delete current avatar"))),
1729 xs_html_tag("p",
1730 xs_html_text(L("Header image (banner): ")),
1731 xs_html_sctag("input",
1732 xs_html_attr("type", "file"),
1733 xs_html_attr("name", "header_file"))),
1734 xs_html_tag("p",
1735 xs_html_sctag("input",
1736 xs_html_attr("type", "checkbox"),
1737 xs_html_attr("name", "header_delete")),
1738 xs_html_text(L("Delete current header image"))),
1739 xs_html_tag("p",
1740 xs_html_text(L("Bio:")),
1741 xs_html_sctag("br", NULL),
1742 xs_html_tag("textarea",
1743 xs_html_attr("name", "bio"),
1744 xs_html_attr("cols", "40"),
1745 xs_html_attr("rows", "4"),
1746 xs_html_attr("placeholder", L("Write about yourself here...")),
1747 xs_html_text(xs_dict_get(user->config, "bio")))),
1748 xs_html_sctag("input",
1749 xs_html_attr("type", "checkbox"),
1750 xs_html_attr("name", "cw"),
1751 xs_html_attr("id", "cw"),
1752 xs_html_attr(strcmp(cw, "open") == 0 ? "checked" : "", NULL)),
1753 xs_html_tag("label",
1754 xs_html_attr("for", "cw"),
1755 xs_html_text(L("Always show sensitive content"))),
1756 xs_html_tag("p",
1757 xs_html_text(L("Email address for notifications:")),
1758 xs_html_sctag("br", NULL),
1759 xs_html_sctag("input",
1760 xs_html_attr("type", "text"),
1761 xs_html_attr("name", "email"),
1762 xs_html_attr("value", email),
1763 xs_html_attr("placeholder", "bob@example.com"))),
1764 xs_html_tag("p",
1765 xs_html_text(L("Telegram notifications (bot key and chat id):")),
1766 xs_html_sctag("br", NULL),
1767 xs_html_sctag("input",
1768 xs_html_attr("type", "text"),
1769 xs_html_attr("name", "telegram_bot"),
1770 xs_html_attr("value", telegram_bot),
1771 xs_html_attr("placeholder", L("Bot API key"))),
1772 xs_html_text(" "),
1773 xs_html_sctag("input",
1774 xs_html_attr("type", "text"),
1775 xs_html_attr("name", "telegram_chat_id"),
1776 xs_html_attr("value", telegram_chat_id),
1777 xs_html_attr("placeholder", L("Chat id")))),
1778 xs_html_tag("p",
1779 xs_html_text(L("ntfy notifications (ntfy server and token):")),
1780 xs_html_sctag("br", NULL),
1781 xs_html_sctag("input",
1782 xs_html_attr("type", "text"),
1783 xs_html_attr("name", "ntfy_server"),
1784 xs_html_attr("value", ntfy_server),
1785 xs_html_attr("placeholder", L("ntfy server - full URL (example: https://ntfy.sh/YourTopic)"))),
1786 xs_html_text(" "),
1787 xs_html_sctag("input",
1788 xs_html_attr("type", "text"),
1789 xs_html_attr("name", "ntfy_token"),
1790 xs_html_attr("value", ntfy_token),
1791 xs_html_attr("placeholder", L("ntfy token - if needed")))),
1792 xs_html_tag("p",
1793 xs_html_text(L("Notify webhook:")),
1794 xs_html_sctag("br", NULL),
1795 xs_html_sctag("input",
1796 xs_html_attr("type", "url"),
1797 xs_html_attr("name", "notify_webhook"),
1798 xs_html_attr("value", webhook),
1799 xs_html_attr("placeholder", L("http://example.com/webhook")))),
1800 xs_html_tag("p",
1801 xs_html_text(L("Maximum days to keep posts (0: server settings):")),
1802 xs_html_sctag("br", NULL),
1803 xs_html_sctag("input",
1804 xs_html_attr("type", "number"),
1805 xs_html_attr("name", "purge_days"),
1806 xs_html_attr("value", purge_days))),
1807 html_checkbox("drop_dm_from_unknown", L("Drop direct messages from people you don't follow"),
1808 xs_is_true(d_dm_f_u)),
1809 html_checkbox("bot", L("This account is a bot"),
1810 xs_is_true(bot)),
1811 html_checkbox("auto_boost", L("Auto-boost all mentions to this account"),
1812 xs_is_true(auto_boost)),
1813 html_checkbox("private", L("This account is private "
1814 "(posts are not shown through the web)"),
1815 xs_is_true(a_private)),
1816 html_checkbox("collapse_threads", L("Collapse top threads by default"),
1817 xs_is_true(coll_thrds)),
1818 html_checkbox("approve_followers", L("Follow requests must be approved"),
1819 xs_is_true(pending)),
1820 html_checkbox("show_contact_metrics", L("Publish follower and following metrics"),
1821 xs_is_true(show_foll)),
1822 html_checkbox("show_unlisted", L("Show unlisted posts in the public page and RSS feed"),
1823 xs_is_true(show_unlisted)),
1824 xs_html_tag("p",
1825 xs_html_text(L("Current location:")),
1826 xs_html_sctag("br", NULL),
1827 xs_html_sctag("input",
1828 xs_html_attr("type", "text"),
1829 xs_html_attr("name", "latitude"),
1830 xs_html_attr("value", latitude),
1831 xs_html_attr("placeholder", "latitude")),
1832 xs_html_text(" "),
1833 xs_html_sctag("input",
1834 xs_html_attr("type", "text"),
1835 xs_html_attr("name", "longitude"),
1836 xs_html_attr("value", longitude),
1837 xs_html_attr("placeholder", "longitude"))),
1838 xs_html_tag("p",
1839 xs_html_text(L("Profile metadata (key=value pairs in each line):")),
1840 xs_html_sctag("br", NULL),
1841 xs_html_tag("textarea",
1842 xs_html_attr("name", "metadata"),
1843 xs_html_attr("cols", "40"),
1844 xs_html_attr("rows", "4"),
1845 xs_html_attr("placeholder", "Blog=https:/"
1846 "/example.com/my-blog\nGPG Key=1FA54\n..."),
1847 xs_html_text(metadata))),
1848
1849 xs_html_tag("p",
1850 xs_html_text(L("Web interface language:")),
1851 xs_html_sctag("br", NULL),
1852 lang_select),
1853
1854 xs_html_tag("p",
1855 xs_html_text(L("Time zone:")),
1856 xs_html_sctag("br", NULL),
1857 tz_select),
1858
1859 xs_html_tag("p",
1860 xs_html_text(L("Languages you usually post in:")),
1861 xs_html_sctag("br", NULL),
1862 xs_html_sctag("input",
1863 xs_html_attr("type", "text"),
1864 xs_html_attr("name", "post_langs"),
1865 xs_html_attr("value", post_langs),
1866 xs_html_attr("placeholder", L("en fr es de_AT")))),
1867
1868 xs_html_tag("p",
1869 xs_html_text(L("Don't show posts written in these languages:")),
1870 xs_html_sctag("br", NULL),
1871 xs_html_sctag("input",
1872 xs_html_attr("type", "text"),
1873 xs_html_attr("name", "excluded_langs"),
1874 xs_html_attr("value", excluded_langs),
1875 xs_html_attr("placeholder", L("en fr es de_AT")))),
1876
1877 xs_html_tag("p",
1878 xs_html_text(L("Show a simpler web UI for browsers with these user agents (one per line):")),
1879 xs_html_sctag("br", NULL),
1880 xs_html_tag("textarea",
1881 xs_html_attr("name", "text_browser_uas"),
1882 xs_html_attr("cols", "40"),
1883 xs_html_attr("rows", "4"),
1884 xs_html_attr("placeholder", "Links\nLynx\nNetSurf"),
1885 xs_html_text(text_browser_uas))),
1886
1887 xs_html_tag("p",
1888 xs_html_text(L("New password:")),
1889 xs_html_sctag("br", NULL),
1890 xs_html_sctag("input",
1891 xs_html_attr("type", "password"),
1892 xs_html_attr("name", "passwd1"),
1893 xs_html_attr("value", ""))),
1894 xs_html_tag("p",
1895 xs_html_text(L("Repeat new password:")),
1896 xs_html_sctag("br", NULL),
1897 xs_html_sctag("input",
1898 xs_html_attr("type", "password"),
1899 xs_html_attr("name", "passwd2"),
1900 xs_html_attr("value", ""))),
1901
1902 xs_html_sctag("input",
1903 xs_html_attr("type", "submit"),
1904 xs_html_attr("class", "button"),
1905 xs_html_attr("value", L("Update user info"))),
1906
1907 xs_html_tag("p", NULL)))));
1908
1909 xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor);
1910 xs *followed_hashtags = xs_join(xs_dict_get_def(user->config,
1911 "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
1912
1913 xs_html_add(top_controls,
1914 xs_html_tag("details",
1915 xs_html_tag("summary",
1916 xs_html_text(L("Followed hashtags..."))),
1917 xs_html_tag("p",
1918 xs_html_text(L("One hashtag per line"))),
1919 xs_html_tag("div",
1920 xs_html_attr("class", "snac-followed-hashtags"),
1921 xs_html_tag("form",
1922 xs_html_attr("autocomplete", "off"),
1923 xs_html_attr("method", "post"),
1924 xs_html_attr("action", followed_hashtags_action),
1925 xs_html_attr("enctype", "multipart/form-data"),
1926
1927 xs_html_tag("textarea",
1928 xs_html_attr("name", "followed_hashtags"),
1929 xs_html_attr("cols", "40"),
1930 xs_html_attr("rows", "4"),
1931 xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic\nhttps:/"
1932 "/mastodon.social/tags/dogs"),
1933 xs_html_text(followed_hashtags)),
1934
1935 xs_html_tag("br", NULL),
1936
1937 xs_html_sctag("input",
1938 xs_html_attr("type", "submit"),
1939 xs_html_attr("class", "button"),
1940 xs_html_attr("value", L("Update hashtags")))))));
1941
1942 xs *blocked_hashtags_action = xs_fmt("%s/admin/blocked-hashtags", user->actor);
1943 xs *blocked_hashtags = xs_join(xs_dict_get_def(user->config,
1944 "blocked_hashtags", xs_stock(XSTYPE_LIST)), "\n");
1945
1946 xs_html_add(top_controls,
1947 xs_html_tag("details",
1948 xs_html_tag("summary",
1949 xs_html_text(L("Blocked hashtags..."))),
1950 xs_html_tag("p",
1951 xs_html_text(L("One hashtag per line"))),
1952 xs_html_tag("div",
1953 xs_html_attr("class", "snac-blocked-hashtags"),
1954 xs_html_tag("form",
1955 xs_html_attr("autocomplete", "off"),
1956 xs_html_attr("method", "post"),
1957 xs_html_attr("action", blocked_hashtags_action),
1958 xs_html_attr("enctype", "multipart/form-data"),
1959
1960 xs_html_tag("textarea",
1961 xs_html_attr("name", "blocked_hashtags"),
1962 xs_html_attr("cols", "40"),
1963 xs_html_attr("rows", "4"),
1964 xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"),
1965 xs_html_text(blocked_hashtags)),
1966
1967 xs_html_tag("br", NULL),
1968
1969 xs_html_sctag("input",
1970 xs_html_attr("type", "submit"),
1971 xs_html_attr("class", "button"),
1972 xs_html_attr("value", L("Update hashtags")))))));
1973
1974 xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor);
1975 xs *muted_words = xs_join(xs_dict_get_def(user->config,
1976 "muted_words", xs_stock(XSTYPE_LIST)), "\n");
1977
1978 xs_html_add(top_controls,
1979 xs_html_tag("details",
1980 xs_html_tag("summary",
1981 xs_html_text(L("Muted words..."))),
1982 xs_html_tag("p",
1983 xs_html_text(L("One word per line, partial matches count"))),
1984 xs_html_tag("div",
1985 xs_html_attr("class", "snac-muted-words"),
1986 xs_html_tag("form",
1987 xs_html_attr("autocomplete", "off"),
1988 xs_html_attr("method", "post"),
1989 xs_html_attr("action", muted_words_action),
1990 xs_html_attr("enctype", "multipart/form-data"),
1991
1992 xs_html_tag("textarea",
1993 xs_html_attr("name", "muted_words"),
1994 xs_html_attr("cols", "40"),
1995 xs_html_attr("rows", "4"),
1996 xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"),
1997 xs_html_text(muted_words)),
1998
1999 xs_html_tag("br", NULL),
2000
2001 xs_html_sctag("input",
2002 xs_html_attr("type", "submit"),
2003 xs_html_attr("class", "button"),
2004 xs_html_attr("value", L("Update muted words")))))));
2005
2006 return top_controls;
2007}
2008
2009
2010static xs_html *html_button(const char *clss, const char *label, const char *hint)
2011{
2012 xs *c = xs_fmt("snac-btn-%s", clss);
2013
2014 /* use an NULL tag to separate non-css-classed buttons from one another */
2015 return xs_html_container(
2016 xs_html_sctag("input",
2017 xs_html_attr("type", "submit"),
2018 xs_html_attr("name", "action"),
2019 xs_html_attr("class", c),
2020 xs_html_attr("value", label),
2021 xs_html_attr("title", hint)),
2022 xs_html_text("\n"));
2023}
2024
2025
2026xs_str *build_mentions(snac *user, const xs_dict *msg)
2027/* returns a string with the mentions in msg */
2028{
2029 xs_str *s = xs_str_new(NULL);
2030 const char *list = xs_dict_get(msg, "tag");
2031 const char *v;
2032 int c = 0;
2033
2034 while (xs_list_next(list, &v, &c)) {
2035 const char *type = xs_dict_get(v, "type");
2036 const char *href = xs_dict_get(v, "href");
2037 const char *name = xs_dict_get(v, "name");
2038
2039 if (type && strcmp(type, "Mention") == 0 &&
2040 href && strcmp(href, user->actor) != 0 && name) {
2041 xs *s1 = NULL;
2042
2043 if (name[0] != '@') {
2044 s1 = xs_fmt("@%s", name);
2045 name = s1;
2046 }
2047
2048 xs *l = xs_split(name, "@");
2049
2050 /* is it a name without a host? */
2051 if (xs_list_len(l) < 3) {
2052 /* split the href and pick the host name LIKE AN ANIMAL */
2053 /* would be better to query the webfinger but *won't do that* here */
2054 xs *l2 = xs_split(href, "/");
2055
2056 if (xs_list_len(l2) >= 3) {
2057 xs *s1 = xs_fmt("%s@%s ", name, xs_list_get(l2, 2));
2058
2059 if (xs_str_in(s, s1) == -1)
2060 s = xs_str_cat(s, s1);
2061 }
2062 }
2063 else {
2064 if (xs_str_in(s, name) == -1) {
2065 s = xs_str_cat(s, name);
2066 s = xs_str_cat(s, " ");
2067 }
2068 }
2069 }
2070 }
2071
2072#if 0
2073 /* disabled by now */
2074
2075 /* also add the author of this post as a mention */
2076 const char *atto = get_atto(msg);
2077
2078 if (xs_is_string(atto) && *atto && strcmp(atto, user->actor) != 0) {
2079 /* check if this author is already in the mentions string */
2080 xs *l = xs_split(atto, "/");
2081
2082 if (xs_list_len(l) > 3) {
2083 xs *actor_o = NULL;
2084 if (valid_status(object_get(atto, &actor_o))) {
2085 const char *uname = xs_dict_get(actor_o, "preferredUsername");
2086 if (!xs_is_null(uname) && *uname) {
2087 xs *handle = xs_fmt("@%s@%s ", uname, xs_list_get(l, 2));
2088 if (xs_str_in(s, handle) == -1) {
2089 s = xs_str_cat(s, handle);
2090 }
2091 }
2092 }
2093 }
2094 }
2095#endif
2096
2097 if (*s) {
2098 xs *s1 = s;
2099 s = xs_fmt("\n\n\nCC: %s", s1);
2100 }
2101
2102 return s;
2103}
2104
2105
2106xs_html *html_entry_controls(snac *user, const char *actor,
2107 const xs_dict *msg, const char *md5, int tb_friendly)
2108{
2109 const char *id = xs_dict_get(msg, "id");
2110 const char *group = xs_dict_get(msg, "audience");
2111
2112 xs *likes = object_likes(id);
2113 xs *boosts = object_announces(id);
2114
2115 xs *action = xs_fmt("%s/admin/action", user->actor);
2116 xs *redir = xs_fmt("%s_entry", md5);
2117
2118 xs_html *form;
2119 xs_html *controls = xs_html_tag("div",
2120 xs_html_attr("class", "snac-controls"),
2121 form = xs_html_tag("form",
2122 xs_html_attr("autocomplete", "off"),
2123 xs_html_attr("method", "post"),
2124 xs_html_attr("action", action),
2125 xs_html_sctag("input",
2126 xs_html_attr("type", "hidden"),
2127 xs_html_attr("name", "id"),
2128 xs_html_attr("value", id)),
2129 xs_html_sctag("input",
2130 xs_html_attr("type", "hidden"),
2131 xs_html_attr("name", "actor"),
2132 xs_html_attr("value", actor)),
2133 xs_html_sctag("input",
2134 xs_html_attr("type", "hidden"),
2135 xs_html_attr("name", "group"),
2136 xs_html_attr("value", xs_is_null(group) ? "" : group)),
2137 xs_html_sctag("input",
2138 xs_html_attr("type", "hidden"),
2139 xs_html_attr("name", "redir"),
2140 xs_html_attr("value", redir))));
2141
2142 if (!is_msg_mine(user, id)) {
2143 if (xs_list_in(likes, user->md5) == -1) {
2144 /* not already liked; add button */
2145 xs_html_add(form,
2146 html_button("like", L("Like"), L("Say you like this post")));
2147 }
2148 else {
2149 /* not like it anymore */
2150 xs_html_add(form,
2151 html_button("unlike", L("Unlike"), L("Nah don't like it that much")));
2152 }
2153 }
2154 else {
2155 if (is_pinned(user, id))
2156 xs_html_add(form,
2157 html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
2158 else
2159 xs_html_add(form,
2160 html_button("pin", L("Pin"), L("Pin this post to the top of your timeline")));
2161 }
2162
2163 if (is_msg_public(msg)) {
2164 if (xs_list_in(boosts, user->md5) == -1) {
2165 /* not already boosted; add button */
2166 xs_html_add(form,
2167 html_button("boost", L("Boost"), L("Announce this post to your followers")));
2168 }
2169 else {
2170 /* already boosted; add button to regret */
2171 xs_html_add(form,
2172 html_button("unboost", L("Unboost"), L("I regret I boosted this")));
2173 }
2174 }
2175
2176 if (is_bookmarked(user, id))
2177 xs_html_add(form,
2178 html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
2179 else
2180 xs_html_add(form,
2181 html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
2182
2183 if (strcmp(actor, user->actor) != 0) {
2184 /* controls for other actors than this one */
2185 if (following_check(user, actor)) {
2186 xs_html_add(form,
2187 html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
2188 }
2189 else {
2190 xs_html_add(form,
2191 html_button("follow", L("Follow"), L("Start following this user's activity")));
2192 }
2193
2194 if (!xs_is_null(group)) {
2195 if (following_check(user, group)) {
2196 xs_html_add(form,
2197 html_button("unfollow", L("Unfollow Group"),
2198 L("Stop following this group or channel")));
2199 }
2200 else {
2201 xs_html_add(form,
2202 html_button("follow", L("Follow Group"),
2203 L("Start following this group or channel")));
2204 }
2205 }
2206
2207 xs_html_add(form,
2208 html_button("mute", L("MUTE"),
2209 L("Block any activity from this user forever")));
2210 }
2211
2212 if (!xs_is_true(xs_dict_get(srv_config, "hide_delete_post_button")))
2213 xs_html_add(form,
2214 html_button("delete", L("Delete"), L("Delete this post")));
2215
2216 xs_html_add(form,
2217 html_button("hide", L("Hide"), L("Hide this post and its children")));
2218
2219 if (tb_friendly) {
2220 /* generate a link a More... text pointing to the one-post-only timeline */
2221 xs *url = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
2222
2223 xs_html_add(controls,
2224 xs_html_tag("p", NULL),
2225 xs_html_tag("a",
2226 xs_html_attr("href", url),
2227 xs_html_text(L("More..."))));
2228
2229 /* and DONE */
2230
2231 return controls;
2232 }
2233
2234 const char *prev_src = xs_dict_get(msg, "sourceContent");
2235
2236 if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/
2237 /* post can be edited */
2238 xs *div_id = xs_fmt("%s_edit", md5);
2239 xs *form_id = xs_fmt("%s_edit_form", md5);
2240 xs *redir = xs_fmt("%s_entry", md5);
2241
2242 xs *att_files = xs_list_new();
2243 xs *att_alt_texts = xs_list_new();
2244
2245 const xs_list *att_list = xs_dict_get(msg, "attachment");
2246
2247 if (xs_is_list(att_list)) {
2248 const xs_dict *d;
2249
2250 xs_list_foreach(att_list, d) {
2251 const char *att_file = xs_dict_get(d, "url");
2252 const char *att_alt_text = xs_dict_get(d, "name");
2253
2254 if (xs_is_string(att_file) && xs_is_string(att_alt_text)) {
2255 att_files = xs_list_append(att_files, att_file);
2256 att_alt_texts = xs_list_append(att_alt_texts, att_alt_text);
2257 }
2258 }
2259 }
2260
2261 const char *note_lang = NULL;
2262 const xs_dict *cmap = xs_dict_get(msg, "contentMap");
2263 if (xs_is_dict(cmap)) {
2264 const char *dummy;
2265 int c = 0;
2266
2267 xs_dict_next(cmap, ¬e_lang, &dummy, &c);
2268 }
2269
2270 const int edit_scope = get_msg_visibility(msg);
2271
2272 xs_html_add(controls, xs_html_tag("div",
2273 xs_html_tag("p", NULL),
2274 html_note(user, L("Edit..."),
2275 div_id, form_id,
2276 "", prev_src,
2277 id, NULL,
2278 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
2279 edit_scope, redir,
2280 NULL, 0, att_files, att_alt_texts, is_draft(user, id),
2281 xs_dict_get(msg, "published"), note_lang)),
2282 xs_html_tag("p", NULL));
2283 }
2284
2285 if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/
2286 /* the post textarea */
2287 xs *div_id = xs_fmt("%s_reply", md5);
2288 xs *form_id = xs_fmt("%s_reply_form", md5);
2289 xs *e_react = emoji_reacted(user, id);
2290
2291 xs_html_add(controls, xs_html_tag("div",
2292 xs_html_tag("p", NULL),
2293 html_emoji(
2294 user, L("Emoji react..."),
2295 div_id, form_id,
2296 ":neocat:", id,
2297 e_react)),
2298 xs_html_tag("p", NULL));
2299 }
2300
2301 { /** reply **/
2302 /* the post textarea */
2303 xs *ct = build_mentions(user, msg);
2304 xs *div_id = xs_fmt("%s_reply", md5);
2305 xs *form_id = xs_fmt("%s_reply_form", md5);
2306 xs *redir = xs_fmt("%s_entry", md5);
2307
2308 const int scope = get_msg_visibility(msg);
2309
2310 xs_html_add(controls, xs_html_tag("div",
2311 xs_html_tag("p", NULL),
2312 html_note(user, L("Reply..."),
2313 div_id, form_id,
2314 "", ct,
2315 NULL, NULL,
2316 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
2317 scope, redir,
2318 id, 0, NULL, NULL, 0, NULL, NULL)),
2319 xs_html_tag("p", NULL));
2320 }
2321
2322 return controls;
2323}
2324
2325
2326xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2327 int level, const char *md5, int hide_children, int tb_friendly)
2328{
2329 const char *id = xs_dict_get(msg, "id");
2330 const char *type = xs_dict_get(msg, "type");
2331 const char *actor;
2332 const char *v;
2333 int has_title = 0;
2334 int collapse_threads = 0;
2335 const char *proxy = NULL;
2336
2337 if (user && !read_only && xs_is_true(xs_dict_get(srv_config, "proxy_media")))
2338 proxy = user->actor;
2339
2340 /* do not show non-public messages in the public timeline */
2341 if ((read_only || !user) && !is_msg_public(msg))
2342 return NULL;
2343
2344 if (id && is_instance_blocked(id))
2345 return NULL;
2346
2347 if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
2348 collapse_threads = 1;
2349
2350 /* hidden? do nothing more for this conversation */
2351 if (user && is_hidden(user, id)) {
2352 xs *s1 = xs_fmt("%s_entry", md5);
2353
2354 /* return just an dummy anchor, to keep position after hitting 'Hide' */
2355 return xs_html_tag("div",
2356 xs_html_tag("a",
2357 xs_html_attr("name", s1)));
2358 }
2359
2360 /* avoid too deep nesting, as it may be a loop */
2361 if (level >= MAX_CONVERSATION_LEVELS)
2362 return xs_html_tag("mark",
2363 xs_html_text(L("Truncated (too deep)")));
2364
2365 const char *lang = NULL;
2366 const xs_dict *cmap = xs_dict_get(msg, "contentMap");
2367 if (xs_is_dict(cmap)) {
2368 const char *dummy;
2369 int c = 0;
2370
2371 xs_dict_next(cmap, &lang, &dummy, &c);
2372
2373 if (user && xs_is_string(lang)) {
2374 /* discard posts in excluded languages */
2375 const char *excluded_langs = xs_dict_get(user->config, "excluded_langs");
2376
2377 if (xs_is_string(excluded_langs) &&
2378 xs_str_in(excluded_langs, lang) != -1) {
2379 snac_debug(user, 1, xs_fmt("excluded post in language '%s'", lang));
2380 return NULL;
2381 }
2382 }
2383 }
2384
2385 if (strcmp(type, "Follow") == 0) {
2386 return xs_html_tag("div",
2387 xs_html_attr("class", "snac-post"),
2388 xs_html_tag("div",
2389 xs_html_attr("class", "snac-post-header"),
2390 xs_html_tag("div",
2391 xs_html_attr("class", "snac-origin"),
2392 xs_html_text(L("follows you"))),
2393 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL, lang)));
2394 }
2395 else
2396 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
2397 /* skip oddities */
2398 snac_debug(user, 1, xs_fmt("html_entry: ignoring object type '%s' %s", type, id));
2399 return NULL;
2400 }
2401
2402 /* ignore notes with "name", as they are votes to Questions */
2403 if (strcmp(type, "Note") == 0 && !xs_is_null(xs_dict_get(msg, "name")))
2404 return NULL;
2405
2406 /* get the attributedTo */
2407 if ((actor = get_atto(msg)) == NULL)
2408 return NULL;
2409
2410 /* ignore muted morons immediately */
2411 if (user && is_muted(user, actor)) {
2412 xs *s1 = xs_fmt("%s_entry", md5);
2413
2414 /* return just an dummy anchor, to keep position after hitting 'MUTE' */
2415 return xs_html_tag("div",
2416 xs_html_tag("a",
2417 xs_html_attr("name", s1)));
2418 }
2419
2420 /* don't show followers-only notes from not followed users */
2421 if (user && get_msg_visibility(msg) == SCOPE_FOLLOWERS &&
2422 strcmp(user->actor, actor) != 0 && following_check(user, actor) == 0) {
2423 return NULL;
2424 }
2425
2426 if ((user == NULL || strcmp(actor, user->actor) != 0)
2427 && !valid_status(actor_get(actor, NULL))) {
2428
2429 if (user)
2430 enqueue_actor_refresh(user, actor, 0);
2431
2432 return NULL;
2433 }
2434
2435 /** html_entry top tag **/
2436 xs_html *entry_top = xs_html_tag("div", NULL);
2437
2438 {
2439 xs *s1 = xs_fmt("%s_entry", md5);
2440 xs_html_add(entry_top,
2441 xs_html_tag("a",
2442 xs_html_attr("name", s1)));
2443 }
2444
2445 xs_html *entry = xs_html_tag("div",
2446 xs_html_attr("class", level == 0 ? "snac-post" : "snac-child"));
2447
2448 xs_html_add(entry_top,
2449 entry);
2450
2451 /** post header **/
2452
2453 xs_html *score;
2454 xs_html *post_header = xs_html_tag("div",
2455 xs_html_attr("class", "snac-post-header"),
2456 score = xs_html_tag("div",
2457 xs_html_attr("class", "snac-score")));
2458
2459 xs_html_add(entry,
2460 post_header);
2461
2462 if (user && is_pinned(user, id)) {
2463 /* add a pin emoji */
2464 xs_html_add(score,
2465 xs_html_tag("span",
2466 xs_html_attr("title", L("Pinned")),
2467 xs_html_raw(" 📌 ")));
2468 }
2469
2470 if (user && !read_only && is_bookmarked(user, id)) {
2471 /* add a bookmark emoji */
2472 xs_html_add(score,
2473 xs_html_tag("span",
2474 xs_html_attr("title", L("Bookmarked")),
2475 xs_html_raw(" 🔖 ")));
2476 }
2477
2478 if (strcmp(type, "Question") == 0) {
2479 /* add the ballot box emoji */
2480 xs_html_add(score,
2481 xs_html_tag("span",
2482 xs_html_attr("title", L("Poll")),
2483 xs_html_raw(" 🗳 ")));
2484
2485 if (user && was_question_voted(user, id)) {
2486 /* add a check to show this poll was voted */
2487 xs_html_add(score,
2488 xs_html_tag("span",
2489 xs_html_attr("title", L("Voted")),
2490 xs_html_raw(" ✓ ")));
2491 }
2492 }
2493
2494 if (strcmp(type, "Event") == 0) {
2495 /* add the calendar emoji */
2496 xs_html_add(score,
2497 xs_html_tag("span",
2498 xs_html_attr("title", L("Event")),
2499 xs_html_raw(" 📅 ")));
2500 }
2501
2502 /* if it's a user from this same instance, add the score */
2503 if (xs_startswith(id, srv_baseurl)) {
2504 int n_likes = object_likes_len(id);
2505 int n_boosts = object_announces_len(id);
2506
2507 /* alternate emojis: %d 👍 %d 🔁 */
2508 xs *s1 = xs_fmt("%d ★ %d ↺\n", n_likes, n_boosts);
2509
2510 xs_html_add(score,
2511 xs_html_raw(s1));
2512 }
2513
2514 xs *boosts = object_announces(id);
2515
2516 if (xs_list_len(boosts)) {
2517 /* if somebody boosted this, show as origin */
2518 const char *p = xs_list_get(boosts, -1);
2519 xs *actor_r = NULL;
2520
2521 if (user && xs_list_in(boosts, user->md5) != -1) {
2522 /* we boosted this */
2523 xs_html_add(post_header,
2524 xs_html_tag("div",
2525 xs_html_attr("class", "snac-origin"),
2526 xs_html_tag("a",
2527 xs_html_attr("href", user->actor),
2528 xs_html_text(xs_dict_get(user->config, "name"))),
2529 xs_html_text(" "),
2530 xs_html_text(L("boosted"))));
2531 }
2532 else
2533 if (valid_status(object_get_by_md5(p, &actor_r))) {
2534 xs *name = actor_name(actor_r, proxy);
2535
2536 if (!xs_is_null(name)) {
2537 xs *href = NULL;
2538 const char *id = xs_dict_get(actor_r, "id");
2539 int fwers = 0;
2540 int fwing = 0;
2541
2542 if (user != NULL) {
2543 fwers = follower_check(user, id);
2544 fwing = following_check(user, id);
2545 }
2546
2547 if (!read_only && (fwers || fwing))
2548 href = xs_fmt("%s/people/%s", user->actor, p);
2549 else
2550 href = xs_dup(id);
2551
2552 xs_html_add(post_header,
2553 xs_html_tag("div",
2554 xs_html_attr("class", "snac-origin"),
2555 xs_html_tag("a",
2556 xs_html_attr("href", href),
2557 xs_html_raw(name)), /* already sanitized */
2558 xs_html_text(" "),
2559 xs_html_text(L("boosted"))));
2560 }
2561 }
2562 }
2563
2564 if (user && strcmp(type, "Note") == 0) {
2565 /* is the parent not here? */
2566 const char *parent = get_in_reply_to(msg);
2567
2568 if (!xs_is_null(parent) && *parent) {
2569 if (!timeline_here(user, parent)) {
2570 xs_html_add(post_header,
2571 xs_html_tag("div",
2572 xs_html_attr("class", "snac-origin"),
2573 xs_html_text(L("in reply to")),
2574 xs_html_text(" "),
2575 xs_html_tag("a",
2576 xs_html_attr("href", parent),
2577 xs_html_text("»"))));
2578 }
2579 }
2580 }
2581
2582 xs_html_add(post_header,
2583 html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5, lang));
2584
2585 /** post content **/
2586
2587 xs_html *snac_content_wrap = xs_html_tag("div",
2588 xs_html_attr("class", "e-content snac-content"));
2589
2590 if (xs_is_string(lang))
2591 xs_html_add(snac_content_wrap,
2592 xs_html_attr("lang", lang));
2593
2594 xs_html_add(entry,
2595 snac_content_wrap);
2596
2597 if (!has_title && !xs_is_null(v = xs_dict_get(msg, "name"))) {
2598 xs_html_add(snac_content_wrap,
2599 xs_html_tag("h3",
2600 xs_html_attr("class", "snac-entry-title"),
2601 xs_html_text(v)));
2602
2603 has_title = 1;
2604 }
2605
2606 xs_html *snac_content = NULL;
2607
2608 v = xs_dict_get(msg, "summary");
2609
2610 /* if it has summary or marked as sensitive - add CW */
2611 /* come clients don't set "sensitive" flag properly */
2612 if ((!xs_is_null(v) && *v) || xs_type(xs_dict_get(msg, "sensitive")) == XSTYPE_TRUE) {
2613 if (xs_is_null(v) || *v == '\0')
2614 v = "...";
2615
2616 const char *cw = "";
2617
2618 if (user) {
2619 /* only show it when not in the public timeline and the config setting is "open" */
2620 cw = xs_dict_get(user->config, "cw");
2621 if (xs_is_null(cw) || read_only)
2622 cw = "";
2623 }
2624
2625 /* summary can't contain HTML */
2626 xs *s1 = xs_regex_replace(v, "<[^>]+>", "");
2627
2628 /* crop stupidly long summaries */
2629 if (xs_utf8_len(s1) > 1024) {
2630 s1 = xs_utf8_crop_i(s1, 0, 1024);
2631 s1 = xs_str_cat(s1, "...");
2632 }
2633
2634 snac_content = xs_html_tag("details",
2635 xs_html_attr(cw, NULL),
2636 xs_html_tag("summary",
2637 xs_html_text(s1),
2638 xs_html_text(L(" [SENSITIVE CONTENT]"))));
2639 }
2640 else
2641 if (user &&
2642 /* muted_words is all lowercase and sorted for performance */
2643 (v = words_in_content(xs_dict_get(user->config, "muted_words"),
2644 xs_dict_get(msg, "content"))) != NULL) {
2645 snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id));
2646 snac_content = xs_html_tag("details",
2647 xs_html_tag("summary",
2648 xs_html_text(L("Muted: ")),
2649 xs_html_text(v)));
2650 }
2651 else {
2652 snac_content = xs_html_tag("div", NULL);
2653 }
2654
2655 xs_html_add(snac_content_wrap,
2656 snac_content);
2657
2658 /* add all emoji reacts */
2659 int is_emoji = 0;
2660 if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2661 int c = 0;
2662 const xs_dict *k;
2663 xs *ls = xs_list_new();
2664 xs *sfrl = xs_dict_new();
2665 xs *rl = object_get_emoji_reacts(id);
2666
2667 while (xs_list_next(rl, &v, &c)) {
2668 xs *m = NULL;
2669 if (valid_status(object_get_by_md5(v, &m))) {
2670 const char *content = xs_dict_get(m, "content");
2671 const char *actor = xs_dict_get(m, "actor");
2672 const xs_list *contentl = xs_dict_get(sfrl, content);
2673
2674 if ((user && is_muted(user, actor)) || is_instance_blocked(actor))
2675 continue;
2676
2677 xs *actors = xs_list_new();
2678 actors = xs_list_append(actors, actor);
2679 char me = actor && user && strcmp(actor, user->actor) == 0;
2680 int count = 1;
2681
2682 if (contentl) {
2683 count = atoi(xs_list_get(contentl, 0)) + 1;
2684 const xs_list *actorsc = xs_list_get(contentl, 1);
2685 if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
2686 me = 1;
2687
2688 if (xs_list_in(actorsc, actor) != -1) {
2689 xs_free(actors);
2690 actors = xs_dup(actorsc);
2691 }
2692 else
2693 actors = xs_list_cat(actors, actorsc);
2694 }
2695
2696 xs *fl = xs_list_new();
2697 xs *c1 = xs_fmt("%d", count);
2698 xs *c2 = xs_fmt("%d", me);
2699 fl = xs_list_append(fl, c1, actors, c2);
2700 sfrl = xs_dict_append(sfrl, content, fl);
2701 }
2702 }
2703
2704 c = 0;
2705
2706 while (xs_list_next(rl, &k, &c)) {
2707 xs *m = NULL;
2708 if (valid_status(object_get_by_md5(k, &m))) {
2709 const xs_dict *tag = xs_dict_get(m, "tag");
2710 const xs_dict *ide = xs_dict_get(m, "id");
2711
2712 const char *content = xs_dict_get(m, "content");
2713 const char *shortname;
2714 shortname = xs_dict_get(m, "content");
2715
2716 const xs_list *items = xs_dict_get(sfrl, content);
2717
2718 if (!xs_is_null(items)) {
2719 const char *nb = xs_list_get(items, 0);
2720 const xs_list *actors = xs_list_get(items, 1);
2721 const char me = *xs_list_get(items, 2) == '1';
2722
2723 is_emoji = 1;
2724
2725 xs *al = xs_join(actors, ",\n\t");
2726 xs *act = atoi(nb) > 1 ?
2727 xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
2728 xs_dup(xs_dict_get(m, "actor"));
2729
2730 xs *class = xs_list_new();
2731 class = xs_list_append(class, "snac-reaction");
2732
2733 xs_html *ret = NULL;
2734 if (tag && shortname) {
2735 xs *cl = xs_list_new();
2736 cl = xs_list_append(cl, "snac-reaction-image");
2737 xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
2738
2739 emoji = xs_strip_chars_i(emoji, ":");
2740
2741 if (me)
2742 class = xs_list_append(class, "snac-reacted");
2743
2744 xs *l1 = xs_join(class, " ");
2745 ret = xs_html_tag("button",
2746 xs_html_attr("type", "submit"),
2747 xs_html_attr("name", "action"),
2748 xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
2749 xs_html_raw(emoji),
2750 xs_html_tag("span",
2751 xs_html_raw(nb),
2752 xs_html_attr("style", "padding-left: 5px;")),
2753 xs_html_attr("title", act),
2754 xs_html_attr("class", l1));
2755
2756 if (!(ide && xs_startswith(ide, srv_baseurl)))
2757 xs_html_add(ret, xs_html_attr("disabled", "true"));
2758 }
2759 else if (shortname) {
2760 xs *sn = xs_dup(shortname);
2761 const char *sna = sn;
2762 unsigned int utf = xs_utf8_dec((const char **)&sna);
2763
2764 if (xs_is_emoji(utf)) {
2765 const char *style = "font-size: large;";
2766 if (me)
2767 class = xs_list_append(class, "snac-reacted");
2768 xs *l1 = xs_join(class, " ");
2769 xs *s1 = xs_fmt("&#%d", utf);
2770 ret = xs_html_tag("button",
2771 xs_html_attr("type", "submit"),
2772 xs_html_attr("name", "action"),
2773 xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
2774 xs_html_raw(s1),
2775 xs_html_tag("span",
2776 xs_html_raw(nb),
2777 xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
2778 xs_html_attr("title", act),
2779 xs_html_attr("class", l1),
2780 xs_html_attr("style", style));
2781 }
2782 }
2783 if (ret) {
2784 xs *s1;
2785 if (user) {
2786 xs *action = xs_fmt("%s/admin/action", user->actor);
2787 xs *form_id = xs_fmt("%s_reply_form", md5);
2788
2789 xs_html *form =
2790 xs_html_tag("form",
2791 xs_html_attr("autocomplete", "off"),
2792 xs_html_attr("method", "post"),
2793 xs_html_attr("action", action),
2794 xs_html_attr("enctype", "multipart/form-data"),
2795 xs_html_attr("style", "display: inline-flex;"
2796 "vertical-align: middle;"),
2797 xs_html_attr("id", form_id),
2798 xs_html_sctag("input",
2799 xs_html_attr("type", "hidden"),
2800 xs_html_attr("name", "id"),
2801 xs_html_attr("value", id)),
2802 xs_html_sctag("input",
2803 xs_html_attr("type", "hidden"),
2804 xs_html_attr("name", "eid"),
2805 xs_html_attr("value", shortname)),
2806 ret);
2807 s1 = xs_html_render(form);
2808 }
2809 else
2810 s1 = xs_html_render(ret);
2811
2812 ls = xs_list_append(ls, s1);
2813 sfrl = xs_dict_del(sfrl, content);
2814 }
2815 }
2816 }
2817 }
2818
2819 c = 0;
2820
2821 xs_html *emoji_div;
2822 if (xs_list_len(ls) > 0) {
2823 emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
2824 xs_html_attr("class", "snac-reaction-div"));
2825
2826 while (ls != NULL && xs_list_next(ls, &k, &c))
2827 xs_html_add(emoji_div, xs_html_raw(k));
2828
2829 xs_html_add(snac_content_wrap, emoji_div);
2830 }
2831
2832 }
2833
2834 {
2835 /** build the content string **/
2836 const char *content = xs_dict_get(msg, "content");
2837
2838 if (xs_type(content) != XSTYPE_STRING) {
2839 if (!xs_is_null(content))
2840 srv_archive_error("unexpected_content_xstype",
2841 "content field type", xs_stock(XSTYPE_DICT), msg);
2842
2843 content = "";
2844 }
2845
2846 /* skip ugly line breaks at the beginning */
2847 while (xs_startswith(content, "<br>"))
2848 content += 4;
2849
2850 xs *c = sanitize(content);
2851
2852 /* do some tweaks to the content */
2853 c = xs_replace_i(c, "\r", "");
2854
2855 while (xs_endswith(c, "<br><br>"))
2856 c = xs_crop_i(c, 0, -4);
2857
2858 c = xs_replace_i(c, "<br><br>", "<p>");
2859
2860 if (is_emoji == 0)
2861 c = xs_str_cat(c, "<p>");
2862
2863 /* replace the :shortnames: */
2864 c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
2865
2866 /* Peertube videos content is in markdown */
2867 const char *mtype = xs_dict_get(msg, "mediaType");
2868 if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
2869 /* a full conversion could be better */
2870 c = xs_replace_i(c, "\r", "");
2871 c = xs_replace_i(c, "\n", "<br>");
2872 }
2873
2874 /* c contains sanitized HTML */
2875 xs_html_add(snac_content,
2876 xs_html_raw(c));
2877
2878 /* quoted post */
2879 const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
2880
2881 if (level < 3 && xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
2882 xs *quoted_post = NULL;
2883
2884 if (valid_status(object_get(quoted_id, "ed_post))) {
2885 xs *md5 = xs_md5_hex(quoted_id, strlen(quoted_id));
2886
2887 xs_html_add(snac_content,
2888 xs_html_tag("blockquote",
2889 xs_html_attr("class", "snac-quoted-post"),
2890 html_entry(user, quoted_post, 1, level + 1, md5, 1, tb_friendly)));
2891 }
2892 else
2893 if (user) {
2894 snac_log(user, xs_fmt("deferred request of quoted post %s", quoted_id));
2895 enqueue_object_request(user, quoted_id, 0);
2896 }
2897 }
2898 }
2899
2900 if (strcmp(type, "Question") == 0) { /** question content **/
2901 const xs_list *oo = xs_dict_get(msg, "oneOf");
2902 const xs_list *ao = xs_dict_get(msg, "anyOf");
2903 const xs_list *p;
2904 const xs_dict *v;
2905 int closed = 0;
2906 const char *f_closed = NULL;
2907
2908 xs_html *poll = xs_html_tag("div", NULL);
2909
2910 if (read_only)
2911 closed = 1; /* non-identified page; show as closed */
2912 else
2913 if (user && is_msg_mine(user, id))
2914 closed = 1; /* we questioned; closed for us */
2915 else
2916 if (user && was_question_voted(user, id))
2917 closed = 1; /* we already voted; closed for us */
2918
2919 if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
2920 /* it has a closed date... but is it in the past? */
2921 time_t t0 = time(NULL);
2922 time_t t1 = xs_parse_iso_date(f_closed, 0);
2923
2924 if (t1 < t0)
2925 closed = 2;
2926 }
2927
2928 /* get the appropriate list of options */
2929 p = oo != NULL ? oo : ao;
2930
2931 if (closed || user == NULL) {
2932 /* closed poll */
2933 xs_html *poll_result = xs_html_tag("table",
2934 xs_html_attr("class", "snac-poll-result"));
2935 int c = 0;
2936
2937 while (xs_list_next(p, &v, &c)) {
2938 const char *name = xs_dict_get(v, "name");
2939 const xs_dict *replies = xs_dict_get(v, "replies");
2940
2941 if (xs_is_string(name) && xs_is_dict(replies)) {
2942 const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
2943
2944 if (xs_is_string(ti))
2945 xs_html_add(poll_result,
2946 xs_html_tag("tr",
2947 xs_html_tag("td",
2948 xs_html_text(name),
2949 xs_html_text(":")),
2950 xs_html_tag("td",
2951 xs_html_text(ti))));
2952 }
2953 }
2954
2955 xs_html_add(poll,
2956 poll_result);
2957 }
2958 else {
2959 /* poll still active */
2960 xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
2961 xs_html *form;
2962 xs_html *poll_form = xs_html_tag("div",
2963 xs_html_attr("class", "snac-poll-form"),
2964 form = xs_html_tag("form",
2965 xs_html_attr("autocomplete", "off"),
2966 xs_html_attr("method", "post"),
2967 xs_html_attr("action", vote_action),
2968 xs_html_sctag("input",
2969 xs_html_attr("type", "hidden"),
2970 xs_html_attr("name", "actor"),
2971 xs_html_attr("value", actor)),
2972 xs_html_sctag("input",
2973 xs_html_attr("type", "hidden"),
2974 xs_html_attr("name", "irt"),
2975 xs_html_attr("value", id))));
2976
2977 int c = 0;
2978 while (xs_list_next(p, &v, &c)) {
2979 const char *name = xs_dict_get(v, "name");
2980 const xs_dict *replies = xs_dict_get(v, "replies");
2981
2982 if (name) {
2983 char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
2984
2985 xs_html *btn = xs_html_sctag("input",
2986 xs_html_attr("id", name),
2987 xs_html_attr("value", name),
2988 xs_html_attr("name", "question"));
2989
2990 if (!xs_is_null(oo)) {
2991 xs_html_add(btn,
2992 xs_html_attr("type", "radio"),
2993 xs_html_attr("required", "required"));
2994 }
2995 else
2996 xs_html_add(btn,
2997 xs_html_attr("type", "checkbox"));
2998
2999 xs_html_add(form,
3000 btn,
3001 xs_html_text(" "),
3002 xs_html_tag("span",
3003 xs_html_attr("title", ti),
3004 xs_html_text(name)),
3005 xs_html_sctag("br", NULL));
3006 }
3007 }
3008
3009 xs_html_add(form,
3010 xs_html_tag("p", NULL),
3011 xs_html_sctag("input",
3012 xs_html_attr("type", "submit"),
3013 xs_html_attr("class", "button"),
3014 xs_html_attr("value", L("Vote"))));
3015
3016 xs_html_add(poll,
3017 poll_form);
3018 }
3019
3020 /* if it's *really* closed, say it */
3021 if (closed == 2) {
3022 xs_html_add(poll,
3023 xs_html_tag("p",
3024 xs_html_text(L("Closed"))));
3025 }
3026 else {
3027 /* show when the poll closes */
3028 const char *end_time = xs_dict_get(msg, "endTime");
3029
3030 /* Pleroma does not have an endTime field;
3031 it has a closed time in the future */
3032 if (xs_is_null(end_time))
3033 end_time = xs_dict_get(msg, "closed");
3034
3035 if (!xs_is_null(end_time)) {
3036 time_t t0 = time(NULL);
3037 time_t t1 = xs_parse_iso_date(end_time, 0);
3038
3039 if (t1 > 0 && t1 > t0) {
3040 time_t diff_time = t1 - t0;
3041 xs *tf = xs_str_time_diff(diff_time);
3042 char *p = tf;
3043
3044 /* skip leading zeros */
3045 for (; *p == '0' || *p == ':'; p++);
3046
3047 xs_html_add(poll,
3048 xs_html_tag("p",
3049 xs_html_text(L("Closes in")),
3050 xs_html_text(" "),
3051 xs_html_text(p)));
3052 }
3053 }
3054 }
3055
3056 xs_html_add(snac_content,
3057 poll);
3058 }
3059
3060 /** attachments **/
3061 xs *attach = get_attachments(msg);
3062
3063 {
3064 /* make custom css for attachments easier */
3065 xs_html *content_attachments = xs_html_tag("div",
3066 xs_html_attr("class", "snac-content-attachments"));
3067
3068 xs_html_add(snac_content,
3069 content_attachments);
3070
3071 const char *content = xs_dict_get(msg, "content");
3072
3073 int c = 0;
3074 const xs_dict *a;
3075 while (xs_list_next(attach, &a, &c)) {
3076 const char *type = xs_dict_get(a, "type");
3077 const char *o_href = xs_dict_get(a, "href");
3078 const char *name = xs_dict_get(a, "name");
3079
3080 if (!xs_is_string(type) || !xs_is_string(o_href))
3081 continue;
3082
3083 /* if this URL is already in the post content, skip */
3084 if (content && xs_str_in(content, o_href) != -1)
3085 continue;
3086
3087 if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
3088 continue;
3089
3090 /* do this attachment include an icon? */
3091 const xs_dict *icon = xs_dict_get(a, "icon");
3092 if (xs_type(icon) == XSTYPE_DICT) {
3093 const char *icon_mtype = xs_dict_get(icon, "mediaType");
3094 const char *icon_url = xs_dict_get(icon, "url");
3095
3096 if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
3097 xs_html_add(content_attachments,
3098 xs_html_tag("a",
3099 xs_html_attr("href", icon_url),
3100 xs_html_attr("target", "_blank"),
3101 xs_html_sctag("img",
3102 xs_html_attr("loading", "lazy"),
3103 xs_html_attr("src", icon_url))));
3104 }
3105 }
3106
3107 xs *href = make_url(o_href, proxy, 0);
3108
3109 if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
3110 xs_html_add(content_attachments,
3111 xs_html_tag("a",
3112 xs_html_attr("href", href),
3113 xs_html_attr("target", "_blank"),
3114 xs_html_sctag("img",
3115 xs_html_attr("loading", "lazy"),
3116 xs_html_attr("src", href),
3117 xs_html_attr("alt", name),
3118 xs_html_attr("title", name))));
3119 }
3120 else
3121 if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
3122 xs_html_add(content_attachments,
3123 xs_html_tag("video",
3124 xs_html_attr("preload", "none"),
3125 xs_html_attr("style", "width: 100%"),
3126 xs_html_attr("class", "snac-embedded-video"),
3127 xs_html_attr("controls", NULL),
3128 xs_html_attr("src", href),
3129 xs_html_text(L("Video")),
3130 xs_html_text(": "),
3131 xs_html_tag("a",
3132 xs_html_attr("href", href),
3133 xs_html_text(name))));
3134 }
3135 else
3136 if (xs_startswith(type, "audio/")) {
3137 xs_html_add(content_attachments,
3138 xs_html_tag("audio",
3139 xs_html_attr("preload", "none"),
3140 xs_html_attr("style", "width: 100%"),
3141 xs_html_attr("class", "snac-embedded-audio"),
3142 xs_html_attr("controls", NULL),
3143 xs_html_attr("src", href),
3144 xs_html_text(L("Audio")),
3145 xs_html_text(": "),
3146 xs_html_tag("a",
3147 xs_html_attr("href", href),
3148 xs_html_text(name))));
3149 }
3150 else
3151 if (strcmp(type, "Link") == 0) {
3152 xs_html_add(content_attachments,
3153 xs_html_tag("p",
3154 xs_html_tag("a",
3155 xs_html_attr("href", o_href),
3156 xs_html_text(o_href))));
3157
3158 /* do not generate an Alt... */
3159 name = NULL;
3160 }
3161 else {
3162 xs *d_href = xs_dup(o_href);
3163 if (strlen(d_href) > 64) {
3164 d_href[64] = '\0';
3165 d_href = xs_str_cat(d_href, "...");
3166 }
3167
3168 xs_html_add(content_attachments,
3169 xs_html_tag("p",
3170 xs_html_tag("a",
3171 xs_html_attr("href", o_href),
3172 xs_html_text(L("Attachment")),
3173 xs_html_text(": "),
3174 xs_html_text(d_href))));
3175
3176 /* do not generate an Alt... */
3177 name = NULL;
3178 }
3179
3180 if (name != NULL && *name) {
3181 xs_html_add(content_attachments,
3182 xs_html_tag("p",
3183 xs_html_attr("class", "snac-alt-text"),
3184 xs_html_tag("details",
3185 xs_html_tag("summary",
3186 xs_html_text(L("Alt..."))),
3187 xs_html_text(name))));
3188 }
3189 }
3190 }
3191
3192 /* has this message an audience (i.e., comes from a channel or community)? */
3193 const char *audience = xs_dict_get(msg, "audience");
3194 if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
3195 xs_html *au_tag = xs_html_tag("p",
3196 xs_html_text("("),
3197 xs_html_tag("a",
3198 xs_html_attr("href", audience),
3199 xs_html_attr("title", L("Source channel or community")),
3200 xs_html_text(audience)),
3201 xs_html_text(")"));
3202
3203 xs_html_add(snac_content_wrap,
3204 au_tag);
3205 }
3206
3207 /* does it have a location? */
3208 const xs_dict *location = xs_dict_get(msg, "location");
3209 if (xs_type(location) == XSTYPE_DICT) {
3210 const xs_number *latitude = xs_dict_get(location, "latitude");
3211 const xs_number *longitude = xs_dict_get(location, "longitude");
3212 const char *name = xs_dict_get(location, "name");
3213 const char *address = xs_dict_get(location, "address");
3214 xs *label_list = xs_list_new();
3215
3216 if (xs_type(name) == XSTYPE_STRING)
3217 label_list = xs_list_append(label_list, name);
3218 if (xs_type(address) == XSTYPE_STRING)
3219 label_list = xs_list_append(label_list, address);
3220
3221 if (xs_list_len(label_list)) {
3222 const char *url = xs_dict_get(location, "url");
3223 xs *label = xs_join(label_list, ", ");
3224
3225 if (xs_type(url) == XSTYPE_STRING) {
3226 xs_html_add(snac_content_wrap,
3227 xs_html_tag("p",
3228 xs_html_text(L("Location: ")),
3229 xs_html_tag("a",
3230 xs_html_attr("href", url),
3231 xs_html_attr("target", "_blank"),
3232 xs_html_text(label))));
3233 }
3234 else
3235 if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
3236 xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
3237 xs_or(xs_number_str(latitude), latitude), xs_or(xs_number_str(longitude), longitude));
3238
3239 xs_html_add(snac_content_wrap,
3240 xs_html_tag("p",
3241 xs_html_text(L("Location: ")),
3242 xs_html_tag("a",
3243 xs_html_attr("href", url),
3244 xs_html_attr("target", "_blank"),
3245 xs_html_text(label))));
3246 }
3247 else
3248 xs_html_add(snac_content_wrap,
3249 xs_html_tag("p",
3250 xs_html_text(L("Location: ")),
3251 xs_html_text(label)));
3252 }
3253 }
3254
3255 if (strcmp(type, "Event") == 0) { /** Event start and end times **/
3256 const char *s_time = xs_dict_get(msg, "startTime");
3257
3258 if (xs_is_string(s_time) && strlen(s_time) > 20) {
3259 const char *e_time = xs_dict_get(msg, "endTime");
3260 const char *tz = xs_dict_get(msg, "timezone");
3261
3262 xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
3263 xs *e = NULL;
3264
3265 if (xs_is_string(e_time) && strlen(e_time) > 20)
3266 e = xs_replace_i(xs_dup(e_time), "T", " ");
3267
3268 /* if the event has a timezone, crop the offsets */
3269 if (xs_is_string(tz)) {
3270 s = xs_crop_i(s, 0, 19);
3271
3272 if (e)
3273 e = xs_crop_i(e, 0, 19);
3274 }
3275 else
3276 tz = "";
3277
3278 /* if start and end share the same day, crop it from the end */
3279 if (e && memcmp(s, e, 11) == 0)
3280 e = xs_crop_i(e, 11, 0);
3281
3282 if (e)
3283 s = xs_str_cat(s, " / ", e);
3284
3285 if (*tz)
3286 s = xs_str_cat(s, " (", tz, ")");
3287
3288 /* replace ugly decimals */
3289 s = xs_replace_i(s, ".000", "");
3290
3291 xs_html_add(snac_content_wrap,
3292 xs_html_tag("p",
3293 xs_html_text(L("Time: ")),
3294 xs_html_text(s)));
3295 }
3296 }
3297
3298 /* show all hashtags that has not been shown previously in the content */
3299 const xs_list *tags = xs_dict_get(msg, "tag");
3300 const char *o_content = xs_dict_get_def(msg, "content", "");
3301
3302 if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
3303 xs *content = xs_utf8_tolower(o_content);
3304 const xs_dict *tag;
3305
3306 xs_html *add_hashtags = xs_html_tag("ul",
3307 xs_html_attr("class", "snac-more-hashtags"));
3308
3309 xs_list_foreach(tags, tag) {
3310 const char *type = xs_dict_get(tag, "type");
3311
3312 if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
3313 const char *o_href = xs_dict_get(tag, "href");
3314 const char *name = xs_dict_get(tag, "name");
3315
3316 if (xs_is_string(o_href) && xs_is_string(name)) {
3317 xs *href = xs_utf8_tolower(o_href);
3318
3319 if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
3320 /* not in the content: add here */
3321 xs_html_add(add_hashtags,
3322 xs_html_tag("li",
3323 xs_html_tag("a",
3324 xs_html_attr("href", href),
3325 xs_html_text(name),
3326 xs_html_text(" "))));
3327 }
3328 }
3329 }
3330 }
3331
3332 xs_html_add(snac_content_wrap,
3333 add_hashtags);
3334 }
3335
3336 /** controls **/
3337
3338 if (!read_only && user) {
3339 xs_html_add(entry,
3340 html_entry_controls(user, actor, msg, md5, tb_friendly));
3341 }
3342
3343 /** children **/
3344 if (!hide_children) {
3345 xs *children = object_children(id);
3346 int left = xs_list_len(children);
3347
3348 if (left) {
3349 xs_html *ch_details = xs_html_tag("details",
3350 xs_html_attr(collapse_threads ? "" : "open", NULL),
3351 xs_html_tag("summary",
3352 xs_html_text("...")));
3353
3354 xs_html_add(entry,
3355 ch_details);
3356
3357 xs_html *fch_container = xs_html_tag("div",
3358 xs_html_attr("class", "snac-thread-cont"));
3359
3360 xs_html_add(ch_details,
3361 fch_container);
3362
3363 xs_html *ch_container = xs_html_tag("div",
3364 xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
3365
3366 xs_html_add(ch_details,
3367 ch_container);
3368
3369 xs_html *ch_older = NULL;
3370 if (left > 3) {
3371 xs_html_add(ch_container,
3372 ch_older = xs_html_tag("details",
3373 xs_html_tag("summary",
3374 xs_html_text(L("Older...")))));
3375 }
3376
3377 int ctxt = 0;
3378 const char *cmd5;
3379 int cnt = 0;
3380 int o_cnt = 0;
3381 int f_cnt = 0;
3382
3383 /* get the first child */
3384 xs_list_next(children, &cmd5, &ctxt);
3385 xs *f_chd = NULL;
3386
3387 object_get_by_md5(cmd5, &f_chd);
3388
3389 if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
3390 const char *p_author = get_atto(msg);
3391 const char *author = get_atto(f_chd);
3392
3393 /* is the first child from the same author? */
3394 if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
3395 /* then, don't add it to the children container,
3396 so that it appears unindented just before the parent
3397 like a fucking Twitter-like thread */
3398 xs_html_add(fch_container,
3399 html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children, tb_friendly));
3400
3401 cnt++;
3402 f_cnt++;
3403 left--;
3404 }
3405 else
3406 ctxt = 0; /* restart from the beginning */
3407 }
3408
3409 while (xs_list_next(children, &cmd5, &ctxt)) {
3410 xs *chd = NULL;
3411
3412 object_get_by_md5(cmd5, &chd);
3413
3414 if (chd != NULL) {
3415 if (xs_is_null(xs_dict_get(chd, "name"))) {
3416 xs_html *che = html_entry(user, chd, read_only,
3417 level + 1, cmd5, hide_children, tb_friendly);
3418
3419 if (che != NULL) {
3420 if (left > 3) {
3421 xs_html_add(ch_older,
3422 che);
3423
3424 o_cnt++;
3425 }
3426 else
3427 xs_html_add(ch_container,
3428 che);
3429
3430 cnt++;
3431 }
3432 }
3433
3434 left--;
3435 }
3436 else
3437 srv_debug(2, xs_fmt("cannot read child %s", cmd5));
3438 }
3439
3440 /* if no children were finally added, hide the details */
3441 if (cnt == 0)
3442 xs_html_add(ch_details,
3443 xs_html_attr("style", "display: none"));
3444
3445 if (o_cnt == 0 && ch_older)
3446 xs_html_add(ch_older,
3447 xs_html_attr("style", "display: none"));
3448
3449 if (f_cnt == 0)
3450 xs_html_add(fch_container,
3451 xs_html_attr("style", "display: none"));
3452 }
3453 }
3454
3455 /* add an invisible hr, to help differentiate between posts in text browsers */
3456 xs_html_add(entry_top,
3457 xs_html_sctag("hr",
3458 xs_html_attr("hidden", NULL)));
3459
3460 return entry_top;
3461}
3462
3463
3464xs_html *html_footer(const snac *user)
3465{
3466 return xs_html_tag("div",
3467 xs_html_attr("class", "snac-footer"),
3468 xs_html_tag("a",
3469 xs_html_attr("href", srv_baseurl),
3470 xs_html_text(L("about this site"))),
3471 xs_html_text(" - "),
3472 xs_html_text(L("powered by ")),
3473 xs_html_tag("a",
3474 xs_html_attr("href", WHAT_IS_SNAC_URL),
3475 xs_html_tag("abbr",
3476 xs_html_attr("title", "Social Networks Are Crap"),
3477 xs_html_text(USER_AGENT))));
3478}
3479
3480
3481xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3482 int skip, int show, int show_more,
3483 const char *title, const char *page,
3484 int utl, const char *error, int terse, int tb_friendly)
3485/* returns the HTML for the timeline */
3486{
3487 const char *v;
3488 double t = ftime();
3489 int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
3490
3491 xs *desc = NULL;
3492 xs *alternate = NULL;
3493
3494 if (xs_list_len(list) == 1) {
3495 /* only one element? pick the description from the source */
3496 const char *id = xs_list_get(list, 0);
3497 xs *d = NULL;
3498 object_get_by_md5(id, &d);
3499 const char *sc = xs_dict_get(d, "sourceContent");
3500 if (d && sc != NULL)
3501 desc = xs_dup(sc);
3502
3503 alternate = xs_dup(xs_dict_get(d, "id"));
3504 }
3505
3506 xs_html *head;
3507 xs_html *body;
3508
3509 if (user) {
3510 head = html_user_head(user, desc, alternate);
3511
3512 if (terse)
3513 body = xs_html_tag("body", NULL);
3514 else
3515 body = html_user_body(user, read_only);
3516 }
3517 else {
3518 head = html_instance_head();
3519 body = html_instance_body();
3520 }
3521
3522 xs_html *html = xs_html_tag("html",
3523 head,
3524 body);
3525
3526 if (user && !read_only)
3527 xs_html_add(body,
3528 html_top_controls(user, tb_friendly));
3529
3530 if (error != NULL) {
3531 xs_html_add(body,
3532 xs_html_tag("dialog",
3533 xs_html_attr("open", NULL),
3534 xs_html_tag("p",
3535 xs_html_text(error)),
3536 xs_html_tag("form",
3537 xs_html_attr("method", "dialog"),
3538 xs_html_sctag("input",
3539 xs_html_attr("type", "submit"),
3540 xs_html_attr("value", L("Dismiss"))))));
3541 }
3542
3543 /* show links to the available lists */
3544 if (user && !read_only) {
3545 xs_html *lol = xs_html_tag("ul",
3546 xs_html_attr("class", "snac-list-of-lists"));
3547 xs_html_add(body, lol);
3548
3549 xs *lists = list_maint(user, NULL, OP_LIST); /* get list of lists */
3550
3551 const char *v;
3552
3553 xs_list_foreach(lists, v) {
3554 const char *lname = xs_list_get(v, 1);
3555 xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
3556 xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
3557
3558 xs_html_add(lol,
3559 xs_html_tag("li",
3560 xs_html_tag("a",
3561 xs_html_attr("href", url),
3562 xs_html_attr("class", "snac-list-link"),
3563 xs_html_attr("title", ttl),
3564 xs_html_text(lname))));
3565 }
3566
3567 {
3568 /* show the list of pinned posts */
3569 xs *url = xs_fmt("%s/pinned", user->actor);
3570 xs_html_add(lol,
3571 xs_html_tag("li",
3572 xs_html_tag("a",
3573 xs_html_attr("href", url),
3574 xs_html_attr("class", "snac-list-link"),
3575 xs_html_attr("title", L("Pinned posts")),
3576 xs_html_text(L("pinned")))));
3577 }
3578
3579 {
3580 /* show the list of bookmarked posts */
3581 xs *url = xs_fmt("%s/admirations", user->actor);
3582 xs_html_add(lol,
3583 xs_html_tag("li",
3584 xs_html_tag("a",
3585 xs_html_attr("href", url),
3586 xs_html_attr("class", "snac-list-link"),
3587 xs_html_attr("title", L("Liked, boosted or reacted posts")),
3588 xs_html_text(L("admirations")))));
3589 }
3590
3591 {
3592 /* show the list of bookmarked posts */
3593 xs *url = xs_fmt("%s/bookmarks", user->actor);
3594 xs_html_add(lol,
3595 xs_html_tag("li",
3596 xs_html_tag("a",
3597 xs_html_attr("href", url),
3598 xs_html_attr("class", "snac-list-link"),
3599 xs_html_attr("title", L("Bookmarked posts")),
3600 xs_html_text(L("bookmarks")))));
3601 }
3602
3603 {
3604 /* show the list of drafts */
3605 xs *url = xs_fmt("%s/drafts", user->actor);
3606 xs_html_add(lol,
3607 xs_html_tag("li",
3608 xs_html_tag("a",
3609 xs_html_attr("href", url),
3610 xs_html_attr("class", "snac-list-link"),
3611 xs_html_attr("title", L("Post drafts")),
3612 xs_html_text(L("drafts")))));
3613 }
3614
3615 {
3616 /* show the list of scheduled posts */
3617 xs *url = xs_fmt("%s/sched", user->actor);
3618 xs_html_add(lol,
3619 xs_html_tag("li",
3620 xs_html_tag("a",
3621 xs_html_attr("href", url),
3622 xs_html_attr("class", "snac-list-link"),
3623 xs_html_attr("title", L("Scheduled posts")),
3624 xs_html_text(L("scheduled posts")))));
3625 }
3626
3627 /* the list of followed hashtags */
3628 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
3629
3630 if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
3631 xs_html *loht = xs_html_tag("ul",
3632 xs_html_attr("class", "snac-list-of-lists"));
3633 xs_html_add(body, loht);
3634
3635 const char *ht;
3636
3637 xs_list_foreach(followed_hashtags, ht) {
3638 xs *url = NULL;
3639
3640 if (!xs_startswith(ht, "https:/""/"))
3641 url = xs_fmt("%s/admin?q=%s", user->actor, ht);
3642 else
3643 url = xs_dup(ht);
3644
3645 url = xs_replace_i(url, "#", "%23");
3646
3647 xs_html_add(loht,
3648 xs_html_tag("li",
3649 xs_html_tag("a",
3650 xs_html_attr("href", url),
3651 xs_html_attr("class", "snac-list-link"),
3652 xs_html_text(ht))));
3653 }
3654 }
3655 }
3656
3657 xs_html_add(body,
3658 xs_html_tag("a",
3659 xs_html_attr("name", "snac-posts")));
3660
3661 xs_html *posts = xs_html_tag("div",
3662 xs_html_attr("class", "snac-posts"));
3663
3664 if (title) {
3665 xs_html_add(posts,
3666 xs_html_tag("h2",
3667 xs_html_attr("class", "snac-header"),
3668 xs_html_text(title)));
3669 }
3670
3671 xs_html_add(body, posts);
3672
3673 int mark_shown = 0;
3674
3675 int show_unlisted = user ? xs_is_true(xs_dict_get(user->config, "show_unlisted")) : 0;
3676
3677 /* disable text-browser friendliness if it's just one post,
3678 to force the details/summary controls to appear */
3679 if (xs_list_len(list) == 1)
3680 tb_friendly = 0;
3681
3682 xs_list_foreach(list, v) {
3683 xs *msg = NULL;
3684 int status;
3685
3686 /* "already seen" mark? */
3687 if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
3688 if (skip == 0 && !mark_shown) {
3689 xs *s = xs_fmt("%s/admin", user->actor);
3690
3691 xs_html_add(posts,
3692 xs_html_tag("div",
3693 xs_html_attr("class", "snac-no-more-unseen-posts"),
3694 xs_html_text(L("No more unseen posts")),
3695 xs_html_text(" - "),
3696 xs_html_tag("a",
3697 xs_html_attr("href", s),
3698 xs_html_text(L("Back to top")))));
3699 }
3700
3701 mark_shown = 1;
3702
3703 continue;
3704 }
3705
3706 if (utl && user && !is_pinned_by_md5(user, v))
3707 status = timeline_get_by_md5(user, v, &msg);
3708 else
3709 status = object_get_by_md5(v, &msg);
3710
3711 if (!valid_status(status))
3712 continue;
3713
3714 /* if it's an instance page, discard messages from private users */
3715 if (user == NULL && is_msg_from_private_user(msg))
3716 continue;
3717
3718 const int scope = get_msg_visibility(msg);
3719 if (user != NULL && scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED && !is_msg_mine(user, xs_dict_get(msg, "id"))) {
3720 /* is this message a non-public reply? */
3721 const char *irt = get_in_reply_to(msg);
3722
3723 /* is it a reply to something not in the storage? */
3724 if (!xs_is_null(irt) && !object_here(irt)) {
3725 /* is it for me? */
3726 const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
3727 const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
3728
3729 if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
3730 snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
3731 continue;
3732 }
3733 }
3734 }
3735 /* hide non-public posts from /instance view */
3736 if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC)
3737 continue;
3738
3739 /* hide non-public posts viewed from outside */
3740 if (read_only) {
3741 if (scope == SCOPE_MENTIONED || scope == SCOPE_FOLLOWERS)
3742 continue;
3743
3744 if (scope == SCOPE_UNLISTED && !show_unlisted)
3745 continue;
3746 }
3747
3748 xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1, tb_friendly);
3749
3750 if (entry != NULL)
3751 xs_html_add(posts,
3752 entry);
3753 }
3754
3755 if (list && user && read_only) {
3756 /** history **/
3757 if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
3758 xs_html *ul = xs_html_tag("ul", NULL);
3759
3760 xs_html *history = xs_html_tag("div",
3761 xs_html_attr("class", "snac-history"),
3762 xs_html_tag("p",
3763 xs_html_attr("class", "snac-history-title"),
3764 xs_html_text(L("History"))),
3765 ul);
3766
3767 xs *list = history_list(user);
3768 xs_list *p = list;
3769 const char *v;
3770
3771 while (xs_list_iter(&p, &v)) {
3772 xs *fn = xs_replace(v, ".html", "");
3773 xs *url = xs_fmt("%s/h/%s", user->actor, v);
3774
3775 xs_html_add(ul,
3776 xs_html_tag("li",
3777 xs_html_tag("a",
3778 xs_html_attr("href", url),
3779 xs_html_text(fn))));
3780 }
3781
3782 xs_html_add(body,
3783 history);
3784 }
3785 }
3786
3787 {
3788 xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
3789 xs_html_add(body,
3790 xs_html_raw(s1));
3791 }
3792
3793 if (show_more) {
3794 xs *m = NULL;
3795 xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3796
3797 xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3798
3799 if (page != NULL)
3800 url = xs_str_cat(url, page);
3801
3802 if (xs_str_in(url, "?") != -1)
3803 m = xs_fmt("%s&%s", url, ss);
3804 else
3805 m = xs_fmt("%s?%s", url, ss);
3806
3807 xs_html *more_links = xs_html_tag("p",
3808 xs_html_tag("a",
3809 xs_html_attr("href", url),
3810 xs_html_attr("name", "snac-more"),
3811 xs_html_text(L("Back to top"))),
3812 xs_html_text(" - "),
3813 xs_html_tag("a",
3814 xs_html_attr("href", m),
3815 xs_html_attr("name", "snac-more"),
3816 xs_html_text(L("More..."))));
3817
3818 xs_html_add(body,
3819 more_links);
3820 }
3821
3822 xs_html_add(body,
3823 html_footer(user));
3824
3825 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3826}
3827
3828
3829xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int count)
3830{
3831 xs_html *snac_posts;
3832 xs *header_cnt;
3833 if (count > -1)
3834 header_cnt = xs_fmt("%s - %d\n", header, count);
3835 else
3836 header_cnt = xs_fmt("%s\n", header);
3837
3838 xs_html *people = xs_html_tag("div",
3839 xs_html_tag("h2",
3840 xs_html_attr("class", "snac-header"),
3841 xs_html_text(header_cnt)),
3842 snac_posts = xs_html_tag("details",
3843 xs_html_attr("open", NULL),
3844 xs_html_tag("summary",
3845 xs_html_text("..."))));
3846
3847 xs *redir = xs_fmt("%s/people", user->actor);
3848
3849 const char *actor_id;
3850
3851 xs_list_foreach(list, actor_id) {
3852 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
3853 xs *actor = NULL;
3854
3855 if (valid_status(actor_get(actor_id, &actor))) {
3856 xs_html *snac_post = xs_html_tag("div",
3857 xs_html_attr("class", "snac-post"),
3858 xs_html_tag("a",
3859 xs_html_attr("name", md5)),
3860 xs_html_tag("div",
3861 xs_html_attr("class", "snac-post-header"),
3862 html_actor_icon(user, actor, xs_dict_get(actor, "published"),
3863 NULL, NULL, -1, 0, proxy, NULL, NULL)));
3864
3865 /* content (user bio) */
3866 const char *c = xs_dict_get(actor, "summary");
3867 const xs_val *tag = xs_dict_get(actor, "tag");
3868
3869 if (!xs_is_null(c)) {
3870 xs *sc = sanitize(c);
3871 sc = replace_shortnames(sc, tag, 2, proxy);
3872
3873 xs_html *snac_content = xs_html_tag("div",
3874 xs_html_attr("class", "snac-content"));
3875
3876 if (xs_startswith(sc, "<p>"))
3877 xs_html_add(snac_content,
3878 xs_html_raw(sc)); /* already sanitized */
3879 else
3880 xs_html_add(snac_content,
3881 xs_html_tag("p",
3882 xs_html_raw(sc))); /* already sanitized */
3883
3884 xs_html_add(snac_post, snac_content);
3885 }
3886
3887 /* does it have account metrics in the object? */
3888 const xs_val *fwing = xs_dict_get(actor, "following_count");
3889 const xs_val *fwers = xs_dict_get(actor, "followers_count");
3890
3891 if (!xs_is_null(fwing) && !xs_is_null(fwers)) {
3892 int i_fwing = (int) xs_number_get_l(fwing);
3893 int i_fwers = (int) xs_number_get_l(fwers);
3894
3895 if (i_fwing || i_fwers) {
3896 xs *s = xs_fmt(L("%d following, %d followers"), i_fwing, i_fwers);
3897
3898 xs_html_add(snac_post,
3899 xs_html_tag("div",
3900 xs_html_tag("p",
3901 xs_html_text(s))));
3902 }
3903 }
3904
3905 /* does it have a location? */
3906 const xs_dict *location = xs_dict_get(actor, "location");
3907
3908 if (xs_is_dict(location)) {
3909 const xs_val *latitude = xs_dict_get(location, "latitude");
3910 const xs_val *longitude = xs_dict_get(location, "longitude");
3911
3912 if (latitude && longitude) {
3913 const char *name = xs_dict_get_def(location, "name", "Home");
3914 const char *la = xs_is_string(latitude) ? latitude : xs_number_str(latitude);
3915 const char *lo = xs_is_string(longitude) ? longitude : xs_number_str(longitude);
3916
3917 if (xs_is_string(la) && xs_is_string(lo)) {
3918 xs *sla = xs_utf8_crop_i(xs_dup(la), 0, 7);
3919 xs *slo = xs_utf8_crop_i(xs_dup(lo), 0, 7);
3920
3921 xs *label = xs_fmt("%s,%s", sla, slo);
3922 xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s", la, lo);
3923
3924 xs_html_add(snac_post,
3925 xs_html_tag("p",
3926 xs_html_text(name),
3927 xs_html_text(": "),
3928 xs_html_tag("a",
3929 xs_html_attr("href", url),
3930 xs_html_attr("target", "_blank"),
3931 xs_html_text(label))));
3932 }
3933 }
3934 }
3935
3936 /* add user metadata */
3937 xs_html *snac_metadata = xs_html_tag("div",
3938 xs_html_attr("class", "snac-metadata"));
3939
3940 int count = 0;
3941 const xs_val *address = xs_dict_get(actor, "vcard:Address");
3942 if (xs_is_string(address)) {
3943 xs_html_add(snac_metadata,
3944 xs_html_tag("span",
3945 xs_html_attr("class", "snac-property-name"),
3946 xs_html_raw("📍 Location")),
3947 xs_html_text(":"),
3948 xs_html_raw(" "),
3949 xs_html_tag("span",
3950 xs_html_attr("class", "snac-property-value p-adr"),
3951 xs_html_text(address)),
3952 xs_html_sctag("br", NULL));
3953
3954 count++;
3955 }
3956
3957 const xs_val *birthday = xs_dict_get(actor, "vcard:bday");
3958 if (xs_is_string(birthday)) {
3959 xs_html_add(snac_metadata,
3960 xs_html_tag("span",
3961 xs_html_attr("class", "snac-property-name"),
3962 xs_html_raw("🎂 Birthday")),
3963 xs_html_text(":"),
3964 xs_html_raw(" "),
3965 xs_html_tag("time",
3966 xs_html_attr("class", "snac-property-value dt-bday"),
3967 xs_html_text(birthday)),
3968 xs_html_sctag("br", NULL));
3969
3970 count++;
3971 }
3972
3973 const xs_list *attachment = xs_dict_get(actor, "attachment");
3974 if (count > 0 && xs_list_len(attachment) > 0) {
3975 xs_html_add(snac_metadata,
3976 xs_html_sctag("hr",
3977 xs_html_attr("class", "snac-property-divider")));
3978 }
3979
3980 const xs_val *v;
3981 xs_list_foreach(attachment, v) {
3982 const char *type = xs_dict_get(v, "type");
3983 const char *name = xs_dict_get(v, "name");
3984 const char *value = xs_dict_get(v, "value");
3985
3986 if (!xs_is_null(type) && !xs_is_null(name) &&
3987 !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
3988 /* both the name and the value can contain emoji */
3989 xs *nam = sanitize(name);
3990 nam = replace_shortnames(nam, tag, 1, proxy);
3991
3992 /* todo: sometimes the value is transmitted as markdown and not html ._. */
3993 xs *val = sanitize(value);
3994 val = replace_shortnames(val, tag, 1, proxy);
3995
3996 /* delete <p> tags, because some software sends them */
3997 val = xs_replace_i(val, "<p>", "");
3998 val = xs_replace_i(val, "</p>", "");
3999
4000 xs_html_add(snac_metadata,
4001 xs_html_tag("span",
4002 xs_html_attr("class", "snac-property-name"),
4003 xs_html_raw(nam)),
4004 xs_html_text(":"),
4005 xs_html_raw(" "),
4006 xs_html_tag("span",
4007 xs_html_attr("class", "snac-property-value"),
4008 xs_html_raw(val)),
4009 xs_html_sctag("br", NULL));
4010
4011 count++;
4012 }
4013 }
4014
4015 if (count > 0) {
4016 xs_html_add(snac_post, snac_metadata);
4017 }
4018 else {
4019 /* free the html, by rendering it... */
4020 xs_free(xs_html_render(snac_metadata));
4021 }
4022
4023 /* buttons */
4024 xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
4025
4026 xs_html *snac_controls = xs_html_tag("div",
4027 xs_html_attr("class", "snac-controls"));
4028
4029 xs_html *form = xs_html_tag("form",
4030 xs_html_attr("autocomplete", "off"),
4031 xs_html_attr("method", "post"),
4032 xs_html_attr("action", btn_form_action),
4033 xs_html_sctag("input",
4034 xs_html_attr("type", "hidden"),
4035 xs_html_attr("name", "actor"),
4036 xs_html_attr("value", actor_id)),
4037 xs_html_sctag("input",
4038 xs_html_attr("type", "hidden"),
4039 xs_html_attr("name", "hard-redir"),
4040 xs_html_attr("value", redir)),
4041 xs_html_sctag("input",
4042 xs_html_attr("type", "hidden"),
4043 xs_html_attr("name", "actor-form"),
4044 xs_html_attr("value", "yes")));
4045
4046 xs_html_add(snac_controls, form);
4047
4048 if (following_check(user, actor_id)) {
4049 xs_html_add(form,
4050 html_button("unfollow", L("Unfollow"),
4051 L("Stop following this user's activity")));
4052
4053 if (is_limited(user, actor_id))
4054 xs_html_add(form,
4055 html_button("unlimit", L("Unlimit"),
4056 L("Allow announces (boosts) from this user")));
4057 else
4058 xs_html_add(form,
4059 html_button("limit", L("Limit"),
4060 L("Block announces (boosts) from this user")));
4061 }
4062 else {
4063 xs_html_add(form,
4064 html_button("follow", L("Follow"),
4065 L("Start following this user's activity")));
4066
4067 if (follower_check(user, actor_id))
4068 xs_html_add(form,
4069 html_button("delete", L("Delete"), L("Delete this user")));
4070 }
4071
4072 if (cool(user, actor_id, OP_CHECK))
4073 xs_html_add(form,
4074 html_button("uncool", L("Unmark as cool"),
4075 L("No longer notify me whenever this user posts")));
4076 else
4077 xs_html_add(form,
4078 html_button("cool", L("Mark as cool"),
4079 L("Notify me whenever this user posts")));
4080
4081 if (pending_check(user, actor_id)) {
4082 xs_html_add(form,
4083 html_button("approve", L("Approve"),
4084 L("Approve this follow request")));
4085
4086 xs_html_add(form,
4087 html_button("discard", L("Discard"), L("Discard this follow request")));
4088 }
4089
4090 if (is_muted(user, actor_id))
4091 xs_html_add(form,
4092 html_button("unmute", L("Unmute"),
4093 L("Stop blocking activities from this user")));
4094 else
4095 xs_html_add(form,
4096 html_button("mute", L("MUTE"),
4097 L("Block any activity from this user")));
4098
4099 /* the post textarea */
4100 xs *dm_div_id = xs_fmt("%s_%s_dm", md5, t);
4101 xs *dm_form_id = xs_fmt("%s_reply_form", md5);
4102
4103 /* build a mention for the DM body: it seems that Mastodon,
4104 if a mention is not present, it does not recognize it as a DM,
4105 even if it has a one-item 'to' field with only the user */
4106 xs *dm_body = NULL;
4107
4108 {
4109 const char *webfinger = xs_dict_get(actor, "webfinger");
4110
4111 if (xs_is_string(webfinger))
4112 dm_body = xs_fmt("@%s ", webfinger);
4113 else {
4114 const char *uname = xs_dict_get_def(actor, "preferredUsername", "anonymous");
4115 xs *l = xs_split(actor_id, "/");
4116 dm_body = xs_fmt("@%s@%s ", uname, xs_list_get(l, 2));
4117 }
4118 }
4119
4120 xs_html_add(snac_controls,
4121 xs_html_tag("p", NULL),
4122 html_note(user, L("Direct Message..."),
4123 dm_div_id, dm_form_id,
4124 "", dm_body,
4125 NULL, actor_id,
4126 xs_stock(XSTYPE_FALSE), "",
4127 SCOPE_MENTIONED, NULL,
4128 NULL, 0, NULL, NULL, 0, NULL, NULL),
4129 xs_html_tag("p", NULL));
4130
4131 xs_html_add(snac_post, snac_controls);
4132
4133 xs_html_add(snac_posts, snac_post);
4134 }
4135 }
4136
4137 return people;
4138}
4139
4140
4141xs_str *html_people(snac *user)
4142{
4143 const char *proxy = NULL;
4144
4145 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4146 proxy = user->actor;
4147
4148 xs *wing = following_list(user);
4149 xs *wers = follower_list(user);
4150 xs *pending = pending_list(user);
4151
4152 xs_html *lists = xs_html_tag("div",
4153 xs_html_attr("class", "snac-posts"));
4154
4155 if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
4156 xs_html_add(lists,
4157 html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, xs_list_len(pending)));
4158 }
4159
4160 xs_html_add(lists,
4161 html_people_list(user, wing, L("People you follow"), "i", proxy, following_list_len(user)),
4162 html_people_list(user, wers, L("People that follow you"), "e", proxy, xs_list_len(wers)));
4163
4164 xs_html *html = xs_html_tag("html",
4165 html_user_head(user, NULL, NULL),
4166 xs_html_add(html_user_body(user, 0),
4167 lists,
4168 html_footer(user)));
4169
4170 return xs_html_render_s(html, "<!DOCTYPE html>\n");
4171}
4172
4173/* Filter list to display only posts by actor. We'll probably show
4174 fewer than show posts. Should we try harder to find some? */
4175xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
4176 int skip, int show, int show_more, const char *page)
4177{
4178 const char *proxy = NULL;
4179 xs_list *p = (xs_list *)list;
4180 const char *v;
4181
4182 enqueue_actor_refresh(user, actor, 0);
4183
4184 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4185 proxy = user->actor;
4186
4187 xs_html *body = html_user_body(user, 0);
4188
4189 xs_html *lists = xs_html_tag("div",
4190 xs_html_attr("class", "snac-posts"));
4191
4192 xs *foll = xs_list_append(xs_list_new(), actor);
4193
4194 xs_html_add(lists,
4195 html_people_list(user, foll, L("Contact's posts"), "p", proxy, -1));
4196
4197 xs_html_add(body, lists);
4198
4199 while (xs_list_iter(&p, &v)) {
4200 xs *msg = NULL;
4201 int status;
4202
4203 status = timeline_get_by_md5(user, v, &msg);
4204
4205 if (!valid_status(status))
4206 continue;
4207
4208 const char *id = xs_dict_get(msg, "id");
4209 const char *by = get_atto(msg);
4210 xs *actor_md5 = NULL;
4211 xs_list *boosts = NULL;
4212 xs_list *likes = NULL;
4213 xs_list *reacts = NULL;
4214 /* Besides actor's posts, also show actor's boosts, and also
4215 posts by user with likes or reacts by actor. I.e., any
4216 actor's actions that user could have seen in the timeline
4217 or in notifications. */
4218 if (!(by && strcmp(actor, by) == 0) &&
4219 xs_list_in((boosts = object_announces(id)),
4220 (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
4221 (!(by && strcmp(user->actor, by) == 0) ||
4222 (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
4223 xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
4224 continue;
4225
4226 xs_html *entry = html_entry(user, msg, 0, 0, v, 1, 0);
4227
4228 if (entry != NULL)
4229 xs_html_add(lists,
4230 entry);
4231 }
4232
4233 if (show_more) {
4234 xs *m = NULL;
4235 xs *m10 = NULL;
4236 xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
4237
4238 xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
4239
4240 if (page != NULL)
4241 url = xs_str_cat(url, page);
4242
4243 if (xs_str_in(url, "?") != -1)
4244 m = xs_fmt("%s&%s", url, ss);
4245 else
4246 m = xs_fmt("%s?%s", url, ss);
4247
4248 m10 = xs_fmt("%s0", m);
4249
4250 xs_html *more_links = xs_html_tag("p",
4251 xs_html_tag("a",
4252 xs_html_attr("href", url),
4253 xs_html_attr("name", "snac-more"),
4254 xs_html_text(L("Back to top"))),
4255 xs_html_text(" - "),
4256 xs_html_tag("a",
4257 xs_html_attr("href", m),
4258 xs_html_attr("name", "snac-more"),
4259 xs_html_text(L("More..."))),
4260 xs_html_text(" - "),
4261 xs_html_tag("a",
4262 xs_html_attr("href", m10),
4263 xs_html_attr("name", "snac-more"),
4264 xs_html_text(L("More (x 10)..."))));
4265
4266 xs_html_add(body,
4267 more_links);
4268 }
4269
4270 xs_html *html = xs_html_tag("html",
4271 html_user_head(user, NULL, NULL),
4272 xs_html_add(body,
4273 html_footer(user)));
4274
4275 return xs_html_render_s(html, "<!DOCTYPE html>\n");
4276}
4277
4278void notify_filter(snac *user, const xs_dict *p_vars)
4279/* sets filter for notifications */
4280{
4281 const char *v;
4282 int likes_on = (v = xs_dict_get(p_vars, "likes_on")) ? strcmp(v, "on") == 0 : 0;
4283 int reacts_on = (v = xs_dict_get(p_vars, "reacts_on")) ? strcmp(v, "on") == 0 : 0;
4284 int ments_on = (v = xs_dict_get(p_vars, "mentions_on")) ? strcmp(v, "on") == 0 : 0;
4285 int ann_on = (v = xs_dict_get(p_vars, "announces_on")) ? strcmp(v, "on") == 0 : 0;
4286 int foll_on = (v = xs_dict_get(p_vars, "follows_on")) ? strcmp(v, "on") == 0 : 0;
4287 int unfoll_on = (v = xs_dict_get(p_vars, "unfollows_on")) ? strcmp(v, "on") == 0 : 0;
4288 int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0;
4289 int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0;
4290 int polls_on = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0;
4291 int webmen_on = (v = xs_dict_get(p_vars, "webmentions_on")) ? strcmp(v, "on") == 0 : 0;
4292 int cool_posts_on = (v = xs_dict_get(p_vars, "cool_posts_on")) ? strcmp(v, "on") == 0 : 0;
4293 xs *filter = xs_dict_new();
4294 filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4295 filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4296 filter = xs_dict_set(filter, "mentions", xs_stock(ments_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4297 filter = xs_dict_set(filter, "announces", xs_stock(ann_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4298 filter = xs_dict_set(filter, "follows", xs_stock(foll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4299 filter = xs_dict_set(filter, "unfollows", xs_stock(unfoll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4300 filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4301 filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4302 filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4303 filter = xs_dict_set(filter, "webmentions", xs_stock(webmen_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4304 filter = xs_dict_set(filter, "cool_posts", xs_stock(cool_posts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4305 user->config = xs_dict_set(user->config, "notify_filter", filter);
4306 user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz
4307}
4308
4309xs_str *html_notifications(snac *user, int skip, int show)
4310{
4311 const char *proxy = NULL;
4312
4313 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4314 proxy = user->actor;
4315
4316 xs *n_list_unfilt = notify_list(user, 0, XS_ALL);
4317 xs *n_time = notify_check_time(user, 0);
4318
4319 xs_html *body = html_user_body(user, 0);
4320 const xs_dict *n_filter = xs_dict_get(user->config, "notify_filter");
4321 if (!n_filter) {
4322 xs *filter = xs_dict_new();
4323 user->config = xs_dict_set(user->config, "notify_filter", filter);
4324 user->tz = xs_dict_get_def(user->config, "tz", "UTC"); // previous line invalidates user->tz
4325 n_filter = xs_dict_get(user->config, "notify_filter");
4326 }
4327 xs *n_list = notify_filter_list(user, n_list_unfilt, skip, show);
4328 /* all filters are true by default */
4329 const xs_val *n_def = xs_stock( XSTYPE_TRUE );
4330 int n_likes_on = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
4331 int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
4332 int n_ments_on = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
4333 int n_ann_on = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
4334 int n_fol_on = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
4335 int n_unfol_on = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
4336 int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
4337 int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
4338 int n_polls_on = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
4339 int n_webmen_on = xs_is_true(xs_dict_get_def(n_filter, "webmentions", n_def));
4340 int n_cool_posts_on = xs_is_true(xs_dict_get_def(n_filter, "cool_posts", n_def));
4341
4342 xs_html *html = xs_html_tag("html",
4343 html_user_head(user, NULL, NULL),
4344 body);
4345
4346 xs *filter_notifs_action = xs_fmt("%s/admin/filter-notifications", user->actor);
4347 xs *notifs_action = xs_fmt("%s/notifications", user->actor);
4348 xs_html *notifs_form = xs_html_tag("form",
4349 xs_html_attr("autocomplete", "off"),
4350 xs_html_attr("method", "post"),
4351 xs_html_attr("action", filter_notifs_action),
4352 xs_html_attr("enctype", "multipart/form-data"),
4353 xs_html_attr("id", "filter"),
4354 xs_html_sctag("input",
4355 xs_html_attr("type", "hidden"),
4356 xs_html_attr("name", "hard-redir"),
4357 xs_html_attr("value", notifs_action)),
4358 html_checkbox("likes_on", L("Likes"), n_likes_on),
4359 html_checkbox("reacts_on", L("Emoji reacts"), n_reacts_on),
4360 html_checkbox("mentions_on", L("Mentions"), n_ments_on),
4361 html_checkbox("announces_on", L("Announces"), n_ann_on),
4362 html_checkbox("follows_on", L("Follows"), n_fol_on),
4363 html_checkbox("unfollows_on", L("Unfollows"), n_unfol_on),
4364 html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on),
4365 html_checkbox("blocks_on", L("Blocks"), n_blocks_on),
4366 html_checkbox("polls_on", L("Polls"), n_polls_on),
4367 html_checkbox("webmentions_on", L("Webmentions"), n_webmen_on),
4368 html_checkbox("cool_posts_on", L("Posts from cool users"), n_cool_posts_on),
4369 xs_html_sctag("input",
4370 xs_html_attr("type", "submit"),
4371 xs_html_attr("class", "button"),
4372 xs_html_attr("value", L("Save"))));
4373
4374 xs_html_add(body,
4375 xs_html_tag("p",
4376 xs_html_tag("div",
4377 xs_html_attr("class", "snac-notify-filter"),
4378 xs_html_tag("details",
4379 xs_html_tag("summary",
4380 xs_html_text(L("Notifications filter..."))),
4381 notifs_form))));
4382
4383 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
4384
4385 xs_html_add(body,
4386 xs_html_tag("form",
4387 xs_html_attr("autocomplete", "off"),
4388 xs_html_attr("method", "post"),
4389 xs_html_attr("action", clear_all_action),
4390 xs_html_attr("id", "clear"),
4391 xs_html_sctag("input",
4392 xs_html_attr("type", "submit"),
4393 xs_html_attr("class", "snac-btn-clear-all"),
4394 xs_html_attr("value", L("Clear all")))));
4395
4396 xs_html *noti_new = NULL;
4397 xs_html *noti_seen = NULL;
4398
4399 xs_html *posts = xs_html_tag("div",
4400 xs_html_attr("class", "snac-posts"));
4401 xs_html_add(body, posts);
4402
4403 xs_set rep;
4404 xs_set_init(&rep);
4405
4406 /* dict to store previous notification labels */
4407 xs *admiration_labels = xs_dict_new();
4408
4409 const xs_str *v;
4410
4411 xs_list_foreach(n_list, v) {
4412 xs *noti = notify_get(user, v);
4413
4414 if (noti == NULL)
4415 continue;
4416
4417 xs *obj = NULL;
4418 const char *type = xs_dict_get(noti, "type");
4419 const char *utype = xs_dict_get(noti, "utype");
4420 const char *id = xs_dict_get(noti, "objid");
4421 const char *date = xs_dict_get(noti, "date");
4422 const char *id2 = xs_dict_get_path(noti, "msg.id");
4423 xs *wrk = NULL;
4424
4425 if (xs_is_null(id))
4426 continue;
4427
4428 if (is_hidden(user, id))
4429 continue;
4430
4431 if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
4432 continue;
4433
4434 if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
4435 continue;
4436
4437 object_get(id, &obj);
4438
4439 const char *msg_id = NULL;
4440
4441 if (xs_is_dict(obj))
4442 msg_id = xs_dict_get(obj, "id");
4443
4444 const char *actor_id = xs_dict_get(noti, "actor");
4445 xs *actor = NULL;
4446 xs *a_name = NULL;
4447
4448 if (valid_status(actor_get(actor_id, &actor)))
4449 a_name = actor_name(actor, proxy);
4450 else {
4451 a_name = xs_dup(actor_id);
4452
4453 /* actor not here: request it */
4454 enqueue_actor_refresh(user, actor_id, 0);
4455 }
4456
4457 const char *html_url = xs_dict_get_def(actor, "url", actor_id);
4458
4459 if (xs_is_list(html_url))
4460 html_url = xs_list_get(html_url, 0);
4461
4462 if (!xs_is_string(html_url))
4463 html_url = actor_id;
4464
4465 xs *label_sanitized = sanitize(type);
4466 const char *label = label_sanitized;
4467
4468 if (strcmp(type, "Create") == 0) {
4469 if (was_mentioned(user, obj))
4470 label = L("Mention");
4471 else
4472 label = L("Post");
4473 }
4474 else
4475 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
4476 label = L("Finished poll");
4477 else
4478 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
4479 label = L("Unfollow");
4480 else
4481 if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
4482 const char *content = xs_dict_get_path(noti, "msg.content");
4483
4484 xs *cd = xs_dup(content);
4485 const char *sna = cd;
4486 const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
4487 unsigned int utf = xs_utf8_dec((const char **)&sna);
4488
4489 int isEmoji = 0;
4490 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
4491 isEmoji = 1;
4492
4493 if (xs_type(content) == XSTYPE_STRING) {
4494 if (!isEmoji && !n_likes_on)
4495 continue;
4496 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
4497 wrk = xs_fmt("%s (%s️)", isEmoji ? "EmojiReact" : "Like", emoji);
4498 label = wrk;
4499 }
4500 }
4501 else
4502 if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
4503 label = L("Follow Request");
4504
4505 xs *s_date = html_date_label(user, date);
4506
4507 xs_html *this_html_label = xs_html_container(
4508 xs_html_tag("b",
4509 xs_html_raw(label),
4510 xs_html_text(" by "),
4511 xs_html_tag("a",
4512 xs_html_attr("href", html_url),
4513 xs_html_raw(a_name))), /* a_name is already sanitized */
4514 xs_html_text(" "),
4515 xs_html_tag("time",
4516 xs_html_attr("class", "dt-published snac-pubdate"),
4517 xs_html_attr("title", date),
4518 xs_html_text(s_date)));
4519
4520 xs_html *html_label = NULL;
4521
4522 if (xs_is_string(msg_id)) {
4523 const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
4524
4525 if (xs_type(prev_label) == XSTYPE_DATA) {
4526 /* there is a previous list of admiration labels! */
4527 xs_data_get(&html_label, prev_label);
4528
4529 xs_html_add(html_label,
4530 xs_html_sctag("br", NULL),
4531 this_html_label);
4532
4533 continue;
4534 }
4535 }
4536
4537 xs_html *entry = NULL;
4538
4539 html_label = xs_html_tag("p",
4540 this_html_label);
4541
4542 /* store in the admiration labels dict */
4543 xs *pl = xs_data_new(&html_label, sizeof(html_label));
4544
4545 if (xs_is_string(msg_id))
4546 admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
4547
4548 entry = xs_html_tag("div",
4549 xs_html_attr("class", "snac-post-with-desc"),
4550 html_label);
4551
4552 if (strcmp(type, "Block") == 0) {
4553 if (actor)
4554 xs_html_add(entry,
4555 xs_html_tag("div",
4556 xs_html_attr("class", "snac-post"),
4557 html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4558 }
4559 else
4560 if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
4561 if (actor) {
4562 xs *action = xs_fmt("%s/admin/action", user->actor);
4563 xs_html *button = NULL;
4564
4565 if (following_check(user, actor_id))
4566 button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"));
4567 else
4568 button = html_button("follow", L("Follow"), L("Start following this user's activity"));
4569
4570 xs_html_add(entry,
4571 xs_html_tag("div",
4572 xs_html_attr("class", "snac-post"),
4573 html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL),
4574 xs_html_tag("form",
4575 xs_html_attr("method", "post"),
4576 xs_html_attr("action", action),
4577 xs_html_sctag("input",
4578 xs_html_attr("type", "hidden"),
4579 xs_html_attr("name", "actor"),
4580 xs_html_attr("value", actor_id)),
4581 button,
4582 xs_html_sctag("br", NULL))));
4583 }
4584 }
4585 else
4586 if (strcmp(type, "Move") == 0) {
4587 const xs_dict *o_msg = xs_dict_get(noti, "msg");
4588 const char *target;
4589
4590 if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
4591 xs *old_actor = NULL;
4592
4593 if (valid_status(actor_get(target, &old_actor))) {
4594 xs_html_add(entry,
4595 xs_html_tag("div",
4596 xs_html_attr("class", "snac-post"),
4597 html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4598 }
4599 }
4600 }
4601 else
4602 if (obj != NULL) {
4603 xs *md5 = xs_md5_hex(id, strlen(id));
4604 xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
4605
4606 xs_html *h = html_entry(user, obj, 0, 0, md5, 1, 0);
4607
4608 if (h != NULL) {
4609 xs_html_add(entry,
4610 xs_html_tag("p",
4611 xs_html_tag("a",
4612 xs_html_attr("href", ctxt),
4613 xs_html_text(L("Context")))),
4614 h);
4615 }
4616 else
4617 xs_html_add(entry,
4618 xs_html_tag("p",
4619 xs_html_text(L("Location: ")),
4620 xs_html_tag("a",
4621 xs_html_attr("href", id),
4622 xs_html_text(id))));
4623 }
4624
4625 if (strcmp(v, n_time) > 0) {
4626 /* unseen notification */
4627 if (noti_new == NULL) {
4628 noti_new = xs_html_tag("div",
4629 xs_html_tag("h2",
4630 xs_html_attr("class", "snac-header"),
4631 xs_html_text(L("New"))));
4632
4633 xs_html_add(posts,
4634 noti_new);
4635 }
4636
4637 xs_html_add(noti_new,
4638 entry);
4639 }
4640 else {
4641 /* already seen notification */
4642 if (noti_seen == NULL) {
4643 noti_seen = xs_html_tag("div",
4644 xs_html_tag("h2",
4645 xs_html_attr("class", "snac-header"),
4646 xs_html_text(L("Already seen"))));
4647
4648 xs_html_add(posts,
4649 noti_seen);
4650 }
4651
4652 xs_html_add(noti_seen,
4653 entry);
4654 }
4655 }
4656
4657 if (noti_new == NULL && noti_seen == NULL)
4658 xs_html_add(body,
4659 xs_html_tag("h2",
4660 xs_html_attr("class", "snac-header"),
4661 xs_html_text(L("None"))));
4662
4663 /* add the navigation footer */
4664 xs *next_p = notify_filter_list(user, n_list_unfilt, skip + show, 1);
4665
4666 if (xs_list_len(next_p)) {
4667 xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
4668 user->actor, skip + show, show);
4669
4670 xs_html_add(body,
4671 xs_html_tag("p",
4672 xs_html_tag("a",
4673 xs_html_attr("href", url),
4674 xs_html_text(L("More...")))));
4675 }
4676
4677 xs_set_free(&rep);
4678
4679 xs_html_add(body,
4680 html_footer(user));
4681
4682 /* set the check time to now */
4683 xs *dummy = notify_check_time(user, 1);
4684 dummy = xs_free(dummy);
4685
4686 timeline_touch(user);
4687
4688 return xs_html_render_s(html, "<!DOCTYPE html>\n");
4689}
4690
4691
4692void set_user_lang(snac *user)
4693/* sets the language dict according to user configuration */
4694{
4695 user->lang = NULL;
4696 const char *lang = xs_dict_get(user->config, "lang");
4697
4698 if (xs_is_string(lang))
4699 user->lang = xs_dict_get(srv_langs, lang);
4700}
4701
4702
4703int text_browser_friendly(const snac *user, const char *user_agent)
4704/* returns true if the web UI must be adapted to text browsers */
4705/* basically, this means to avoid details/summary as much as possible */
4706{
4707 if (user != NULL && xs_is_string(user_agent)) {
4708 snac_debug(user, 2, xs_fmt("user-agent: %s", user_agent));
4709
4710 const char *text_browser_uas = xs_dict_get(user->config, "text_browser_uas");
4711
4712 if (xs_is_string(text_browser_uas)) {
4713 xs *l = xs_split(text_browser_uas, "\n");
4714 const char *v;
4715
4716 xs_list_foreach(l, v) {
4717 if (*v == '\0')
4718 continue;
4719
4720 xs *v2 = xs_strip_i(xs_dup(v));
4721
4722 /* an asterisk alone means "all browsers", so yes */
4723 if (strcmp(v2, "*") == 0)
4724 return 1;
4725
4726 /* if this string is inside user_agent, then yes */
4727 if (xs_str_in(user_agent, v2) != -1)
4728 return 1;
4729 }
4730 }
4731 }
4732
4733 return 0;
4734}
4735
4736
4737int html_get_handler(const xs_dict *req, const char *q_path,
4738 char **body, int *b_size, char **ctype,
4739 xs_str **etag, xs_str **last_modified)
4740{
4741 const char *accept = xs_dict_get(req, "accept");
4742 int status = HTTP_STATUS_NOT_FOUND;
4743 const snac *user = NULL;
4744 snac snac;
4745 xs *uid = NULL;
4746 const char *p_path;
4747 int cache = 1;
4748 int save = 1;
4749 int proxy = 0;
4750 int terse = 0;
4751 int tb_friendly = 0;
4752 const char *v;
4753
4754 const xs_dict *q_vars = xs_dict_get(req, "q_vars");
4755
4756 xs *l = xs_split_n(q_path, "/", 2);
4757 v = xs_list_get(l, 1);
4758
4759 if (xs_is_null(v)) {
4760 srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
4761 return HTTP_STATUS_NOT_FOUND;
4762 }
4763
4764 if (!xs_is_null(xs_dict_get(q_vars, "terse")))
4765 terse = 1;
4766
4767 if (strcmp(v, "share-bridge") == 0) {
4768 /* temporary redirect for a post */
4769 const char *login = xs_dict_get(q_vars, "login");
4770 const char *content = xs_dict_get(q_vars, "content");
4771
4772 if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
4773 xs *b64 = xs_base64_enc(content, strlen(content));
4774 srv_log(xs_fmt("share-bridge for user '%s'", login));
4775
4776 *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
4777 return HTTP_STATUS_SEE_OTHER;
4778 }
4779 else
4780 return HTTP_STATUS_NOT_FOUND;
4781 }
4782 else
4783 if (strcmp(v, "auth-int-bridge") == 0) {
4784 const char *login = xs_dict_get(q_vars, "login");
4785 const char *id = xs_dict_get(q_vars, "id");
4786 const char *action = xs_dict_get(q_vars, "action");
4787
4788 if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
4789 *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
4790 srv_baseurl, login, action, id);
4791
4792 return HTTP_STATUS_SEE_OTHER;
4793 }
4794 else
4795 return HTTP_STATUS_NOT_FOUND;
4796 }
4797
4798 uid = xs_dup(v);
4799
4800 /* rss extension? */
4801 if (xs_endswith(uid, ".rss")) {
4802 uid = xs_crop_i(uid, 0, -4);
4803 p_path = ".rss";
4804 }
4805 else
4806 p_path = xs_list_get(l, 2);
4807
4808 if (!uid || !user_open(&snac, uid)) {
4809 /* invalid user */
4810 status = grave(uid, OP_CHECK) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
4811 srv_debug(2, xs_fmt("html_get_handler bad user '%s' %d", xs_or(uid, "(null)"), status));
4812 return status;
4813 }
4814
4815 user = &snac; /* for L() */
4816 set_user_lang(&snac);
4817
4818 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4819 proxy = 1;
4820
4821 tb_friendly = text_browser_friendly(user, xs_dict_get(req, "user-agent"));
4822
4823 /* return the RSS if requested by Accept header */
4824 if (accept != NULL) {
4825 if (xs_str_in(accept, "text/xml") != -1 ||
4826 xs_str_in(accept, "application/rss+xml") != -1)
4827 p_path = ".rss";
4828 }
4829
4830 /* check if server config variable 'disable_cache' is set */
4831 if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
4832 cache = 0;
4833
4834 int skip = 0;
4835 const char *max_show_default = "50";
4836 int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
4837 max_show_default));
4838 int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
4839 xs_dict_get_def(srv_config, "max_timeline_entries",
4840 max_show_default)));
4841 int show = def_show;
4842
4843 if ((v = xs_dict_get(q_vars, "skip")) != NULL)
4844 skip = atoi(v), cache = 0, save = 0;
4845 if ((v = xs_dict_get(q_vars, "show")) != NULL)
4846 show = atoi(v), cache = 0, save = 0;
4847 if ((v = xs_dict_get(q_vars, "da")) != NULL) {
4848 /* user dismissed an announcement */
4849 if (login(&snac, req)) {
4850 double ts = atof(v);
4851 xs *timestamp = xs_number_new(ts);
4852 srv_log(xs_fmt("user dismissed announcements until %d", ts));
4853 snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
4854 user_persist(&snac, 0);
4855 }
4856 }
4857
4858 /* get a possible error message */
4859 const char *error = xs_dict_get(q_vars, "error");
4860 if (error != NULL)
4861 cache = 0;
4862
4863 /* a show of 0 has no sense */
4864 if (show == 0)
4865 show = def_show;
4866 if (show > max_show)
4867 show = max_show;
4868
4869 if (p_path == NULL) { /** public timeline **/
4870 xs *h = xs_str_localtime(0, "%Y-%m.html");
4871
4872 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
4873 /** empty public timeline for private users **/
4874 *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse, tb_friendly);
4875 *b_size = strlen(*body);
4876 status = HTTP_STATUS_OK;
4877 }
4878 else
4879 if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
4880 snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
4881
4882 status = history_get(&snac, h, body, b_size,
4883 xs_dict_get(req, "if-none-match"), etag);
4884 }
4885 else {
4886 xs *list = NULL;
4887 int more = 0;
4888
4889 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
4890 list = timeline_simple_list(&snac, "public", skip, show, &more);
4891 else
4892 list = timeline_list(&snac, "public", skip, show, &more);
4893
4894 xs *pins = pinned_list(&snac);
4895 pins = xs_list_cat(pins, list);
4896
4897 *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse, tb_friendly);
4898
4899 *b_size = strlen(*body);
4900 status = HTTP_STATUS_OK;
4901
4902 if (save)
4903 history_add(&snac, h, *body, *b_size, etag);
4904 }
4905 }
4906 else
4907 if (strcmp(p_path, "operations") == 0) { /** operations and user settings **/
4908 if (!login(&snac, req)) {
4909 *body = xs_dup(uid);
4910 status = HTTP_STATUS_UNAUTHORIZED;
4911 }
4912 else {
4913 xs_html *h_body = html_user_body(&snac, 0);
4914
4915 xs_html *html = xs_html_tag("html",
4916 html_user_head(&snac, NULL, NULL),
4917 h_body);
4918
4919 xs_html_add(h_body,
4920 html_top_controls(&snac, 0),
4921 html_footer(&snac));
4922
4923 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
4924 *b_size = strlen(*body);
4925 status = HTTP_STATUS_OK;
4926 }
4927 }
4928 else
4929 if (strcmp(p_path, "admin") == 0) { /** private timeline **/
4930 if (!login(&snac, req)) {
4931 *body = xs_dup(uid);
4932 status = HTTP_STATUS_UNAUTHORIZED;
4933 }
4934 else {
4935 const char *q = NULL;
4936 xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
4937 xs *url_acct = NULL;
4938
4939 if (xs_is_string(cq)) {
4940 cq = xs_strip_i(cq);
4941 q = cq;
4942 }
4943
4944 /* searching for an URL? */
4945 if (q && xs_match(q, "https://*|http://*")) {
4946 /* bring it to the user's timeline */
4947 xs *actor_obj = NULL;
4948 xs *object = NULL;
4949 int status;
4950
4951 status = activitypub_request(&snac, q, &object);
4952 snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
4953
4954 if (valid_status(status) && xs_is_dict(object)) {
4955 /* got it; also request the actor */
4956 const char *attr_to = get_atto(object);
4957
4958 if (!xs_is_null(attr_to)) {
4959 status = actor_request(&snac, attr_to, &actor_obj);
4960
4961 if (valid_status(status)) {
4962 /* reset the query string to be the real id */
4963 url_acct = xs_dup(xs_dict_get(object, "id"));
4964 q = url_acct;
4965
4966 /* add the post to the timeline */
4967 if (!timeline_here(&snac, q))
4968 timeline_add(&snac, q, object);
4969 }
4970 }
4971 else {
4972 /* retry webfinger, this time with the 'official' id */
4973 const char *id = xs_dict_get(object, "id");
4974
4975 if (xs_is_string(id) && valid_status(webfinger_request_fake(id, &actor_obj, &url_acct)) &&
4976 xs_is_string(url_acct))
4977 q = url_acct;
4978 }
4979 }
4980 else
4981 /* may by an actor; try a webfinger */
4982 if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) {
4983 /* it's an actor; do the dirty trick of changing q to the account name */
4984 q = url_acct;
4985 }
4986
4987 /* fall through */
4988 }
4989
4990 if (q && *q) {
4991 if (xs_regex_match(q, "^@?[^@]+@[a-zA-Z0-9-]+\\.")) {
4992 /** search account **/
4993 xs *actor = NULL;
4994 xs *acct = NULL;
4995 xs *l = xs_list_new();
4996 xs_html *page = NULL;
4997
4998 if (valid_status(webfinger_request(q, &actor, &acct))) {
4999 xs *actor_obj = NULL;
5000
5001 if (valid_status(actor_request(&snac, actor, &actor_obj))) {
5002 actor_add(actor, actor_obj);
5003
5004 /* create a people list with only one element */
5005 l = xs_list_append(l, actor);
5006
5007 xs *title = xs_fmt(L("Search results for account %s"), q);
5008
5009 page = html_people_list(&snac, l, title, "wf", NULL, 1);
5010 }
5011 }
5012
5013 if (page == NULL) {
5014 xs *title = xs_fmt(L("Account %s not found"), q);
5015
5016 page = xs_html_tag("div",
5017 xs_html_tag("h2",
5018 xs_html_attr("class", "snac-header"),
5019 xs_html_text(title)));
5020 }
5021
5022 xs_html *html = xs_html_tag("html",
5023 html_user_head(&snac, NULL, NULL),
5024 xs_html_add(html_user_body(&snac, 0),
5025 page,
5026 html_footer(user)));
5027
5028 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
5029 *b_size = strlen(*body);
5030 status = HTTP_STATUS_OK;
5031 }
5032 else
5033 if (*q == '#') {
5034 /** search by tag **/
5035 xs *tl = tag_search(q, skip, show + 1);
5036 int more = 0;
5037 if (xs_list_len(tl) >= show + 1) {
5038 /* drop the last one */
5039 tl = xs_list_del(tl, -1);
5040 more = 1;
5041 }
5042
5043 xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
5044 xs *title = xs_fmt(xs_list_len(tl) ?
5045 L("Search results for tag %s") : L("Nothing found for tag %s"), q);
5046
5047 *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse, tb_friendly);
5048 *b_size = strlen(*body);
5049 status = HTTP_STATUS_OK;
5050 }
5051 else {
5052 /** search by content **/
5053 int to = 0;
5054 int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
5055 xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
5056 xs *title = NULL;
5057 xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
5058 int tl_len = xs_list_len(tl);
5059
5060 if (to)
5061 title = xs_fmt(L("Search results for '%s' (may be more)"), q);
5062 else
5063 if (tl_len)
5064 title = xs_fmt(L("Search results for '%s'"), q);
5065 else
5066 if (skip)
5067 title = xs_fmt(L("No more matches for '%s'"), q);
5068 else
5069 title = xs_fmt(L("Nothing found for '%s'"), q);
5070
5071 *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
5072 title, page, 0, error, terse, tb_friendly);
5073 *b_size = strlen(*body);
5074 status = HTTP_STATUS_OK;
5075 }
5076 }
5077 else {
5078 /** the private timeline **/
5079 double t = history_mtime(&snac, "timeline.html_");
5080
5081 /* if enabled by admin, return a cached page if its timestamp is:
5082 a) newer than the timeline timestamp
5083 b) newer than the start time of the server
5084 */
5085 if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
5086 snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
5087
5088 status = history_get(&snac, "timeline.html_", body, b_size,
5089 xs_dict_get(req, "if-none-match"), etag);
5090 }
5091 else {
5092 int more = 0;
5093
5094 snac_debug(&snac, 1, xs_fmt("building timeline"));
5095
5096 xs *list = timeline_list(&snac, "private", skip, show, &more);
5097
5098 *body = html_timeline(&snac, list, 0, skip, show,
5099 more, NULL, "/admin", 1, error, terse, tb_friendly);
5100
5101 *b_size = strlen(*body);
5102 status = HTTP_STATUS_OK;
5103
5104 if (save)
5105 history_add(&snac, "timeline.html_", *body, *b_size, etag);
5106
5107 timeline_add_mark(&snac);
5108 }
5109 }
5110 }
5111 }
5112 else
5113 if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
5114 if (!login(&snac, req)) {
5115 *body = xs_dup(uid);
5116 status = HTTP_STATUS_UNAUTHORIZED;
5117 }
5118 else {
5119 xs *l = xs_split(p_path, "/");
5120 const char *md5 = xs_list_get(l, -1);
5121
5122 if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) {
5123 xs *list0 = xs_list_append(xs_list_new(), md5);
5124 xs *list = timeline_top_level(&snac, list0);
5125
5126 *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse, tb_friendly);
5127 *b_size = strlen(*body);
5128 status = HTTP_STATUS_OK;
5129 }
5130 }
5131 }
5132 else
5133 if (strcmp(p_path, "people") == 0) { /** the list of people **/
5134 if (!login(&snac, req)) {
5135 *body = xs_dup(uid);
5136 status = HTTP_STATUS_UNAUTHORIZED;
5137 }
5138 else {
5139 *body = html_people(&snac);
5140 *b_size = strlen(*body);
5141 status = HTTP_STATUS_OK;
5142 }
5143 }
5144 else
5145 if (xs_startswith(p_path, "people/")) { /** a single actor **/
5146 if (!login(&snac, req)) {
5147 *body = xs_dup(uid);
5148 status = HTTP_STATUS_UNAUTHORIZED;
5149 }
5150 else {
5151 xs *actor_dict = NULL;
5152 const char *actor_id = NULL;
5153 xs *actor = NULL;
5154 xs_list *page_lst = xs_split_n(p_path, "?", 2);
5155 xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
5156 xs_list *l = xs_split_n(page, "/", 3);
5157 const char *actor_md5 = xs_list_get(l, 2);
5158
5159 if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
5160 (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
5161 valid_status(actor_get(actor_id, &actor))) {
5162 int more = 0;
5163 xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
5164
5165 *body = html_people_one(&snac, actor_id, list, skip, show, more, page);
5166 *b_size = strlen(*body);
5167 status = HTTP_STATUS_OK;
5168 }
5169 else {
5170 *body = xs_dup(uid);
5171 status = HTTP_STATUS_NOT_FOUND;
5172 }
5173 }
5174 }
5175 else
5176 if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
5177 if (!login(&snac, req)) {
5178 *body = xs_dup(uid);
5179 status = HTTP_STATUS_UNAUTHORIZED;
5180 }
5181 else {
5182 *body = html_notifications(&snac, skip, show);
5183 *b_size = strlen(*body);
5184 status = HTTP_STATUS_OK;
5185 }
5186 }
5187 else
5188 if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
5189 if (!login(&snac, req)) {
5190 *body = xs_dup(uid);
5191 status = HTTP_STATUS_UNAUTHORIZED;
5192 }
5193 else {
5194 xs *list = timeline_instance_list(skip, show);
5195 xs *next = timeline_instance_list(skip + show, 1);
5196
5197 *body = html_timeline(&snac, list, 0, skip, show,
5198 xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse, tb_friendly);
5199 *b_size = strlen(*body);
5200 status = HTTP_STATUS_OK;
5201 }
5202 }
5203 else
5204 if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
5205 if (!login(&snac, req)) {
5206 *body = xs_dup(uid);
5207 status = HTTP_STATUS_UNAUTHORIZED;
5208 }
5209 else {
5210 xs *list = pinned_list(&snac);
5211
5212 *body = html_timeline(&snac, list, 0, skip, show,
5213 0, L("Pinned posts"), "", 0, error, terse, tb_friendly);
5214 *b_size = strlen(*body);
5215 status = HTTP_STATUS_OK;
5216 }
5217 }
5218 else
5219 if (strcmp(p_path, "admirations") == 0) { /** list of admired posts **/
5220 if (!login(&snac, req)) {
5221 *body = xs_dup(uid);
5222 status = HTTP_STATUS_UNAUTHORIZED;
5223 }
5224 else {
5225 int more = 0;
5226 xs *list = timeline_list(&snac, "admire", skip, show, &more);
5227
5228 *body = html_timeline(&snac, list, 0, skip, show,
5229 more, L("Liked, boosted or reacted posts"), "/admirations", 0, error, terse, tb_friendly);
5230 *b_size = strlen(*body);
5231 status = HTTP_STATUS_OK;
5232 }
5233 }
5234 else
5235 if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
5236 if (!login(&snac, req)) {
5237 *body = xs_dup(uid);
5238 status = HTTP_STATUS_UNAUTHORIZED;
5239 }
5240 else {
5241 xs *list = bookmark_list(&snac);
5242
5243 *body = html_timeline(&snac, list, 0, skip, show,
5244 0, L("Bookmarked posts"), "", 0, error, terse, tb_friendly);
5245 *b_size = strlen(*body);
5246 status = HTTP_STATUS_OK;
5247 }
5248 }
5249 else
5250 if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
5251 if (!login(&snac, req)) {
5252 *body = xs_dup(uid);
5253 status = HTTP_STATUS_UNAUTHORIZED;
5254 }
5255 else {
5256 xs *list = draft_list(&snac);
5257
5258 *body = html_timeline(&snac, list, 0, skip, show,
5259 0, L("Post drafts"), "", 0, error, terse, tb_friendly);
5260 *b_size = strlen(*body);
5261 status = HTTP_STATUS_OK;
5262 }
5263 }
5264 else
5265 if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
5266 if (!login(&snac, req)) {
5267 *body = xs_dup(uid);
5268 status = HTTP_STATUS_UNAUTHORIZED;
5269 }
5270 else {
5271 xs *list = scheduled_list(&snac);
5272
5273 *body = html_timeline(&snac, list, 0, skip, show,
5274 0, L("Scheduled posts"), "", 0, error, terse, tb_friendly);
5275 *b_size = strlen(*body);
5276 status = HTTP_STATUS_OK;
5277 }
5278 }
5279 else
5280 if (xs_startswith(p_path, "list/")) { /** list timelines **/
5281 if (!login(&snac, req)) {
5282 *body = xs_dup(uid);
5283 status = HTTP_STATUS_UNAUTHORIZED;
5284 }
5285 else {
5286 xs *l = xs_split(p_path, "/");
5287 const char *lid = xs_list_get(l, -1);
5288
5289 xs *list = list_timeline(&snac, lid, skip, show);
5290 xs *next = list_timeline(&snac, lid, skip + show, 1);
5291
5292 if (list != NULL) {
5293 xs *ttl = timeline_top_level(&snac, list);
5294
5295 xs *base = xs_fmt("/list/%s", lid);
5296 xs *name = list_maint(&snac, lid, OP_ID);
5297 xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
5298
5299 *body = html_timeline(&snac, ttl, 0, skip, show,
5300 xs_list_len(next), title, base, 1, error, terse, tb_friendly);
5301 *b_size = strlen(*body);
5302 status = HTTP_STATUS_OK;
5303 }
5304 }
5305 }
5306 else
5307 if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
5308 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5309 return HTTP_STATUS_FORBIDDEN;
5310
5311 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
5312 xs *msg = NULL;
5313
5314 if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) {
5315 xs *md5 = xs_md5_hex(id, strlen(id));
5316 xs *list = xs_list_new();
5317
5318 list = xs_list_append(list, md5);
5319
5320 *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse, tb_friendly);
5321 *b_size = strlen(*body);
5322 status = HTTP_STATUS_OK;
5323 }
5324 }
5325 else
5326 if (xs_startswith(p_path, "s/")) { /** a static file **/
5327 xs *l = xs_split_n(p_path, "/", 1);
5328 const char *id = xs_list_get(l, 1);
5329 int sz;
5330
5331 if (id && *id) {
5332 status = static_get(&snac, id, body, &sz,
5333 xs_dict_get(req, "if-none-match"), etag);
5334
5335 if (valid_status(status)) {
5336 *b_size = sz;
5337 *ctype = (char *)xs_mime_by_ext(id);
5338 }
5339 }
5340 }
5341 else
5342 if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
5343 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5344 return HTTP_STATUS_FORBIDDEN;
5345
5346 if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
5347 return HTTP_STATUS_FORBIDDEN;
5348
5349 xs *l = xs_split(p_path, "/");
5350 const char *id = xs_list_get(l, 1);
5351
5352 if (id && *id) {
5353 if (xs_endswith(id, "timeline.html_")) {
5354 /* Don't let them in */
5355 *b_size = 0;
5356 status = HTTP_STATUS_NOT_FOUND;
5357 }
5358 else
5359 status = history_get(&snac, id, body, b_size,
5360 xs_dict_get(req, "if-none-match"), etag);
5361 }
5362 }
5363 else
5364 if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
5365 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
5366 return HTTP_STATUS_FORBIDDEN;
5367
5368 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
5369
5370 xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
5371 xs *bio = xs_dup(xs_dict_get(snac.config, "bio"));
5372
5373 xs *rss_title = xs_fmt("%s (@%s@%s)",
5374 xs_dict_get(snac.config, "name"),
5375 snac.uid,
5376 xs_dict_get(srv_config, "host"));
5377 xs *rss_link = xs_fmt("%s.rss", snac.actor);
5378
5379 *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio);
5380 *b_size = strlen(*body);
5381 *ctype = "application/rss+xml; charset=utf-8";
5382 status = HTTP_STATUS_OK;
5383
5384 snac_debug(&snac, 1, xs_fmt("serving RSS"));
5385 }
5386 else
5387 if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
5388 xs *proxy_prefix = NULL;
5389
5390 if (xs_startswith(p_path, "x/")) {
5391 /* proxy usage authorized by http basic auth */
5392 if (login(&snac, req))
5393 proxy_prefix = xs_str_new("x/");
5394 else {
5395 *body = xs_dup(uid);
5396 status = HTTP_STATUS_UNAUTHORIZED;
5397 }
5398 }
5399 else {
5400 /* proxy usage authorized by proxy_token */
5401 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
5402 xs *tk = xs_md5_hex(tks, strlen(tks));
5403 xs *p = xs_fmt("y/%s/", tk);
5404
5405 if (xs_startswith(p_path, p))
5406 proxy_prefix = xs_dup(p);
5407 }
5408
5409 if (proxy_prefix) {
5410 /* pick the raw path (including optional ? arguments) */
5411 const char *raw_path = xs_dict_get(req, "raw_path");
5412
5413 /* skip the prefix and user part */
5414 xs *s = xs_fmt("%s/%s/", xs_dict_get(srv_config, "prefix"), snac.uid);
5415 raw_path += strlen(s);
5416
5417 /* skip to where the proxy/ string starts */
5418 raw_path += xs_str_in(raw_path, proxy_prefix);
5419
5420 xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
5421 xs *hdrs = xs_dict_new();
5422
5423 hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
5424
5425 const char *ims = xs_dict_get(req, "if-modified-since");
5426 const char *inm = xs_dict_get(req, "if-none-match");
5427
5428 if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
5429 if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
5430
5431 xs *rsp = xs_http_request("GET", url, hdrs,
5432 NULL, 0, &status, body, b_size, 0);
5433
5434 if (valid_status(status)) {
5435 const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
5436 const char *lm = xs_dict_get(rsp, "last-modified");
5437 const char *et = xs_dict_get(rsp, "etag");
5438
5439 if (lm) *last_modified = xs_dup(lm);
5440 if (et) *etag = xs_dup(et);
5441
5442 /* find the content-type in the static mime types,
5443 and return that value instead of ct, which will
5444 be destroyed when out of scope */
5445 for (int n = 0; xs_mime_types[n]; n += 2) {
5446 if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
5447 *ctype = (char *)xs_mime_types[n + 1];
5448 break;
5449 }
5450 }
5451 }
5452
5453 snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
5454
5455 if (status >= 400 && status <= 499)
5456 status = HTTP_STATUS_NOT_FOUND;
5457 }
5458 }
5459 else
5460 if (strcmp(p_path, "share") == 0) { /** direct post **/
5461 if (!login(&snac, req)) {
5462 *body = xs_dup(uid);
5463 status = HTTP_STATUS_UNAUTHORIZED;
5464 }
5465 else {
5466 const char *b64 = xs_dict_get(q_vars, "content");
5467 xs *b64_2 = xs_replace(b64, " ", "+");
5468 int sz;
5469 xs *content = xs_base64_dec(b64_2, &sz);
5470 xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL);
5471 xs *c_msg = msg_create(&snac, msg);
5472
5473 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
5474
5475 enqueue_message(&snac, c_msg);
5476
5477 snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
5478
5479 *body = xs_fmt("%s/admin", snac.actor);
5480 *b_size = strlen(*body);
5481 status = HTTP_STATUS_SEE_OTHER;
5482 }
5483 }
5484 else
5485 if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
5486 if (!login(&snac, req)) {
5487 *body = xs_dup(uid);
5488 status = HTTP_STATUS_UNAUTHORIZED;
5489 }
5490 else {
5491 status = HTTP_STATUS_NOT_FOUND;
5492
5493 const char *id = xs_dict_get(q_vars, "id");
5494 const char *action = xs_dict_get(q_vars, "action");
5495
5496 if (xs_is_string(id) && xs_is_string(action)) {
5497 if (strcmp(action, "Follow") == 0) {
5498 xs *msg = msg_follow(&snac, id);
5499
5500 if (msg != NULL) {
5501 const char *actor = xs_dict_get(msg, "object");
5502
5503 following_add(&snac, actor, msg);
5504
5505 enqueue_output_by_actor(&snac, msg, actor, 0);
5506
5507 status = HTTP_STATUS_SEE_OTHER;
5508 }
5509 }
5510 else
5511 if (xs_match(action, "Like|Boost|Announce")) {
5512 /* bring the post */
5513 xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
5514
5515 if (msg != NULL) {
5516 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
5517 enqueue_message(&snac, msg);
5518
5519 status = HTTP_STATUS_SEE_OTHER;
5520 }
5521 }
5522 }
5523
5524 if (status == HTTP_STATUS_SEE_OTHER) {
5525 *body = xs_fmt("%s/admin", snac.actor);
5526 *b_size = strlen(*body);
5527 }
5528 }
5529 }
5530 else
5531 status = HTTP_STATUS_NOT_FOUND;
5532
5533 user_free(&snac);
5534
5535 if (valid_status(status) && *ctype == NULL) {
5536 *ctype = "text/html; charset=utf-8";
5537 }
5538
5539 return status;
5540}
5541
5542
5543int html_post_handler(const xs_dict *req, const char *q_path,
5544 char *payload, int p_size,
5545 char **body, int *b_size, char **ctype)
5546{
5547 (void)p_size;
5548 (void)ctype;
5549
5550 int status = 0;
5551 const snac *user = NULL;
5552 snac snac;
5553 const char *uid;
5554 const char *p_path;
5555 const xs_dict *p_vars;
5556
5557 xs *l = xs_split_n(q_path, "/", 2);
5558
5559 uid = xs_list_get(l, 1);
5560 if (!uid || !user_open(&snac, uid)) {
5561 /* invalid user */
5562 srv_debug(1, xs_fmt("html_post_handler bad user %s", xs_or(uid, "(null)")));
5563 return HTTP_STATUS_NOT_FOUND;
5564 }
5565
5566 p_path = xs_list_get(l, 2);
5567
5568 /* all posts must be authenticated */
5569 if (!login(&snac, req)) {
5570 user_free(&snac);
5571 *body = xs_dup(uid);
5572 return HTTP_STATUS_UNAUTHORIZED;
5573 }
5574
5575 user = &snac; /* for L() */
5576 set_user_lang(&snac);
5577
5578 p_vars = xs_dict_get(req, "p_vars");
5579
5580 if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
5581 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
5582
5583 /* post note */
5584 const char *content = xs_dict_get(p_vars, "content");
5585 const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
5586 const char *to = xs_dict_get(p_vars, "to");
5587 const char *sensitive = xs_dict_get(p_vars, "sensitive");
5588 const char *summary = xs_dict_get(p_vars, "summary");
5589 const char *edit_id = xs_dict_get(p_vars, "edit_id");
5590 const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
5591 const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
5592 const char *post_lang = xs_dict_get(p_vars, "post_lang");
5593 const char *visibility = xs_dict_get(p_vars, "visibility");
5594 int scope = SCOPE_PUBLIC; /* default to public */
5595 if (!xs_is_null(visibility)) {
5596 if (strcmp(visibility, "unlisted") == 0)
5597 scope = SCOPE_UNLISTED;
5598 else
5599 if (strcmp(visibility, "followers") == 0)
5600 scope = SCOPE_FOLLOWERS;
5601 else
5602 if (strcmp(visibility, "mentioned") == 0)
5603 scope = SCOPE_MENTIONED;
5604 }
5605 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
5606 xs *attach_list = xs_list_new();
5607
5608 /* iterate the attachments */
5609 int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
5610
5611 for (int att_n = 0; att_n < max_attachments; att_n++) {
5612 xs *url_lbl = xs_fmt("attach_url_%d", att_n);
5613 xs *att_lbl = xs_fmt("attach_%d", att_n);
5614 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
5615
5616 const char *attach_url = xs_dict_get(p_vars, url_lbl);
5617 const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
5618 const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, "");
5619
5620 if (xs_is_string(attach_url) && *attach_url != '\0') {
5621 xs *l = xs_list_new();
5622
5623 l = xs_list_append(l, attach_url);
5624 l = xs_list_append(l, alt_text);
5625
5626 attach_list = xs_list_append(attach_list, l);
5627 }
5628 else
5629 if (xs_is_list(attach_file)) {
5630 const char *fn = xs_list_get(attach_file, 0);
5631
5632 if (xs_is_string(fn) && *fn != '\0') {
5633 char rnd[32];
5634 xs_rnd_buf(rnd, sizeof(rnd));
5635
5636 const char *ext = strrchr(fn, '.');
5637 xs *hash = xs_md5_hex(rnd, sizeof(rnd));
5638 xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
5639 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
5640 int fo = xs_number_get(xs_list_get(attach_file, 1));
5641 int fs = xs_number_get(xs_list_get(attach_file, 2));
5642
5643 /* store */
5644 static_put(&snac, id, payload + fo, fs);
5645
5646 xs *l = xs_list_new();
5647
5648 l = xs_list_append(l, url);
5649 l = xs_list_append(l, alt_text);
5650
5651 attach_list = xs_list_append(attach_list, l);
5652 }
5653 }
5654 }
5655
5656 if (content != NULL) {
5657 xs *msg = NULL;
5658 xs *c_msg = NULL;
5659 xs *content_2 = xs_replace(content, "\r", "");
5660 xs *poll_opts = NULL;
5661
5662 /* is there a valid set of poll options? */
5663 const char *v = xs_dict_get(p_vars, "poll_options");
5664 if (!xs_is_null(v) && *v) {
5665 xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
5666
5667 poll_opts = xs_split(v2, "\n");
5668 }
5669
5670 if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
5671 /* get the rest of poll configuration */
5672 const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
5673 const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
5674 int multiple = 0;
5675
5676 int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
5677
5678 if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
5679 multiple = 1;
5680
5681 msg = msg_question(&snac, content_2, attach_list,
5682 poll_opts, multiple, end_secs);
5683
5684 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
5685 }
5686 else
5687 msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
5688
5689 if (sensitive != NULL) {
5690 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
5691 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
5692 }
5693
5694 if (xs_is_string(post_date) && *post_date) {
5695 xs *post_pubdate = xs_fmt("%sT%s", post_date,
5696 xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
5697
5698 time_t t = xs_parse_iso_date(post_pubdate, 0);
5699
5700 if (t != 0) {
5701 t -= xs_tz_offset(snac.tz);
5702
5703 xs *iso_date = xs_str_iso_date(t);
5704 msg = xs_dict_set(msg, "published", iso_date);
5705
5706 snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
5707 }
5708 else
5709 snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
5710 }
5711
5712 /* is the published date from the future? */
5713 int future_post = 0;
5714 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
5715
5716 if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
5717 future_post = 1;
5718
5719 if (xs_is_null(edit_id)) {
5720 /* new message */
5721 const char *id = xs_dict_get(msg, "id");
5722
5723 if (store_as_draft) {
5724 draft_add(&snac, id, msg);
5725 }
5726 else
5727 if (future_post) {
5728 schedule_add(&snac, id, msg);
5729 }
5730 else {
5731 c_msg = msg_create(&snac, msg);
5732 timeline_add(&snac, id, msg);
5733 }
5734 }
5735 else {
5736 /* an edition of a previous message */
5737 xs *p_msg = NULL;
5738
5739 if (valid_status(object_get(edit_id, &p_msg))) {
5740 /* copy relevant fields from previous version */
5741 char *fields[] = { "id", "context", "url",
5742 "cc", "attributedTo",
5743 "to", "inReplyTo", NULL };
5744 int n;
5745
5746 for (n = 0; fields[n]; n++) {
5747 const char *v = xs_dict_get(p_msg, fields[n]);
5748 msg = xs_dict_set(msg, fields[n], v);
5749 }
5750
5751 if (store_as_draft) {
5752 draft_add(&snac, edit_id, msg);
5753 }
5754 else
5755 if (is_draft(&snac, edit_id)) {
5756 /* message was previously a draft; it's a create activity */
5757
5758 /* if the date is from the past, overwrite it with right_now */
5759 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
5760 snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
5761 msg = xs_dict_set(msg, "published", right_now);
5762 }
5763
5764 /* overwrite object */
5765 object_add_ow(edit_id, msg);
5766
5767 if (future_post) {
5768 schedule_add(&snac, edit_id, msg);
5769 }
5770 else {
5771 c_msg = msg_create(&snac, msg);
5772 timeline_add(&snac, edit_id, msg);
5773 }
5774
5775 draft_del(&snac, edit_id);
5776 }
5777 else
5778 if (is_scheduled(&snac, edit_id)) {
5779 /* editing an scheduled post; just update it */
5780 schedule_add(&snac, edit_id, msg);
5781 }
5782 else {
5783 /* ignore the (possibly changed) published date */
5784 msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
5785
5786 /* set the updated field */
5787 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
5788 msg = xs_dict_set(msg, "updated", updated);
5789
5790 /* overwrite object, not updating the indexes */
5791 object_add_ow(edit_id, msg);
5792
5793 /* index tags */
5794 tag_index(edit_id, msg);
5795
5796 /* update message */
5797 c_msg = msg_update(&snac, msg);
5798 }
5799 }
5800 else
5801 snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
5802 }
5803
5804 if (c_msg != NULL) {
5805 enqueue_message(&snac, c_msg);
5806 enqueue_webmention(msg);
5807 }
5808
5809 history_del(&snac, "timeline.html_");
5810 }
5811
5812 status = HTTP_STATUS_SEE_OTHER;
5813 }
5814 else
5815 if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
5816 /* action on an entry */
5817 const char *id = xs_dict_get(p_vars, "id");
5818 const char *actor = xs_dict_get(p_vars, "actor");
5819 const char *action = xs_dict_get(p_vars, "action");
5820 const char *group = xs_dict_get(p_vars, "group");
5821
5822 if (action == NULL)
5823 return HTTP_STATUS_NOT_FOUND;
5824
5825 snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
5826
5827 status = HTTP_STATUS_SEE_OTHER;
5828
5829 if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
5830 const char *eid = xs_dict_get(p_vars, "eid");
5831
5832 if (eid != NULL) {
5833 object_user_cache_del(&snac, id, "admire");
5834 xs *n_msg = msg_emoji_unreact(&snac, id, eid);
5835
5836 if (n_msg != NULL)
5837 enqueue_message(&snac, n_msg);
5838 }
5839 }
5840 else
5841 if (strcmp(action, L("EmojiReact")) == 0) { /** **/
5842 xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
5843
5844 if (xs_is_string(eid) && strcmp(eid, "%")) {
5845 xs *ret = msg_emoji_init(&snac, id, eid);
5846 /* fails if either invalid or already reacted */
5847 if (!ret) {
5848 object_user_cache_del(&snac, id, "admire");
5849 ret = msg_emoji_unreact(&snac, id, eid);
5850 }
5851 if (!ret)
5852 status = HTTP_STATUS_NOT_FOUND;
5853 }
5854 }
5855 else
5856 if (strcmp(action, L("Like")) == 0) { /** **/
5857 xs *msg = msg_admiration(&snac, id, "Like");
5858
5859 if (msg != NULL) {
5860 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
5861 enqueue_message(&snac, msg);
5862 }
5863 }
5864 else
5865 if (strcmp(action, L("Boost")) == 0) { /** **/
5866 xs *msg = msg_admiration(&snac, id, "Announce");
5867
5868 if (msg != NULL) {
5869 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
5870 enqueue_message(&snac, msg);
5871 }
5872 }
5873 else
5874 if (strcmp(action, L("Unlike")) == 0) { /** **/
5875 xs *msg = msg_repulsion(&snac, id, "Like");
5876
5877 if (msg != NULL) {
5878 object_user_cache_del(&snac, id, "admire");
5879 enqueue_message(&snac, msg);
5880 }
5881 }
5882 else
5883 if (strcmp(action, L("Unboost")) == 0) { /** **/
5884 xs *msg = msg_repulsion(&snac, id, "Announce");
5885
5886 if (msg != NULL) {
5887 object_user_cache_del(&snac, id, "admire");
5888 enqueue_message(&snac, msg);
5889 }
5890 }
5891 else
5892 if (strcmp(action, L("MUTE")) == 0) { /** **/
5893 mute(&snac, actor);
5894 }
5895 else
5896 if (strcmp(action, L("Unmute")) == 0) { /** **/
5897 unmute(&snac, actor);
5898 }
5899 else
5900 if (strcmp(action, L("Hide")) == 0) { /** **/
5901 if (is_draft(&snac, id))
5902 draft_del(&snac, id);
5903 else
5904 if (is_scheduled(&snac, id))
5905 schedule_del(&snac, id);
5906 else
5907 hide(&snac, id);
5908 }
5909 else
5910 if (strcmp(action, L("Limit")) == 0) { /** **/
5911 limit(&snac, actor);
5912 }
5913 else
5914 if (strcmp(action, L("Unlimit")) == 0) { /** **/
5915 unlimit(&snac, actor);
5916 }
5917 else
5918 if (strcmp(action, L("Mark as cool")) == 0) { /** **/
5919 cool(&snac, actor, OP_ADD);
5920 }
5921 else
5922 if (strcmp(action, L("Unmark as cool")) == 0) { /** **/
5923 cool(&snac, actor, OP_DEL);
5924 }
5925 else
5926 if (strcmp(action, L("Follow")) == 0) { /** **/
5927 xs *msg = msg_follow(&snac, actor);
5928
5929 if (msg != NULL) {
5930 /* reload the actor from the message, in may be different */
5931 actor = xs_dict_get(msg, "object");
5932
5933 following_add(&snac, actor, msg);
5934
5935 enqueue_output_by_actor(&snac, msg, actor, 0);
5936 }
5937 }
5938 else
5939 if (strcmp(action, L("Unfollow")) == 0) { /** **/
5940 /* get the following object */
5941 xs *object = NULL;
5942
5943 if (valid_status(following_get(&snac, actor, &object))) {
5944 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5945
5946 following_del(&snac, actor);
5947
5948 enqueue_output_by_actor(&snac, msg, actor, 0);
5949
5950 snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
5951 }
5952 else
5953 snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5954 }
5955 else
5956 if (strcmp(action, L("Follow Group")) == 0) { /** **/
5957 xs *msg = msg_follow(&snac, group);
5958
5959 if (msg != NULL) {
5960 /* reload the group from the message, in may be different */
5961 group = xs_dict_get(msg, "object");
5962
5963 following_add(&snac, group, msg);
5964
5965 enqueue_output_by_actor(&snac, msg, group, 0);
5966 }
5967 }
5968 else
5969 if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
5970 /* get the following object */
5971 xs *object = NULL;
5972
5973 if (valid_status(following_get(&snac, group, &object))) {
5974 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5975
5976 following_del(&snac, group);
5977
5978 enqueue_output_by_actor(&snac, msg, group, 0);
5979
5980 snac_log(&snac, xs_fmt("unfollowed group %s", group));
5981 }
5982 else
5983 snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5984 }
5985 else
5986 if (strcmp(action, L("Delete")) == 0) { /** **/
5987 const char *actor_form = xs_dict_get(p_vars, "actor-form");
5988 if (actor_form != NULL) {
5989 /* delete follower */
5990 if (valid_status(follower_del(&snac, actor)))
5991 snac_log(&snac, xs_fmt("deleted follower %s", actor));
5992 else
5993 snac_log(&snac, xs_fmt("error deleting follower %s", actor));
5994 }
5995 else {
5996 /* delete an entry */
5997 if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
5998 /* it's a post by us: generate a delete */
5999 xs *msg = msg_delete(&snac, id);
6000
6001 enqueue_message(&snac, msg);
6002
6003 snac_log(&snac, xs_fmt("posted tombstone for %s", id));
6004 }
6005
6006 timeline_del(&snac, id);
6007
6008 draft_del(&snac, id);
6009
6010 schedule_del(&snac, id);
6011
6012 snac_log(&snac, xs_fmt("deleted entry %s", id));
6013 }
6014 }
6015 else
6016 if (strcmp(action, L("Pin")) == 0) { /** **/
6017 pin(&snac, id);
6018 timeline_touch(&snac);
6019 }
6020 else
6021 if (strcmp(action, L("Unpin")) == 0) { /** **/
6022 unpin(&snac, id);
6023 timeline_touch(&snac);
6024 }
6025 else
6026 if (strcmp(action, L("Bookmark")) == 0) { /** **/
6027 bookmark(&snac, id);
6028 timeline_touch(&snac);
6029 }
6030 else
6031 if (strcmp(action, L("Unbookmark")) == 0) { /** **/
6032 unbookmark(&snac, id);
6033 timeline_touch(&snac);
6034 }
6035 else
6036 if (strcmp(action, L("Approve")) == 0) { /** **/
6037 xs *fwreq = pending_get(&snac, actor);
6038
6039 if (fwreq != NULL) {
6040 xs *reply = msg_accept(&snac, fwreq, actor);
6041
6042 enqueue_message(&snac, reply);
6043
6044 if (xs_is_null(xs_dict_get(fwreq, "published"))) {
6045 /* add a date if it doesn't include one (Mastodon) */
6046 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
6047 fwreq = xs_dict_set(fwreq, "published", date);
6048 }
6049
6050 timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
6051
6052 follower_add(&snac, actor);
6053
6054 pending_del(&snac, actor);
6055
6056 snac_log(&snac, xs_fmt("new follower %s", actor));
6057 }
6058 }
6059 else
6060 if (strcmp(action, L("Discard")) == 0) { /** **/
6061 pending_del(&snac, actor);
6062 }
6063 else
6064 status = HTTP_STATUS_NOT_FOUND;
6065
6066 /* delete the cached timeline */
6067 if (status == HTTP_STATUS_SEE_OTHER)
6068 history_del(&snac, "timeline.html_");
6069 }
6070 else
6071 if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
6072 /* change of user data */
6073 const char *v;
6074 const char *p1, *p2;
6075
6076 if ((v = xs_dict_get(p_vars, "name")) != NULL)
6077 snac.config = xs_dict_set(snac.config, "name", v);
6078 if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
6079 snac.config = xs_dict_set(snac.config, "avatar", v);
6080 if ((v = xs_dict_get(p_vars, "bio")) != NULL)
6081 snac.config = xs_dict_set(snac.config, "bio", v);
6082 if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
6083 strcmp(v, "on") == 0) {
6084 snac.config = xs_dict_set(snac.config, "cw", "open");
6085 } else { /* if the checkbox is not set, the parameter is missing */
6086 snac.config = xs_dict_set(snac.config, "cw", "");
6087 }
6088 if ((v = xs_dict_get(p_vars, "email")) != NULL)
6089 snac.config = xs_dict_set(snac.config, "email", v);
6090 else
6091 snac.config = xs_dict_del(snac.config, "email");
6092 if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
6093 snac.config = xs_dict_set(snac.config, "telegram_bot", v);
6094 if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
6095 snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
6096 if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
6097 snac.config = xs_dict_set(snac.config, "ntfy_server", v);
6098 if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
6099 snac.config = xs_dict_set(snac.config, "ntfy_token", v);
6100 if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
6101 xs *days = xs_number_new(atof(v));
6102 snac.config = xs_dict_set(snac.config, "purge_days", days);
6103 }
6104 if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
6105 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
6106 else
6107 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
6108 if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
6109 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
6110 else
6111 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
6112 if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
6113 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
6114 else
6115 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
6116 if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
6117 snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
6118 else
6119 snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
6120 if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
6121 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
6122 else
6123 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
6124 if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
6125 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
6126 else
6127 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
6128 if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
6129 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
6130 else
6131 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
6132 if ((v = xs_dict_get(p_vars, "show_unlisted")) != NULL && strcmp(v, "on") == 0)
6133 snac.config = xs_dict_set(snac.config, "show_unlisted", xs_stock(XSTYPE_TRUE));
6134 else
6135 snac.config = xs_dict_set(snac.config, "show_unlisted", xs_stock(XSTYPE_FALSE));
6136 if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
6137 snac.config = xs_dict_set(snac.config, "lang", v);
6138 if ((v = xs_dict_get(p_vars, "tz")) != NULL)
6139 snac.config = xs_dict_set(snac.config, "tz", v);
6140 if ((v = xs_dict_get(p_vars, "post_langs")) != NULL)
6141 snac.config = xs_dict_set(snac.config, "post_langs", v);
6142 if ((v = xs_dict_get(p_vars, "excluded_langs")) != NULL)
6143 snac.config = xs_dict_set(snac.config, "excluded_langs", v);
6144 else
6145 snac.config = xs_dict_del(snac.config, "excluded_langs");
6146
6147 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
6148 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
6149
6150 snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", ""));
6151
6152 if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
6153 snac.config = xs_dict_set(snac.config, "metadata", v);
6154
6155 snac.config = xs_dict_set(snac.config, "text_browser_uas", xs_dict_get_def(p_vars, "text_browser_uas", ""));
6156
6157 /* uploads */
6158 const char *uploads[] = { "avatar", "header", NULL };
6159 int n;
6160 for (n = 0; uploads[n]; n++) {
6161 xs *var_name = xs_fmt("%s_file", uploads[n]);
6162
6163 const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
6164 if (xs_type(uploaded_file) == XSTYPE_LIST) {
6165 const char *fn = xs_list_get(uploaded_file, 0);
6166
6167 if (fn && *fn) {
6168 const char *mimetype = xs_mime_by_ext(fn);
6169
6170 if (xs_startswith(mimetype, "image/")) {
6171 const char *ext = strrchr(fn, '.');
6172 xs *hash = xs_md5_hex(fn, strlen(fn));
6173 xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
6174 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
6175 int fo = xs_number_get(xs_list_get(uploaded_file, 1));
6176 int fs = xs_number_get(xs_list_get(uploaded_file, 2));
6177
6178 /* store */
6179 static_put(&snac, id, payload + fo, fs);
6180
6181 snac.config = xs_dict_set(snac.config, uploads[n], url);
6182 }
6183 }
6184 }
6185 }
6186
6187 /* delete images by removing url from user.json */
6188 for (n = 0; uploads[n]; n++) {
6189 xs *var_name = xs_fmt("%s_delete", uploads[n]);
6190 const char *delete_var = xs_dict_get(p_vars, var_name);
6191
6192 if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
6193 snac.config = xs_dict_set(snac.config, uploads[n], "");
6194 }
6195 }
6196
6197 /* password change? */
6198 if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
6199 (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
6200 *p1 && strcmp(p1, p2) == 0) {
6201 xs *pw = hash_password(snac.uid, p1, NULL);
6202 snac.config = xs_dict_set(snac.config, "passwd", pw);
6203 }
6204
6205 user_persist(&snac, 1);
6206
6207 status = HTTP_STATUS_SEE_OTHER;
6208 }
6209 else
6210 if (p_path && strcmp(p_path, "admin/filter-notifications") == 0) { /** **/
6211 notify_filter(&snac, p_vars);
6212 user_persist(&snac, 0);
6213 status = HTTP_STATUS_SEE_OTHER;
6214 }
6215 else
6216 if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
6217 notify_clear(&snac);
6218 timeline_touch(&snac);
6219
6220 status = HTTP_STATUS_SEE_OTHER;
6221 }
6222 else
6223 if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
6224 const char *irt = xs_dict_get(p_vars, "irt");
6225 const char *opt = xs_dict_get(p_vars, "question");
6226 const char *actor = xs_dict_get(p_vars, "actor");
6227
6228 xs *ls = NULL;
6229
6230 /* multiple choices? */
6231 if (xs_type(opt) == XSTYPE_LIST)
6232 ls = xs_dup(opt);
6233 else
6234 if (xs_type(opt) == XSTYPE_STRING) {
6235 ls = xs_list_new();
6236 ls = xs_list_append(ls, opt);
6237 }
6238
6239 const xs_str *v;
6240 int c = 0;
6241
6242 while (xs_list_next(ls, &v, &c)) {
6243 xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL);
6244
6245 /* set the option */
6246 msg = xs_dict_append(msg, "name", v);
6247
6248 /* delete the content */
6249 msg = xs_dict_del(msg, "content");
6250
6251 xs *c_msg = msg_create(&snac, msg);
6252
6253 enqueue_message(&snac, c_msg);
6254
6255 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
6256 }
6257
6258 if (ls != NULL) {
6259 /* get the poll object */
6260 xs *poll = NULL;
6261
6262 if (valid_status(object_get(irt, &poll))) {
6263 const char *date = xs_dict_get(poll, "endTime");
6264 if (xs_is_null(date))
6265 date = xs_dict_get(poll, "closed");
6266
6267 if (!xs_is_null(date)) {
6268 time_t t = xs_parse_iso_date(date, 0) - time(NULL);
6269
6270 /* request the poll when it's closed;
6271 Pleroma does not send and update when the poll closes */
6272 enqueue_object_request(&snac, irt, t + 2);
6273 }
6274 }
6275 }
6276
6277 status = HTTP_STATUS_SEE_OTHER;
6278 }
6279 else
6280 if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
6281 const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
6282
6283 if (xs_is_null(followed_hashtags))
6284 followed_hashtags = "";
6285
6286 if (xs_is_string(followed_hashtags)) {
6287 xs *new_hashtags = xs_list_new();
6288 xs *l = xs_split(followed_hashtags, "\n");
6289 const char *v;
6290
6291 xs_list_foreach(l, v) {
6292 xs *s1 = xs_strip_i(xs_dup(v));
6293 s1 = xs_replace_i(s1, " ", "");
6294
6295 if (*s1 == '\0')
6296 continue;
6297
6298 xs *s2 = NULL;
6299
6300 if (xs_startswith(s1, "https:/"))
6301 s2 = xs_dup(s1);
6302 else {
6303 s2 = xs_utf8_tolower(s1);
6304
6305 if (*s2 != '#')
6306 s2 = xs_str_prepend_i(s2, "#");
6307 }
6308
6309 new_hashtags = xs_list_append(new_hashtags, s2);
6310 }
6311
6312 snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
6313 user_persist(&snac, 0);
6314 }
6315
6316 status = HTTP_STATUS_SEE_OTHER;
6317 }
6318 else
6319 if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/
6320 const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags");
6321
6322 if (xs_is_null(hashtags))
6323 hashtags = "";
6324
6325 if (xs_is_string(hashtags)) {
6326 xs *new_hashtags = xs_list_new();
6327 xs *l = xs_split(hashtags, "\n");
6328 const char *v;
6329
6330 xs_list_foreach(l, v) {
6331 xs *s1 = xs_strip_i(xs_dup(v));
6332 s1 = xs_replace_i(s1, " ", "");
6333
6334 if (*s1 == '\0')
6335 continue;
6336
6337 xs *s2 = xs_utf8_tolower(s1);
6338 if (*s2 != '#')
6339 s2 = xs_str_prepend_i(s2, "#");
6340
6341 new_hashtags = xs_list_append(new_hashtags, s2);
6342 }
6343
6344 snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags);
6345 user_persist(&snac, 0);
6346 }
6347
6348 status = HTTP_STATUS_SEE_OTHER;
6349 }
6350 else
6351 if (p_path && strcmp(p_path, "admin/muted-words") == 0) {
6352 const char *words = xs_dict_get(p_vars, "muted_words");
6353
6354 if (xs_is_string(words)) {
6355 xs *new_words = xs_list_new();
6356 xs *l = xs_split(words, "\n");
6357 const char *v;
6358
6359 xs_list_foreach(l, v) {
6360 xs *s1 = xs_strip_i(xs_dup(v));
6361 s1 = xs_replace_i(s1, " ", "");
6362
6363 if (*s1 == '\0')
6364 continue;
6365
6366 xs *s2 = xs_utf8_tolower(s1);
6367
6368 new_words = xs_list_insert_sorted(new_words, s2);
6369 }
6370
6371 snac.config = xs_dict_set(snac.config, "muted_words", new_words);
6372 user_persist(&snac, 0);
6373 }
6374
6375 status = HTTP_STATUS_SEE_OTHER;
6376 }
6377
6378 if (status == HTTP_STATUS_SEE_OTHER) {
6379 const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
6380
6381 if (xs_is_string(hard_redir))
6382 *body = xs_dup(hard_redir);
6383 else {
6384 const char *redir = xs_dict_get(p_vars, "redir");
6385
6386 if (xs_is_null(redir))
6387 redir = "top";
6388
6389 *body = xs_fmt("%s/admin#%s", snac.actor, redir);
6390 }
6391
6392 *b_size = strlen(*body);
6393 }
6394
6395 user_free(&snac);
6396
6397 return status;
6398}