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