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