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