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