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