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