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