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