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