2.86
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 xs *actors = xs_list_new();
2464 actors = xs_list_append(actors, actor);
2465 char me = actor && user && strcmp(actor, user->actor) == 0;
2466 int count = 1;
2467
2468 if (contentl) {
2469 count = atoi(xs_list_get(contentl, 0)) + 1;
2470 const xs_list *actorsc = xs_list_get(contentl, 1);
2471 if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
2472 me = 1;
2473
2474 if (xs_list_in(actorsc, actor) != -1) {
2475 xs_free(actors);
2476 actors = xs_dup(actorsc);
2477 }
2478 else
2479 actors = xs_list_cat(actors, actorsc);
2480 }
2481
2482 xs *fl = xs_list_new();
2483 xs *c1 = xs_fmt("%d", count);
2484 xs *c2 = xs_fmt("%d", me);
2485 fl = xs_list_append(fl, c1, actors, c2);
2486 sfrl = xs_dict_append(sfrl, content, fl);
2487 }
2488 }
2489
2490 c = 0;
2491
2492 while (xs_list_next(rl, &k, &c)) {
2493 xs *m = NULL;
2494 if (valid_status(object_get_by_md5(k, &m))) {
2495 const xs_dict *tag = xs_dict_get(m, "tag");
2496 const xs_dict *ide = xs_dict_get(m, "id");
2497
2498 const char *content = xs_dict_get(m, "content");
2499 const char *shortname;
2500 shortname = xs_dict_get(m, "content");
2501
2502 const xs_list *items = xs_dict_get(sfrl, content);
2503
2504 if (!xs_is_null(items)) {
2505 const char *nb = xs_list_get(items, 0);
2506 const xs_list *actors = xs_list_get(items, 1);
2507 const char me = *xs_list_get(items, 2) == '1';
2508
2509 is_emoji = 1;
2510
2511 xs *al = xs_join(actors, ",\n\t");
2512 xs *act = atoi(nb) > 1 ?
2513 xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
2514 xs_dup(xs_dict_get(m, "actor"));
2515
2516 xs *class = xs_list_new();
2517 class = xs_list_append(class, "snac-reaction");
2518
2519 xs_html *ret = NULL;
2520 if (tag && shortname) {
2521 xs *cl = xs_list_new();
2522 cl = xs_list_append(cl, "snac-reaction-image");
2523 xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
2524
2525 emoji = xs_strip_chars_i(emoji, ":");
2526
2527 if (me)
2528 class = xs_list_append(class, "snac-reacted");
2529
2530 xs *l1 = xs_join(class, " ");
2531 ret = xs_html_tag("button",
2532 xs_html_attr("type", "submit"),
2533 xs_html_attr("name", "action"),
2534 xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
2535 xs_html_raw(emoji),
2536 xs_html_tag("span",
2537 xs_html_raw(nb),
2538 xs_html_attr("style", "padding-left: 5px;")),
2539 xs_html_attr("title", act),
2540 xs_html_attr("class", l1));
2541
2542 if (!(ide && xs_startswith(ide, srv_baseurl)))
2543 xs_html_add(ret, xs_html_attr("disabled", "true"));
2544 }
2545 else if (shortname) {
2546 xs *sn = xs_dup(shortname);
2547 const char *sna = sn;
2548 unsigned int utf = xs_utf8_dec((const char **)&sna);
2549
2550 if (xs_is_emoji(utf)) {
2551 const char *style = "font-size: large;";
2552 if (me)
2553 class = xs_list_append(class, "snac-reacted");
2554 xs *l1 = xs_join(class, " ");
2555 xs *s1 = xs_fmt("&#%d", utf);
2556 ret = xs_html_tag("button",
2557 xs_html_attr("type", "submit"),
2558 xs_html_attr("name", "action"),
2559 xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
2560 xs_html_raw(s1),
2561 xs_html_tag("span",
2562 xs_html_raw(nb),
2563 xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
2564 xs_html_attr("title", act),
2565 xs_html_attr("class", l1),
2566 xs_html_attr("style", style));
2567 }
2568 }
2569 if (ret) {
2570 xs *s1;
2571 if (user) {
2572 xs *action = xs_fmt("%s/admin/action", user->actor);
2573 xs *form_id = xs_fmt("%s_reply_form", md5);
2574
2575 xs_html *form =
2576 xs_html_tag("form",
2577 xs_html_attr("autocomplete", "off"),
2578 xs_html_attr("method", "post"),
2579 xs_html_attr("action", action),
2580 xs_html_attr("enctype", "multipart/form-data"),
2581 xs_html_attr("style", "display: inline-flex;"
2582 "vertical-align: middle;"),
2583 xs_html_attr("id", form_id),
2584 xs_html_sctag("input",
2585 xs_html_attr("type", "hidden"),
2586 xs_html_attr("name", "id"),
2587 xs_html_attr("value", id)),
2588 xs_html_sctag("input",
2589 xs_html_attr("type", "hidden"),
2590 xs_html_attr("name", "eid"),
2591 xs_html_attr("value", shortname)),
2592 ret);
2593 s1 = xs_html_render(form);
2594 }
2595 else
2596 s1 = xs_html_render(ret);
2597
2598 ls = xs_list_append(ls, s1);
2599 sfrl = xs_dict_del(sfrl, content);
2600 }
2601 }
2602 }
2603 }
2604
2605 c = 0;
2606
2607 xs_html *emoji_div;
2608 if (xs_list_len(ls) > 0) {
2609 emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
2610 xs_html_attr("class", "snac-reaction-div"));
2611
2612 while (ls != NULL && xs_list_next(ls, &k, &c))
2613 xs_html_add(emoji_div, xs_html_raw(k));
2614
2615 xs_html_add(snac_content_wrap, emoji_div);
2616 }
2617
2618 }
2619
2620 {
2621 /** build the content string **/
2622 const char *content = xs_dict_get(msg, "content");
2623
2624 if (xs_type(content) != XSTYPE_STRING) {
2625 if (!xs_is_null(content))
2626 srv_archive_error("unexpected_content_xstype",
2627 "content field type", xs_stock(XSTYPE_DICT), msg);
2628
2629 content = "";
2630 }
2631
2632 /* skip ugly line breaks at the beginning */
2633 while (xs_startswith(content, "<br>"))
2634 content += 4;
2635
2636 xs *c = sanitize(content);
2637
2638 /* do some tweaks to the content */
2639 c = xs_replace_i(c, "\r", "");
2640
2641 while (xs_endswith(c, "<br><br>"))
2642 c = xs_crop_i(c, 0, -4);
2643
2644 c = xs_replace_i(c, "<br><br>", "<p>");
2645
2646 if (is_emoji == 0)
2647 c = xs_str_cat(c, "<p>");
2648
2649 /* replace the :shortnames: */
2650 c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
2651
2652 /* Peertube videos content is in markdown */
2653 const char *mtype = xs_dict_get(msg, "mediaType");
2654 if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
2655 /* a full conversion could be better */
2656 c = xs_replace_i(c, "\r", "");
2657 c = xs_replace_i(c, "\n", "<br>");
2658 }
2659
2660 /* c contains sanitized HTML */
2661 xs_html_add(snac_content,
2662 xs_html_raw(c));
2663
2664 /* quoted post */
2665 const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
2666
2667 if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
2668 xs *quoted_post = NULL;
2669
2670 if (valid_status(object_get(quoted_id, "ed_post))) {
2671 xs_html_add(snac_content,
2672 xs_html_tag("blockquote",
2673 xs_html_attr("class", "snac-quoted-post"),
2674 html_entry(user, quoted_post, 1, 1, NULL, 1)));
2675 }
2676 else
2677 if (user)
2678 enqueue_object_request(user, quoted_id, 0);
2679 }
2680 }
2681
2682 if (strcmp(type, "Question") == 0) { /** question content **/
2683 const xs_list *oo = xs_dict_get(msg, "oneOf");
2684 const xs_list *ao = xs_dict_get(msg, "anyOf");
2685 const xs_list *p;
2686 const xs_dict *v;
2687 int closed = 0;
2688 const char *f_closed = NULL;
2689
2690 xs_html *poll = xs_html_tag("div", NULL);
2691
2692 if (read_only)
2693 closed = 1; /* non-identified page; show as closed */
2694 else
2695 if (user && is_msg_mine(user, id))
2696 closed = 1; /* we questioned; closed for us */
2697 else
2698 if (user && was_question_voted(user, id))
2699 closed = 1; /* we already voted; closed for us */
2700
2701 if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
2702 /* it has a closed date... but is it in the past? */
2703 time_t t0 = time(NULL);
2704 time_t t1 = xs_parse_iso_date(f_closed, 0);
2705
2706 if (t1 < t0)
2707 closed = 2;
2708 }
2709
2710 /* get the appropriate list of options */
2711 p = oo != NULL ? oo : ao;
2712
2713 if (closed || user == NULL) {
2714 /* closed poll */
2715 xs_html *poll_result = xs_html_tag("table",
2716 xs_html_attr("class", "snac-poll-result"));
2717 int c = 0;
2718
2719 while (xs_list_next(p, &v, &c)) {
2720 const char *name = xs_dict_get(v, "name");
2721 const xs_dict *replies = xs_dict_get(v, "replies");
2722
2723 if (xs_is_string(name) && xs_is_dict(replies)) {
2724 const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
2725
2726 if (xs_is_string(ti))
2727 xs_html_add(poll_result,
2728 xs_html_tag("tr",
2729 xs_html_tag("td",
2730 xs_html_text(name),
2731 xs_html_text(":")),
2732 xs_html_tag("td",
2733 xs_html_text(ti))));
2734 }
2735 }
2736
2737 xs_html_add(poll,
2738 poll_result);
2739 }
2740 else {
2741 /* poll still active */
2742 xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
2743 xs_html *form;
2744 xs_html *poll_form = xs_html_tag("div",
2745 xs_html_attr("class", "snac-poll-form"),
2746 form = xs_html_tag("form",
2747 xs_html_attr("autocomplete", "off"),
2748 xs_html_attr("method", "post"),
2749 xs_html_attr("action", vote_action),
2750 xs_html_sctag("input",
2751 xs_html_attr("type", "hidden"),
2752 xs_html_attr("name", "actor"),
2753 xs_html_attr("value", actor)),
2754 xs_html_sctag("input",
2755 xs_html_attr("type", "hidden"),
2756 xs_html_attr("name", "irt"),
2757 xs_html_attr("value", id))));
2758
2759 int c = 0;
2760 while (xs_list_next(p, &v, &c)) {
2761 const char *name = xs_dict_get(v, "name");
2762 const xs_dict *replies = xs_dict_get(v, "replies");
2763
2764 if (name) {
2765 char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
2766
2767 xs_html *btn = xs_html_sctag("input",
2768 xs_html_attr("id", name),
2769 xs_html_attr("value", name),
2770 xs_html_attr("name", "question"));
2771
2772 if (!xs_is_null(oo)) {
2773 xs_html_add(btn,
2774 xs_html_attr("type", "radio"),
2775 xs_html_attr("required", "required"));
2776 }
2777 else
2778 xs_html_add(btn,
2779 xs_html_attr("type", "checkbox"));
2780
2781 xs_html_add(form,
2782 btn,
2783 xs_html_text(" "),
2784 xs_html_tag("span",
2785 xs_html_attr("title", ti),
2786 xs_html_text(name)),
2787 xs_html_sctag("br", NULL));
2788 }
2789 }
2790
2791 xs_html_add(form,
2792 xs_html_tag("p", NULL),
2793 xs_html_sctag("input",
2794 xs_html_attr("type", "submit"),
2795 xs_html_attr("class", "button"),
2796 xs_html_attr("value", L("Vote"))));
2797
2798 xs_html_add(poll,
2799 poll_form);
2800 }
2801
2802 /* if it's *really* closed, say it */
2803 if (closed == 2) {
2804 xs_html_add(poll,
2805 xs_html_tag("p",
2806 xs_html_text(L("Closed"))));
2807 }
2808 else {
2809 /* show when the poll closes */
2810 const char *end_time = xs_dict_get(msg, "endTime");
2811
2812 /* Pleroma does not have an endTime field;
2813 it has a closed time in the future */
2814 if (xs_is_null(end_time))
2815 end_time = xs_dict_get(msg, "closed");
2816
2817 if (!xs_is_null(end_time)) {
2818 time_t t0 = time(NULL);
2819 time_t t1 = xs_parse_iso_date(end_time, 0);
2820
2821 if (t1 > 0 && t1 > t0) {
2822 time_t diff_time = t1 - t0;
2823 xs *tf = xs_str_time_diff(diff_time);
2824 char *p = tf;
2825
2826 /* skip leading zeros */
2827 for (; *p == '0' || *p == ':'; p++);
2828
2829 xs_html_add(poll,
2830 xs_html_tag("p",
2831 xs_html_text(L("Closes in")),
2832 xs_html_text(" "),
2833 xs_html_text(p)));
2834 }
2835 }
2836 }
2837
2838 xs_html_add(snac_content,
2839 poll);
2840 }
2841
2842 /** attachments **/
2843 xs *attach = get_attachments(msg);
2844
2845 {
2846 /* make custom css for attachments easier */
2847 xs_html *content_attachments = xs_html_tag("div",
2848 xs_html_attr("class", "snac-content-attachments"));
2849
2850 xs_html_add(snac_content,
2851 content_attachments);
2852
2853 const char *content = xs_dict_get(msg, "content");
2854
2855 int c = 0;
2856 const xs_dict *a;
2857 while (xs_list_next(attach, &a, &c)) {
2858 const char *type = xs_dict_get(a, "type");
2859 const char *o_href = xs_dict_get(a, "href");
2860 const char *name = xs_dict_get(a, "name");
2861
2862 if (!xs_is_string(type) || !xs_is_string(o_href))
2863 continue;
2864
2865 /* if this URL is already in the post content, skip */
2866 if (content && xs_str_in(content, o_href) != -1)
2867 continue;
2868
2869 if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
2870 continue;
2871
2872 /* do this attachment include an icon? */
2873 const xs_dict *icon = xs_dict_get(a, "icon");
2874 if (xs_type(icon) == XSTYPE_DICT) {
2875 const char *icon_mtype = xs_dict_get(icon, "mediaType");
2876 const char *icon_url = xs_dict_get(icon, "url");
2877
2878 if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
2879 xs_html_add(content_attachments,
2880 xs_html_tag("a",
2881 xs_html_attr("href", icon_url),
2882 xs_html_attr("target", "_blank"),
2883 xs_html_sctag("img",
2884 xs_html_attr("loading", "lazy"),
2885 xs_html_attr("src", icon_url))));
2886 }
2887 }
2888
2889 xs *href = make_url(o_href, proxy, 0);
2890
2891 if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
2892 xs_html_add(content_attachments,
2893 xs_html_tag("a",
2894 xs_html_attr("href", href),
2895 xs_html_attr("target", "_blank"),
2896 xs_html_sctag("img",
2897 xs_html_attr("loading", "lazy"),
2898 xs_html_attr("src", href),
2899 xs_html_attr("alt", name),
2900 xs_html_attr("title", name))));
2901 }
2902 else
2903 if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
2904 xs_html_add(content_attachments,
2905 xs_html_tag("video",
2906 xs_html_attr("preload", "none"),
2907 xs_html_attr("style", "width: 100%"),
2908 xs_html_attr("class", "snac-embedded-video"),
2909 xs_html_attr("controls", NULL),
2910 xs_html_attr("src", href),
2911 xs_html_text(L("Video")),
2912 xs_html_text(": "),
2913 xs_html_tag("a",
2914 xs_html_attr("href", href),
2915 xs_html_text(name))));
2916 }
2917 else
2918 if (xs_startswith(type, "audio/")) {
2919 xs_html_add(content_attachments,
2920 xs_html_tag("audio",
2921 xs_html_attr("preload", "none"),
2922 xs_html_attr("style", "width: 100%"),
2923 xs_html_attr("class", "snac-embedded-audio"),
2924 xs_html_attr("controls", NULL),
2925 xs_html_attr("src", href),
2926 xs_html_text(L("Audio")),
2927 xs_html_text(": "),
2928 xs_html_tag("a",
2929 xs_html_attr("href", href),
2930 xs_html_text(name))));
2931 }
2932 else
2933 if (strcmp(type, "Link") == 0) {
2934 xs_html_add(content_attachments,
2935 xs_html_tag("p",
2936 xs_html_tag("a",
2937 xs_html_attr("href", o_href),
2938 xs_html_text(href))));
2939
2940 /* do not generate an Alt... */
2941 name = NULL;
2942 }
2943 else {
2944 xs *d_href = xs_dup(o_href);
2945 if (strlen(d_href) > 64) {
2946 d_href[64] = '\0';
2947 d_href = xs_str_cat(d_href, "...");
2948 }
2949
2950 xs_html_add(content_attachments,
2951 xs_html_tag("p",
2952 xs_html_tag("a",
2953 xs_html_attr("href", o_href),
2954 xs_html_text(L("Attachment")),
2955 xs_html_text(": "),
2956 xs_html_text(d_href))));
2957
2958 /* do not generate an Alt... */
2959 name = NULL;
2960 }
2961
2962 if (name != NULL && *name) {
2963 xs_html_add(content_attachments,
2964 xs_html_tag("p",
2965 xs_html_attr("class", "snac-alt-text"),
2966 xs_html_tag("details",
2967 xs_html_tag("summary",
2968 xs_html_text(L("Alt..."))),
2969 xs_html_text(name))));
2970 }
2971 }
2972 }
2973
2974 /* has this message an audience (i.e., comes from a channel or community)? */
2975 const char *audience = xs_dict_get(msg, "audience");
2976 if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
2977 xs_html *au_tag = xs_html_tag("p",
2978 xs_html_text("("),
2979 xs_html_tag("a",
2980 xs_html_attr("href", audience),
2981 xs_html_attr("title", L("Source channel or community")),
2982 xs_html_text(audience)),
2983 xs_html_text(")"));
2984
2985 xs_html_add(snac_content_wrap,
2986 au_tag);
2987 }
2988
2989 /* does it have a location? */
2990 const xs_dict *location = xs_dict_get(msg, "location");
2991 if (xs_type(location) == XSTYPE_DICT) {
2992 const xs_number *latitude = xs_dict_get(location, "latitude");
2993 const xs_number *longitude = xs_dict_get(location, "longitude");
2994 const char *name = xs_dict_get(location, "name");
2995 const char *address = xs_dict_get(location, "address");
2996 xs *label_list = xs_list_new();
2997
2998 if (xs_type(name) == XSTYPE_STRING)
2999 label_list = xs_list_append(label_list, name);
3000 if (xs_type(address) == XSTYPE_STRING)
3001 label_list = xs_list_append(label_list, address);
3002
3003 if (xs_list_len(label_list)) {
3004 const char *url = xs_dict_get(location, "url");
3005 xs *label = xs_join(label_list, ", ");
3006
3007 if (xs_type(url) == XSTYPE_STRING) {
3008 xs_html_add(snac_content_wrap,
3009 xs_html_tag("p",
3010 xs_html_text(L("Location: ")),
3011 xs_html_tag("a",
3012 xs_html_attr("href", url),
3013 xs_html_attr("target", "_blank"),
3014 xs_html_text(label))));
3015 }
3016 else
3017 if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
3018 xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
3019 xs_number_str(latitude), xs_number_str(longitude));
3020
3021 xs_html_add(snac_content_wrap,
3022 xs_html_tag("p",
3023 xs_html_text(L("Location: ")),
3024 xs_html_tag("a",
3025 xs_html_attr("href", url),
3026 xs_html_attr("target", "_blank"),
3027 xs_html_text(label))));
3028 }
3029 else
3030 xs_html_add(snac_content_wrap,
3031 xs_html_tag("p",
3032 xs_html_text(L("Location: ")),
3033 xs_html_text(label)));
3034 }
3035 }
3036
3037 if (strcmp(type, "Event") == 0) { /** Event start and end times **/
3038 const char *s_time = xs_dict_get(msg, "startTime");
3039
3040 if (xs_is_string(s_time) && strlen(s_time) > 20) {
3041 const char *e_time = xs_dict_get(msg, "endTime");
3042 const char *tz = xs_dict_get(msg, "timezone");
3043
3044 xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
3045 xs *e = NULL;
3046
3047 if (xs_is_string(e_time) && strlen(e_time) > 20)
3048 e = xs_replace_i(xs_dup(e_time), "T", " ");
3049
3050 /* if the event has a timezone, crop the offsets */
3051 if (xs_is_string(tz)) {
3052 s = xs_crop_i(s, 0, 19);
3053
3054 if (e)
3055 e = xs_crop_i(e, 0, 19);
3056 }
3057 else
3058 tz = "";
3059
3060 /* if start and end share the same day, crop it from the end */
3061 if (e && memcmp(s, e, 11) == 0)
3062 e = xs_crop_i(e, 11, 0);
3063
3064 if (e)
3065 s = xs_str_cat(s, " / ", e);
3066
3067 if (*tz)
3068 s = xs_str_cat(s, " (", tz, ")");
3069
3070 /* replace ugly decimals */
3071 s = xs_replace_i(s, ".000", "");
3072
3073 xs_html_add(snac_content_wrap,
3074 xs_html_tag("p",
3075 xs_html_text(L("Time: ")),
3076 xs_html_text(s)));
3077 }
3078 }
3079
3080 /* show all hashtags that has not been shown previously in the content */
3081 const xs_list *tags = xs_dict_get(msg, "tag");
3082 const char *o_content = xs_dict_get_def(msg, "content", "");
3083
3084 if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
3085 xs *content = xs_utf8_to_lower(o_content);
3086 const xs_dict *tag;
3087
3088 xs_html *add_hashtags = xs_html_tag("ul",
3089 xs_html_attr("class", "snac-more-hashtags"));
3090
3091 xs_list_foreach(tags, tag) {
3092 const char *type = xs_dict_get(tag, "type");
3093
3094 if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
3095 const char *o_href = xs_dict_get(tag, "href");
3096 const char *name = xs_dict_get(tag, "name");
3097
3098 if (xs_is_string(o_href) && xs_is_string(name)) {
3099 xs *href = xs_utf8_to_lower(o_href);
3100
3101 if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
3102 /* not in the content: add here */
3103 xs_html_add(add_hashtags,
3104 xs_html_tag("li",
3105 xs_html_tag("a",
3106 xs_html_attr("href", href),
3107 xs_html_text(name),
3108 xs_html_text(" "))));
3109 }
3110 }
3111 }
3112 }
3113
3114 xs_html_add(snac_content_wrap,
3115 add_hashtags);
3116 }
3117
3118 /** controls **/
3119
3120 if (!read_only && user) {
3121 xs_html_add(entry,
3122 html_entry_controls(user, actor, msg, md5));
3123 }
3124
3125 /** children **/
3126 if (!hide_children) {
3127 xs *children = object_children(id);
3128 int left = xs_list_len(children);
3129
3130 if (left) {
3131 xs_html *ch_details = xs_html_tag("details",
3132 xs_html_attr(collapse_threads ? "" : "open", NULL),
3133 xs_html_tag("summary",
3134 xs_html_text("...")));
3135
3136 xs_html_add(entry,
3137 ch_details);
3138
3139 xs_html *fch_container = xs_html_tag("div",
3140 xs_html_attr("class", "snac-thread-cont"));
3141
3142 xs_html_add(ch_details,
3143 fch_container);
3144
3145 xs_html *ch_container = xs_html_tag("div",
3146 xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
3147
3148 xs_html_add(ch_details,
3149 ch_container);
3150
3151 xs_html *ch_older = NULL;
3152 if (left > 3) {
3153 xs_html_add(ch_container,
3154 ch_older = xs_html_tag("details",
3155 xs_html_tag("summary",
3156 xs_html_text(L("Older...")))));
3157 }
3158
3159 int ctxt = 0;
3160 const char *cmd5;
3161 int cnt = 0;
3162 int o_cnt = 0;
3163 int f_cnt = 0;
3164
3165 /* get the first child */
3166 xs_list_next(children, &cmd5, &ctxt);
3167 xs *f_chd = NULL;
3168
3169 if (user)
3170 timeline_get_by_md5(user, cmd5, &f_chd);
3171 else
3172 object_get_by_md5(cmd5, &f_chd);
3173
3174 if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
3175 const char *p_author = get_atto(msg);
3176 const char *author = get_atto(f_chd);
3177
3178 /* is the first child from the same author? */
3179 if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
3180 /* then, don't add it to the children container,
3181 so that it appears unindented just before the parent
3182 like a fucking Twitter-like thread */
3183 xs_html_add(fch_container,
3184 html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children));
3185
3186 cnt++;
3187 f_cnt++;
3188 left--;
3189 }
3190 else
3191 ctxt = 0; /* restart from the beginning */
3192 }
3193
3194 while (xs_list_next(children, &cmd5, &ctxt)) {
3195 xs *chd = NULL;
3196
3197 if (user)
3198 timeline_get_by_md5(user, cmd5, &chd);
3199 else
3200 object_get_by_md5(cmd5, &chd);
3201
3202 if (chd != NULL) {
3203 if (xs_is_null(xs_dict_get(chd, "name"))) {
3204 xs_html *che = html_entry(user, chd, read_only,
3205 level + 1, cmd5, hide_children);
3206
3207 if (che != NULL) {
3208 if (left > 3) {
3209 xs_html_add(ch_older,
3210 che);
3211
3212 o_cnt++;
3213 }
3214 else
3215 xs_html_add(ch_container,
3216 che);
3217
3218 cnt++;
3219 }
3220 }
3221
3222 left--;
3223 }
3224 else
3225 srv_debug(2, xs_fmt("cannot read child %s", cmd5));
3226 }
3227
3228 /* if no children were finally added, hide the details */
3229 if (cnt == 0)
3230 xs_html_add(ch_details,
3231 xs_html_attr("style", "display: none"));
3232
3233 if (o_cnt == 0 && ch_older)
3234 xs_html_add(ch_older,
3235 xs_html_attr("style", "display: none"));
3236
3237 if (f_cnt == 0)
3238 xs_html_add(fch_container,
3239 xs_html_attr("style", "display: none"));
3240 }
3241 }
3242
3243 /* add an invisible hr, to help differentiate between posts in text browsers */
3244 xs_html_add(entry_top,
3245 xs_html_sctag("hr",
3246 xs_html_attr("hidden", NULL)));
3247
3248 return entry_top;
3249}
3250
3251
3252xs_html *html_footer(const snac *user)
3253{
3254 return xs_html_tag("div",
3255 xs_html_attr("class", "snac-footer"),
3256 xs_html_tag("a",
3257 xs_html_attr("href", srv_baseurl),
3258 xs_html_text(L("about this site"))),
3259 xs_html_text(" - "),
3260 xs_html_text(L("powered by ")),
3261 xs_html_tag("a",
3262 xs_html_attr("href", WHAT_IS_SNAC_URL),
3263 xs_html_tag("abbr",
3264 xs_html_attr("title", "Social Networks Are Crap"),
3265 xs_html_text(USER_AGENT))));
3266}
3267
3268
3269xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3270 int skip, int show, int show_more,
3271 const char *title, const char *page,
3272 int utl, const char *error, int terse)
3273/* returns the HTML for the timeline */
3274{
3275 xs_list *p = (xs_list *)list;
3276 const char *v;
3277 double t = ftime();
3278 int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
3279
3280 xs *desc = NULL;
3281 xs *alternate = NULL;
3282
3283 if (xs_list_len(list) == 1) {
3284 /* only one element? pick the description from the source */
3285 const char *id = xs_list_get(list, 0);
3286 xs *d = NULL;
3287 object_get_by_md5(id, &d);
3288 const char *sc = xs_dict_get(d, "sourceContent");
3289 if (d && sc != NULL)
3290 desc = xs_dup(sc);
3291
3292 alternate = xs_dup(xs_dict_get(d, "id"));
3293 }
3294
3295 xs_html *head;
3296 xs_html *body;
3297
3298 if (user) {
3299 head = html_user_head(user, desc, alternate);
3300
3301 if (terse)
3302 body = xs_html_tag("body", NULL);
3303 else
3304 body = html_user_body(user, read_only);
3305 }
3306 else {
3307 head = html_instance_head();
3308 body = html_instance_body();
3309 }
3310
3311 xs_html *html = xs_html_tag("html",
3312 head,
3313 body);
3314
3315 if (user && !read_only)
3316 xs_html_add(body,
3317 html_top_controls(user));
3318
3319 if (error != NULL) {
3320 xs_html_add(body,
3321 xs_html_tag("dialog",
3322 xs_html_attr("open", NULL),
3323 xs_html_tag("p",
3324 xs_html_text(error)),
3325 xs_html_tag("form",
3326 xs_html_attr("method", "dialog"),
3327 xs_html_sctag("input",
3328 xs_html_attr("type", "submit"),
3329 xs_html_attr("value", L("Dismiss"))))));
3330 }
3331
3332 /* show links to the available lists */
3333 if (user && !read_only) {
3334 xs_html *lol = xs_html_tag("ul",
3335 xs_html_attr("class", "snac-list-of-lists"));
3336 xs_html_add(body, lol);
3337
3338 xs *lists = list_maint(user, NULL, 0); /* get list of lists */
3339
3340 int ct = 0;
3341 const char *v;
3342
3343 while (xs_list_next(lists, &v, &ct)) {
3344 const char *lname = xs_list_get(v, 1);
3345 xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
3346 xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
3347
3348 xs_html_add(lol,
3349 xs_html_tag("li",
3350 xs_html_tag("a",
3351 xs_html_attr("href", url),
3352 xs_html_attr("class", "snac-list-link"),
3353 xs_html_attr("title", ttl),
3354 xs_html_text(lname))));
3355 }
3356
3357 {
3358 /* show the list of pinned posts */
3359 xs *url = xs_fmt("%s/pinned", user->actor);
3360 xs_html_add(lol,
3361 xs_html_tag("li",
3362 xs_html_tag("a",
3363 xs_html_attr("href", url),
3364 xs_html_attr("class", "snac-list-link"),
3365 xs_html_attr("title", L("Pinned posts")),
3366 xs_html_text(L("pinned")))));
3367 }
3368
3369 {
3370 /* show the list of bookmarked posts */
3371 xs *url = xs_fmt("%s/bookmarks", user->actor);
3372 xs_html_add(lol,
3373 xs_html_tag("li",
3374 xs_html_tag("a",
3375 xs_html_attr("href", url),
3376 xs_html_attr("class", "snac-list-link"),
3377 xs_html_attr("title", L("Bookmarked posts")),
3378 xs_html_text(L("bookmarks")))));
3379 }
3380
3381 {
3382 /* show the list of drafts */
3383 xs *url = xs_fmt("%s/drafts", user->actor);
3384 xs_html_add(lol,
3385 xs_html_tag("li",
3386 xs_html_tag("a",
3387 xs_html_attr("href", url),
3388 xs_html_attr("class", "snac-list-link"),
3389 xs_html_attr("title", L("Post drafts")),
3390 xs_html_text(L("drafts")))));
3391 }
3392
3393 {
3394 /* show the list of scheduled posts */
3395 xs *url = xs_fmt("%s/sched", user->actor);
3396 xs_html_add(lol,
3397 xs_html_tag("li",
3398 xs_html_tag("a",
3399 xs_html_attr("href", url),
3400 xs_html_attr("class", "snac-list-link"),
3401 xs_html_attr("title", L("Scheduled posts")),
3402 xs_html_text(L("scheduled posts")))));
3403 }
3404
3405 /* the list of followed hashtags */
3406 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
3407
3408 if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
3409 xs_html *loht = xs_html_tag("ul",
3410 xs_html_attr("class", "snac-list-of-lists"));
3411 xs_html_add(body, loht);
3412
3413 const char *ht;
3414
3415 xs_list_foreach(followed_hashtags, ht) {
3416 xs *url = NULL;
3417
3418 if (!xs_startswith(ht, "https:/""/"))
3419 url = xs_fmt("%s/admin?q=%s", user->actor, ht);
3420 else
3421 url = xs_dup(ht);
3422
3423 url = xs_replace_i(url, "#", "%23");
3424
3425 xs_html_add(loht,
3426 xs_html_tag("li",
3427 xs_html_tag("a",
3428 xs_html_attr("href", url),
3429 xs_html_attr("class", "snac-list-link"),
3430 xs_html_text(ht))));
3431 }
3432 }
3433 }
3434
3435 xs_html_add(body,
3436 xs_html_tag("a",
3437 xs_html_attr("name", "snac-posts")));
3438
3439 xs_html *posts = xs_html_tag("div",
3440 xs_html_attr("class", "snac-posts"));
3441
3442 if (title) {
3443 xs_html_add(posts,
3444 xs_html_tag("h2",
3445 xs_html_attr("class", "snac-header"),
3446 xs_html_text(title)));
3447 }
3448
3449 xs_html_add(body, posts);
3450
3451 int mark_shown = 0;
3452
3453 while (xs_list_iter(&p, &v)) {
3454 xs *msg = NULL;
3455 int status;
3456
3457 /* "already seen" mark? */
3458 if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
3459 if (skip == 0 && !mark_shown) {
3460 xs *s = xs_fmt("%s/admin", user->actor);
3461
3462 xs_html_add(posts,
3463 xs_html_tag("div",
3464 xs_html_attr("class", "snac-no-more-unseen-posts"),
3465 xs_html_text(L("No more unseen posts")),
3466 xs_html_text(" - "),
3467 xs_html_tag("a",
3468 xs_html_attr("href", s),
3469 xs_html_text(L("Back to top")))));
3470 }
3471
3472 mark_shown = 1;
3473
3474 continue;
3475 }
3476
3477 if (utl && user && !is_pinned_by_md5(user, v))
3478 status = timeline_get_by_md5(user, v, &msg);
3479 else
3480 status = object_get_by_md5(v, &msg);
3481
3482 if (!valid_status(status))
3483 continue;
3484
3485 /* if it's an instance page, discard messages from private users */
3486 if (user == NULL && is_msg_from_private_user(msg))
3487 continue;
3488
3489 const int scope = get_msg_visibility(msg);
3490 if (user != NULL && scope != SCOPE_PUBLIC){
3491 /* is this message a non-public reply? */
3492 const char *irt = get_in_reply_to(msg);
3493
3494 /* is it a reply to something not in the storage? */
3495 if (!xs_is_null(irt) && !object_here(irt)) {
3496 /* is it for me? */
3497 const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
3498 const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
3499
3500 if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
3501 snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
3502 continue;
3503 }
3504 }
3505 }
3506 /* hide non-public posts from /instance view */
3507 if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){
3508 continue;
3509 }
3510 /* hide non-public posts viewed from outside */
3511 if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) {
3512 continue;
3513 }
3514
3515 xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
3516
3517 if (entry != NULL)
3518 xs_html_add(posts,
3519 entry);
3520 }
3521
3522 if (list && user && read_only) {
3523 /** history **/
3524 if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
3525 xs_html *ul = xs_html_tag("ul", NULL);
3526
3527 xs_html *history = xs_html_tag("div",
3528 xs_html_attr("class", "snac-history"),
3529 xs_html_tag("p",
3530 xs_html_attr("class", "snac-history-title"),
3531 xs_html_text(L("History"))),
3532 ul);
3533
3534 xs *list = history_list(user);
3535 xs_list *p = list;
3536 const char *v;
3537
3538 while (xs_list_iter(&p, &v)) {
3539 xs *fn = xs_replace(v, ".html", "");
3540 xs *url = xs_fmt("%s/h/%s", user->actor, v);
3541
3542 xs_html_add(ul,
3543 xs_html_tag("li",
3544 xs_html_tag("a",
3545 xs_html_attr("href", url),
3546 xs_html_text(fn))));
3547 }
3548
3549 xs_html_add(body,
3550 history);
3551 }
3552 }
3553
3554 {
3555 xs *s1 = xs_fmt("\n<!-- %lf seconds -->\n", ftime() - t);
3556 xs_html_add(body,
3557 xs_html_raw(s1));
3558 }
3559
3560 if (show_more) {
3561 xs *m = NULL;
3562 xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3563
3564 xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3565
3566 if (page != NULL)
3567 url = xs_str_cat(url, page);
3568
3569 if (xs_str_in(url, "?") != -1)
3570 m = xs_fmt("%s&%s", url, ss);
3571 else
3572 m = xs_fmt("%s?%s", url, ss);
3573
3574 xs_html *more_links = xs_html_tag("p",
3575 xs_html_tag("a",
3576 xs_html_attr("href", url),
3577 xs_html_attr("name", "snac-more"),
3578 xs_html_text(L("Back to top"))),
3579 xs_html_text(" - "),
3580 xs_html_tag("a",
3581 xs_html_attr("href", m),
3582 xs_html_attr("name", "snac-more"),
3583 xs_html_text(L("More..."))));
3584
3585 xs_html_add(body,
3586 more_links);
3587 }
3588
3589 xs_html_add(body,
3590 html_footer(user));
3591
3592 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3593}
3594
3595
3596xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
3597{
3598 xs_html *snac_posts;
3599 xs_html *people = xs_html_tag("div",
3600 xs_html_tag("h2",
3601 xs_html_attr("class", "snac-header"),
3602 xs_html_text(header)),
3603 snac_posts = xs_html_tag("details",
3604 xs_html_attr("open", NULL),
3605 xs_html_tag("summary",
3606 xs_html_text("..."))));
3607
3608 xs *redir = xs_fmt("%s/people", user->actor);
3609
3610 const char *actor_id;
3611
3612 xs_list_foreach(list, actor_id) {
3613 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
3614 xs *actor = NULL;
3615
3616 if (valid_status(actor_get(actor_id, &actor))) {
3617 xs_html *snac_post = xs_html_tag("div",
3618 xs_html_attr("class", "snac-post"),
3619 xs_html_tag("a",
3620 xs_html_attr("name", md5)),
3621 xs_html_tag("div",
3622 xs_html_attr("class", "snac-post-header"),
3623 html_actor_icon(user, actor, xs_dict_get(actor, "published"),
3624 NULL, NULL, -1, 1, proxy, NULL, NULL)));
3625
3626 /* content (user bio) */
3627 const char *c = xs_dict_get(actor, "summary");
3628 const xs_val *tag = xs_dict_get(actor, "tag");
3629
3630 if (!xs_is_null(c)) {
3631 xs *sc = sanitize(c);
3632 sc = replace_shortnames(sc, tag, 2, proxy);
3633
3634 xs_html *snac_content = xs_html_tag("div",
3635 xs_html_attr("class", "snac-content"));
3636
3637 if (xs_startswith(sc, "<p>"))
3638 xs_html_add(snac_content,
3639 xs_html_raw(sc)); /* already sanitized */
3640 else
3641 xs_html_add(snac_content,
3642 xs_html_tag("p",
3643 xs_html_raw(sc))); /* already sanitized */
3644
3645 xs_html_add(snac_post, snac_content);
3646 }
3647
3648 /* add user metadata */
3649 xs_html *snac_metadata = xs_html_tag("div",
3650 xs_html_attr("class", "snac-metadata"));
3651
3652 int count = 0;
3653 const xs_val *address = xs_dict_get(actor, "vcard:Address");
3654 if (xs_is_string(address)) {
3655 xs_html_add(snac_metadata,
3656 xs_html_tag("span",
3657 xs_html_attr("class", "snac-property-name"),
3658 xs_html_raw("📍 Location")),
3659 xs_html_text(":"),
3660 xs_html_raw(" "),
3661 xs_html_tag("span",
3662 xs_html_attr("class", "snac-property-value p-adr"),
3663 xs_html_text(address)),
3664 xs_html_sctag("br", NULL));
3665
3666 count++;
3667 }
3668
3669 const xs_val *birthday = xs_dict_get(actor, "vcard:bday");
3670 if (xs_is_string(birthday)) {
3671 xs_html_add(snac_metadata,
3672 xs_html_tag("span",
3673 xs_html_attr("class", "snac-property-name"),
3674 xs_html_raw("🎂 Birthday")),
3675 xs_html_text(":"),
3676 xs_html_raw(" "),
3677 xs_html_tag("time",
3678 xs_html_attr("class", "snac-property-value dt-bday"),
3679 xs_html_text(birthday)),
3680 xs_html_sctag("br", NULL));
3681
3682 count++;
3683 }
3684
3685 const xs_list *attachment = xs_dict_get(actor, "attachment");
3686 if (count > 0 && xs_list_len(attachment) > 0) {
3687 xs_html_add(snac_metadata,
3688 xs_html_sctag("hr",
3689 xs_html_attr("class", "snac-property-divider")));
3690 }
3691
3692 const xs_val *v;
3693 xs_list_foreach(attachment, v) {
3694 const char *type = xs_dict_get(v, "type");
3695 const char *name = xs_dict_get(v, "name");
3696 const char *value = xs_dict_get(v, "value");
3697
3698 if (!xs_is_null(type) && !xs_is_null(name) &&
3699 !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) {
3700 /* both the name and the value can contain emoji */
3701 xs *nam = sanitize(name);
3702 nam = replace_shortnames(nam, tag, 1, proxy);
3703
3704 /* todo: sometimes the value is transmitted as markdown and not html ._. */
3705 xs *val = sanitize(value);
3706 val = replace_shortnames(val, tag, 1, proxy);
3707
3708 /* delete <p> tags, because some software sends them */
3709 val = xs_replace_i(val, "<p>", "");
3710 val = xs_replace_i(val, "</p>", "");
3711
3712 xs_html_add(snac_metadata,
3713 xs_html_tag("span",
3714 xs_html_attr("class", "snac-property-name"),
3715 xs_html_raw(nam)),
3716 xs_html_text(":"),
3717 xs_html_raw(" "),
3718 xs_html_tag("span",
3719 xs_html_attr("class", "snac-property-value"),
3720 xs_html_raw(val)),
3721 xs_html_sctag("br", NULL));
3722
3723 count++;
3724 }
3725 }
3726
3727 if (count > 0) {
3728 xs_html_add(snac_post, snac_metadata);
3729 }
3730 else {
3731 /* free the html, by rendering it... */
3732 xs_free(xs_html_render(snac_metadata));
3733 }
3734
3735 /* buttons */
3736 xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
3737
3738 xs_html *snac_controls = xs_html_tag("div",
3739 xs_html_attr("class", "snac-controls"));
3740
3741 xs_html *form = xs_html_tag("form",
3742 xs_html_attr("autocomplete", "off"),
3743 xs_html_attr("method", "post"),
3744 xs_html_attr("action", btn_form_action),
3745 xs_html_sctag("input",
3746 xs_html_attr("type", "hidden"),
3747 xs_html_attr("name", "actor"),
3748 xs_html_attr("value", actor_id)),
3749 xs_html_sctag("input",
3750 xs_html_attr("type", "hidden"),
3751 xs_html_attr("name", "hard-redir"),
3752 xs_html_attr("value", redir)),
3753 xs_html_sctag("input",
3754 xs_html_attr("type", "hidden"),
3755 xs_html_attr("name", "actor-form"),
3756 xs_html_attr("value", "yes")));
3757
3758 xs_html_add(snac_controls, form);
3759
3760 if (following_check(user, actor_id)) {
3761 xs_html_add(form,
3762 html_button("unfollow", L("Unfollow"),
3763 L("Stop following this user's activity")));
3764
3765 if (is_limited(user, actor_id))
3766 xs_html_add(form,
3767 html_button("unlimit", L("Unlimit"),
3768 L("Allow announces (boosts) from this user")));
3769 else
3770 xs_html_add(form,
3771 html_button("limit", L("Limit"),
3772 L("Block announces (boosts) from this user")));
3773 }
3774 else {
3775 xs_html_add(form,
3776 html_button("follow", L("Follow"),
3777 L("Start following this user's activity")));
3778
3779 if (follower_check(user, actor_id))
3780 xs_html_add(form,
3781 html_button("delete", L("Delete"), L("Delete this user")));
3782 }
3783
3784 if (pending_check(user, actor_id)) {
3785 xs_html_add(form,
3786 html_button("approve", L("Approve"),
3787 L("Approve this follow request")));
3788
3789 xs_html_add(form,
3790 html_button("discard", L("Discard"), L("Discard this follow request")));
3791 }
3792
3793 if (is_muted(user, actor_id))
3794 xs_html_add(form,
3795 html_button("unmute", L("Unmute"),
3796 L("Stop blocking activities from this user")));
3797 else
3798 xs_html_add(form,
3799 html_button("mute", L("MUTE"),
3800 L("Block any activity from this user")));
3801
3802 /* the post textarea */
3803 xs *dm_div_id = xs_fmt("%s_%s_dm", md5, t);
3804 xs *dm_form_id = xs_fmt("%s_reply_form", md5);
3805
3806 xs_html_add(snac_controls,
3807 xs_html_tag("p", NULL),
3808 html_note(user, L("Direct Message..."),
3809 dm_div_id, dm_form_id,
3810 "", "",
3811 NULL, actor_id,
3812 xs_stock(XSTYPE_FALSE), "",
3813 SCOPE_MENTIONED, NULL,
3814 NULL, 0, NULL, NULL, 0, NULL, NULL),
3815 xs_html_tag("p", NULL));
3816
3817 xs_html_add(snac_post, snac_controls);
3818
3819 xs_html_add(snac_posts, snac_post);
3820 }
3821 }
3822
3823 return people;
3824}
3825
3826
3827xs_str *html_people(snac *user)
3828{
3829 const char *proxy = NULL;
3830
3831 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3832 proxy = user->actor;
3833
3834 xs *wing = following_list(user);
3835 xs *wers = follower_list(user);
3836 xs *pending = pending_list(user);
3837
3838 xs_html *lists = xs_html_tag("div",
3839 xs_html_attr("class", "snac-posts"));
3840
3841 if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
3842 xs_html_add(lists,
3843 html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
3844 }
3845
3846 xs_html_add(lists,
3847 html_people_list(user, wing, L("People you follow"), "i", proxy),
3848 html_people_list(user, wers, L("People that follow you"), "e", proxy));
3849
3850 xs_html *html = xs_html_tag("html",
3851 html_user_head(user, NULL, NULL),
3852 xs_html_add(html_user_body(user, 0),
3853 lists,
3854 html_footer(user)));
3855
3856 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3857}
3858
3859/* Filter list to display only posts by actor. We'll probably show
3860 fewer than show posts. Should we try harder to find some? */
3861xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3862 int skip, int show, int show_more, const char *page)
3863{
3864 const char *proxy = NULL;
3865 xs_list *p = (xs_list *)list;
3866 const char *v;
3867
3868 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3869 proxy = user->actor;
3870
3871 xs_html *body = html_user_body(user, 0);
3872
3873 xs_html *lists = xs_html_tag("div",
3874 xs_html_attr("class", "snac-posts"));
3875
3876 xs *foll = xs_list_append(xs_list_new(), actor);
3877
3878 xs_html_add(lists,
3879 html_people_list(user, foll, L("Contact's posts"), "p", proxy));
3880
3881 xs_html_add(body, lists);
3882
3883 while (xs_list_iter(&p, &v)) {
3884 xs *msg = NULL;
3885 int status;
3886
3887 status = timeline_get_by_md5(user, v, &msg);
3888
3889 if (!valid_status(status))
3890 continue;
3891
3892 const char *id = xs_dict_get(msg, "id");
3893 const char *by = get_atto(msg);
3894 xs *actor_md5 = NULL;
3895 xs_list *boosts = NULL;
3896 xs_list *likes = NULL;
3897 xs_list *reacts = NULL;
3898 /* Besides actor's posts, also show actor's boosts, and also
3899 posts by user with likes or reacts by actor. I.e., any
3900 actor's actions that user could have seen in the timeline
3901 or in notifications. */
3902 if (!(by && strcmp(actor, by) == 0) &&
3903 xs_list_in((boosts = object_announces(id)),
3904 (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
3905 (!(by && strcmp(user->actor, by) == 0) ||
3906 (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
3907 xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
3908 continue;
3909
3910 xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
3911
3912 if (entry != NULL)
3913 xs_html_add(lists,
3914 entry);
3915 }
3916
3917 if (show_more) {
3918 xs *m = NULL;
3919 xs *m10 = NULL;
3920 xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3921
3922 xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3923
3924 if (page != NULL)
3925 url = xs_str_cat(url, page);
3926
3927 if (xs_str_in(url, "?") != -1)
3928 m = xs_fmt("%s&%s", url, ss);
3929 else
3930 m = xs_fmt("%s?%s", url, ss);
3931
3932 m10 = xs_fmt("%s0", m);
3933
3934 xs_html *more_links = xs_html_tag("p",
3935 xs_html_tag("a",
3936 xs_html_attr("href", url),
3937 xs_html_attr("name", "snac-more"),
3938 xs_html_text(L("Back to top"))),
3939 xs_html_text(" - "),
3940 xs_html_tag("a",
3941 xs_html_attr("href", m),
3942 xs_html_attr("name", "snac-more"),
3943 xs_html_text(L("More..."))),
3944 xs_html_text(" - "),
3945 xs_html_tag("a",
3946 xs_html_attr("href", m10),
3947 xs_html_attr("name", "snac-more"),
3948 xs_html_text(L("More (x 10)..."))));
3949
3950 xs_html_add(body,
3951 more_links);
3952 }
3953
3954 xs_html *html = xs_html_tag("html",
3955 html_user_head(user, NULL, NULL),
3956 xs_html_add(body,
3957 html_footer(user)));
3958
3959 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3960}
3961
3962xs_str *html_notifications(snac *user, int skip, int show)
3963{
3964 const char *proxy = NULL;
3965
3966 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3967 proxy = user->actor;
3968
3969 xs *n_list = notify_list(user, skip, show);
3970 xs *n_time = notify_check_time(user, 0);
3971
3972 xs_html *body = html_user_body(user, 0);
3973
3974 xs_html *html = xs_html_tag("html",
3975 html_user_head(user, NULL, NULL),
3976 body);
3977
3978 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
3979
3980 xs_html_add(body,
3981 xs_html_tag("form",
3982 xs_html_attr("autocomplete", "off"),
3983 xs_html_attr("method", "post"),
3984 xs_html_attr("action", clear_all_action),
3985 xs_html_attr("id", "clear"),
3986 xs_html_sctag("input",
3987 xs_html_attr("type", "submit"),
3988 xs_html_attr("class", "snac-btn-like"),
3989 xs_html_attr("value", L("Clear all")))));
3990
3991 xs_html *noti_new = NULL;
3992 xs_html *noti_seen = NULL;
3993
3994 xs_html *posts = xs_html_tag("div",
3995 xs_html_attr("class", "snac-posts"));
3996 xs_html_add(body, posts);
3997
3998 xs_set rep;
3999 xs_set_init(&rep);
4000
4001 /* dict to store previous notification labels */
4002 xs *admiration_labels = xs_dict_new();
4003
4004 const xs_str *v;
4005
4006 xs_list_foreach(n_list, v) {
4007 xs *noti = notify_get(user, v);
4008
4009 if (noti == NULL)
4010 continue;
4011
4012 xs *obj = NULL;
4013 const char *type = xs_dict_get(noti, "type");
4014 const char *utype = xs_dict_get(noti, "utype");
4015 const char *id = xs_dict_get(noti, "objid");
4016 const char *date = xs_dict_get(noti, "date");
4017 const char *id2 = xs_dict_get_path(noti, "msg.id");
4018 xs *wrk = NULL;
4019
4020 if (xs_is_null(id))
4021 continue;
4022
4023 if (is_hidden(user, id))
4024 continue;
4025
4026 if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
4027 continue;
4028
4029 object_get(id, &obj);
4030
4031 const char *msg_id = NULL;
4032
4033 if (xs_is_dict(obj))
4034 msg_id = xs_dict_get(obj, "id");
4035
4036 const char *actor_id = xs_dict_get(noti, "actor");
4037 xs *actor = NULL;
4038 xs *a_name = NULL;
4039
4040 if (valid_status(actor_get(actor_id, &actor)))
4041 a_name = actor_name(actor, proxy);
4042 else {
4043 a_name = xs_dup(actor_id);
4044
4045 /* actor not here: request it */
4046 enqueue_actor_refresh(user, actor_id, 0);
4047 }
4048
4049 xs *label_sanitized = sanitize(type);
4050 const char *label = label_sanitized;
4051
4052 if (strcmp(type, "Create") == 0)
4053 label = L("Mention");
4054 else
4055 if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0)
4056 label = L("Finished poll");
4057 else
4058 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
4059 label = L("Unfollow");
4060 else
4061 if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
4062 const char *content = xs_dict_get_path(noti, "msg.content");
4063
4064 xs *cd = xs_dup(content);
4065 const char *sna = cd;
4066 const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
4067 unsigned int utf = xs_utf8_dec((const char **)&sna);
4068
4069 int isEmoji = 0;
4070 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
4071 isEmoji = 1;
4072
4073 if (xs_type(content) == XSTYPE_STRING) {
4074 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
4075 wrk = xs_fmt("%s (%s️)", isEmoji ? "EmojiReact" : "Like", emoji);
4076 label = wrk;
4077 }
4078 }
4079 else
4080 if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
4081 label = L("Follow Request");
4082
4083 xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
4084
4085 xs_html *this_html_label = xs_html_container(
4086 xs_html_tag("b",
4087 xs_html_raw(label),
4088 xs_html_text(" by "),
4089 xs_html_tag("a",
4090 xs_html_attr("href", actor_id),
4091 xs_html_raw(a_name))), /* a_name is already sanitized */
4092 xs_html_text(" "),
4093 xs_html_tag("time",
4094 xs_html_attr("class", "dt-published snac-pubdate"),
4095 xs_html_attr("title", date),
4096 xs_html_text(s_date)));
4097
4098 xs_html *html_label = NULL;
4099
4100 if (xs_is_string(msg_id)) {
4101 const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
4102
4103 if (xs_type(prev_label) == XSTYPE_DATA) {
4104 /* there is a previous list of admiration labels! */
4105 xs_data_get(&html_label, prev_label);
4106
4107 xs_html_add(html_label,
4108 xs_html_sctag("br", NULL),
4109 this_html_label);
4110
4111 continue;
4112 }
4113 }
4114
4115 xs_html *entry = NULL;
4116
4117 html_label = xs_html_tag("p",
4118 this_html_label);
4119
4120 /* store in the admiration labels dict */
4121 xs *pl = xs_data_new(&html_label, sizeof(html_label));
4122
4123 if (xs_is_string(msg_id))
4124 admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
4125
4126 entry = xs_html_tag("div",
4127 xs_html_attr("class", "snac-post-with-desc"),
4128 html_label);
4129
4130 if (strcmp(type, "Block") == 0) {
4131 if (actor)
4132 xs_html_add(entry,
4133 xs_html_tag("div",
4134 xs_html_attr("class", "snac-post"),
4135 html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4136 }
4137 else
4138 if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) {
4139 if (actor) {
4140 xs *action = xs_fmt("%s/admin/action", user->actor);
4141 xs_html *button = NULL;
4142
4143 if (following_check(user, actor_id))
4144 button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"));
4145 else
4146 button = html_button("follow", L("Follow"), L("Start following this user's activity"));
4147
4148 xs_html_add(entry,
4149 xs_html_tag("div",
4150 xs_html_attr("class", "snac-post"),
4151 html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL),
4152 xs_html_tag("form",
4153 xs_html_attr("method", "post"),
4154 xs_html_attr("action", action),
4155 xs_html_sctag("input",
4156 xs_html_attr("type", "hidden"),
4157 xs_html_attr("name", "actor"),
4158 xs_html_attr("value", actor_id)),
4159 button,
4160 xs_html_sctag("br", NULL))));
4161 }
4162 }
4163 else
4164 if (strcmp(type, "Move") == 0) {
4165 const xs_dict *o_msg = xs_dict_get(noti, "msg");
4166 const char *target;
4167
4168 if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) {
4169 xs *old_actor = NULL;
4170
4171 if (valid_status(actor_get(target, &old_actor))) {
4172 xs_html_add(entry,
4173 xs_html_tag("div",
4174 xs_html_attr("class", "snac-post"),
4175 html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
4176 }
4177 }
4178 }
4179 else
4180 if (obj != NULL) {
4181 xs *md5 = xs_md5_hex(id, strlen(id));
4182 xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
4183
4184 xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
4185
4186 if (h != NULL) {
4187 xs_html_add(entry,
4188 xs_html_tag("p",
4189 xs_html_tag("a",
4190 xs_html_attr("href", ctxt),
4191 xs_html_text(L("Context")))),
4192 h);
4193 }
4194 else
4195 xs_html_add(entry,
4196 xs_html_tag("p",
4197 xs_html_text(L("Location: ")),
4198 xs_html_tag("a",
4199 xs_html_attr("href", id),
4200 xs_html_text(id))));
4201 }
4202
4203 if (strcmp(v, n_time) > 0) {
4204 /* unseen notification */
4205 if (noti_new == NULL) {
4206 noti_new = xs_html_tag("div",
4207 xs_html_tag("h2",
4208 xs_html_attr("class", "snac-header"),
4209 xs_html_text(L("New"))));
4210
4211 xs_html_add(posts,
4212 noti_new);
4213 }
4214
4215 xs_html_add(noti_new,
4216 entry);
4217 }
4218 else {
4219 /* already seen notification */
4220 if (noti_seen == NULL) {
4221 noti_seen = xs_html_tag("div",
4222 xs_html_tag("h2",
4223 xs_html_attr("class", "snac-header"),
4224 xs_html_text(L("Already seen"))));
4225
4226 xs_html_add(posts,
4227 noti_seen);
4228 }
4229
4230 xs_html_add(noti_seen,
4231 entry);
4232 }
4233 }
4234
4235 if (noti_new == NULL && noti_seen == NULL)
4236 xs_html_add(body,
4237 xs_html_tag("h2",
4238 xs_html_attr("class", "snac-header"),
4239 xs_html_text(L("None"))));
4240
4241 /* add the navigation footer */
4242 xs *next_p = notify_list(user, skip + show, 1);
4243 if (xs_list_len(next_p)) {
4244 xs *url = xs_fmt("%s/notifications?skip=%d&show=%d",
4245 user->actor, skip + show, show);
4246
4247 xs_html_add(body,
4248 xs_html_tag("p",
4249 xs_html_tag("a",
4250 xs_html_attr("href", url),
4251 xs_html_text(L("More...")))));
4252 }
4253
4254 xs_set_free(&rep);
4255
4256 xs_html_add(body,
4257 html_footer(user));
4258
4259 /* set the check time to now */
4260 xs *dummy = notify_check_time(user, 1);
4261 dummy = xs_free(dummy);
4262
4263 timeline_touch(user);
4264
4265 return xs_html_render_s(html, "<!DOCTYPE html>\n");
4266}
4267
4268
4269void set_user_lang(snac *user)
4270/* sets the language dict according to user configuration */
4271{
4272 user->lang = NULL;
4273 const char *lang = xs_dict_get(user->config, "lang");
4274
4275 if (xs_is_string(lang))
4276 user->lang = xs_dict_get(srv_langs, lang);
4277}
4278
4279
4280int html_get_handler(const xs_dict *req, const char *q_path,
4281 char **body, int *b_size, char **ctype,
4282 xs_str **etag, xs_str **last_modified)
4283{
4284 const char *accept = xs_dict_get(req, "accept");
4285 int status = HTTP_STATUS_NOT_FOUND;
4286 const snac *user = NULL;
4287 snac snac;
4288 xs *uid = NULL;
4289 const char *p_path;
4290 int cache = 1;
4291 int save = 1;
4292 int proxy = 0;
4293 int terse = 0;
4294 const char *v;
4295
4296 const xs_dict *q_vars = xs_dict_get(req, "q_vars");
4297
4298 xs *l = xs_split_n(q_path, "/", 2);
4299 v = xs_list_get(l, 1);
4300
4301 if (xs_is_null(v)) {
4302 srv_log(xs_fmt("html_get_handler bad query '%s'", q_path));
4303 return HTTP_STATUS_NOT_FOUND;
4304 }
4305
4306 if (!xs_is_null(xs_dict_get(q_vars, "terse")))
4307 terse = 1;
4308
4309 if (strcmp(v, "share-bridge") == 0) {
4310 /* temporary redirect for a post */
4311 const char *login = xs_dict_get(q_vars, "login");
4312 const char *content = xs_dict_get(q_vars, "content");
4313
4314 if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) {
4315 xs *b64 = xs_base64_enc(content, strlen(content));
4316
4317 srv_log(xs_fmt("share-bridge for user '%s'", login));
4318
4319 *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64);
4320 return HTTP_STATUS_SEE_OTHER;
4321 }
4322 else
4323 return HTTP_STATUS_NOT_FOUND;
4324 }
4325 else
4326 if (strcmp(v, "auth-int-bridge") == 0) {
4327 const char *login = xs_dict_get(q_vars, "login");
4328 const char *id = xs_dict_get(q_vars, "id");
4329 const char *action = xs_dict_get(q_vars, "action");
4330
4331 if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
4332 *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
4333 srv_baseurl, login, action, id);
4334
4335 return HTTP_STATUS_SEE_OTHER;
4336 }
4337 else
4338 return HTTP_STATUS_NOT_FOUND;
4339 }
4340
4341 uid = xs_dup(v);
4342
4343 /* rss extension? */
4344 if (xs_endswith(uid, ".rss")) {
4345 uid = xs_crop_i(uid, 0, -4);
4346 p_path = ".rss";
4347 }
4348 else
4349 p_path = xs_list_get(l, 2);
4350
4351 if (!uid || !user_open(&snac, uid)) {
4352 /* invalid user */
4353 status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND;
4354 srv_debug(1, xs_fmt("html_get_handler bad user %s %d", uid, status));
4355 return status;
4356 }
4357
4358 user = &snac; /* for L() */
4359 set_user_lang(&snac);
4360
4361 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
4362 proxy = 1;
4363
4364 /* return the RSS if requested by Accept header */
4365 if (accept != NULL) {
4366 if (xs_str_in(accept, "text/xml") != -1 ||
4367 xs_str_in(accept, "application/rss+xml") != -1)
4368 p_path = ".rss";
4369 }
4370
4371 /* check if server config variable 'disable_cache' is set */
4372 if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE)
4373 cache = 0;
4374
4375 int skip = 0;
4376 const char *max_show_default = "50";
4377 int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
4378 max_show_default));
4379 int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
4380 xs_dict_get_def(srv_config, "max_timeline_entries",
4381 max_show_default)));
4382 int show = def_show;
4383
4384 if ((v = xs_dict_get(q_vars, "skip")) != NULL)
4385 skip = atoi(v), cache = 0, save = 0;
4386 if ((v = xs_dict_get(q_vars, "show")) != NULL)
4387 show = atoi(v), cache = 0, save = 0;
4388 if ((v = xs_dict_get(q_vars, "da")) != NULL) {
4389 /* user dismissed an announcement */
4390 if (login(&snac, req)) {
4391 double ts = atof(v);
4392 xs *timestamp = xs_number_new(ts);
4393 srv_log(xs_fmt("user dismissed announcements until %d", ts));
4394 snac.config = xs_dict_set(snac.config, "last_announcement", timestamp);
4395 user_persist(&snac, 0);
4396 }
4397 }
4398
4399 /* get a possible error message */
4400 const char *error = xs_dict_get(q_vars, "error");
4401 if (error != NULL)
4402 cache = 0;
4403
4404 /* a show of 0 has no sense */
4405 if (show == 0)
4406 show = def_show;
4407 if (show > max_show)
4408 show = max_show;
4409
4410 if (p_path == NULL) { /** public timeline **/
4411 xs *h = xs_str_localtime(0, "%Y-%m.html");
4412
4413 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
4414 /** empty public timeline for private users **/
4415 *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse);
4416 *b_size = strlen(*body);
4417 status = HTTP_STATUS_OK;
4418 }
4419 else
4420 if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) {
4421 snac_debug(&snac, 1, xs_fmt("serving cached local timeline"));
4422
4423 status = history_get(&snac, h, body, b_size,
4424 xs_dict_get(req, "if-none-match"), etag);
4425 }
4426 else {
4427 xs *list = NULL;
4428 int more = 0;
4429
4430 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
4431 list = timeline_simple_list(&snac, "public", skip, show, &more);
4432 else
4433 list = timeline_list(&snac, "public", skip, show, &more);
4434
4435 xs *pins = pinned_list(&snac);
4436 pins = xs_list_cat(pins, list);
4437
4438 *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse);
4439
4440 *b_size = strlen(*body);
4441 status = HTTP_STATUS_OK;
4442
4443 if (save)
4444 history_add(&snac, h, *body, *b_size, etag);
4445 }
4446 }
4447 else
4448 if (strcmp(p_path, "admin") == 0) { /** private timeline **/
4449 if (!login(&snac, req)) {
4450 *body = xs_dup(uid);
4451 status = HTTP_STATUS_UNAUTHORIZED;
4452 }
4453 else {
4454 const char *q = NULL;
4455 xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
4456 xs *url_acct = NULL;
4457
4458 if (xs_is_string(cq)) {
4459 cq = xs_strip_i(cq);
4460 q = cq;
4461 }
4462
4463 /* searching for an URL? */
4464 if (q && xs_match(q, "https://*|http://*")) {
4465 /* may by an actor; try a webfinger */
4466 xs *actor_obj = NULL;
4467
4468 if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) {
4469 /* it's an actor; do the dirty trick of changing q to the account name */
4470 q = url_acct;
4471 }
4472 else {
4473 /* bring it to the user's timeline */
4474 xs *object = NULL;
4475 int status;
4476
4477 status = activitypub_request(&snac, q, &object);
4478 snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status));
4479
4480 if (valid_status(status)) {
4481 /* got it; also request the actor */
4482 const char *attr_to = get_atto(object);
4483
4484 if (!xs_is_null(attr_to)) {
4485 status = actor_request(&snac, attr_to, &actor_obj);
4486
4487 if (valid_status(status)) {
4488 /* reset the query string to be the real id */
4489 url_acct = xs_dup(xs_dict_get(object, "id"));
4490 q = url_acct;
4491
4492 /* add the post to the timeline */
4493 if (!timeline_here(&snac, q))
4494 timeline_add(&snac, q, object);
4495 }
4496 }
4497 else {
4498 /* retry webfinger, this time with the 'official' id */
4499 const char *id = xs_dict_get(object, "id");
4500
4501 if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) &&
4502 xs_is_string(url_acct))
4503 q = url_acct;
4504 }
4505 }
4506 }
4507
4508 /* fall through */
4509 }
4510
4511 if (q && *q) {
4512 if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) {
4513 /** search account **/
4514 xs *actor = NULL;
4515 xs *acct = NULL;
4516 xs *l = xs_list_new();
4517 xs_html *page = NULL;
4518
4519 if (valid_status(webfinger_request(q, &actor, &acct))) {
4520 xs *actor_obj = NULL;
4521
4522 if (valid_status(actor_request(&snac, actor, &actor_obj))) {
4523 actor_add(actor, actor_obj);
4524
4525 /* create a people list with only one element */
4526 l = xs_list_append(l, actor);
4527
4528 xs *title = xs_fmt(L("Search results for account %s"), q);
4529
4530 page = html_people_list(&snac, l, title, "wf", NULL);
4531 }
4532 }
4533
4534 if (page == NULL) {
4535 xs *title = xs_fmt(L("Account %s not found"), q);
4536
4537 page = xs_html_tag("div",
4538 xs_html_tag("h2",
4539 xs_html_attr("class", "snac-header"),
4540 xs_html_text(title)));
4541 }
4542
4543 xs_html *html = xs_html_tag("html",
4544 html_user_head(&snac, NULL, NULL),
4545 xs_html_add(html_user_body(&snac, 0),
4546 page,
4547 html_footer(user)));
4548
4549 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
4550 *b_size = strlen(*body);
4551 status = HTTP_STATUS_OK;
4552 }
4553 else
4554 if (*q == '#') {
4555 /** search by tag **/
4556 xs *tl = tag_search(q, skip, show + 1);
4557 int more = 0;
4558 if (xs_list_len(tl) >= show + 1) {
4559 /* drop the last one */
4560 tl = xs_list_del(tl, -1);
4561 more = 1;
4562 }
4563
4564 xs *page = xs_fmt("/admin?q=%%23%s", q + 1);
4565 xs *title = xs_fmt(xs_list_len(tl) ?
4566 L("Search results for tag %s") : L("Nothing found for tag %s"), q);
4567
4568 *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse);
4569 *b_size = strlen(*body);
4570 status = HTTP_STATUS_OK;
4571 }
4572 else {
4573 /** search by content **/
4574 int to = 0;
4575 int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0"));
4576 xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to);
4577 xs *title = NULL;
4578 xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10);
4579 int tl_len = xs_list_len(tl);
4580
4581 if (to)
4582 title = xs_fmt(L("Search results for '%s' (may be more)"), q);
4583 else
4584 if (tl_len)
4585 title = xs_fmt(L("Search results for '%s'"), q);
4586 else
4587 if (skip)
4588 title = xs_fmt(L("No more matches for '%s'"), q);
4589 else
4590 title = xs_fmt(L("Nothing found for '%s'"), q);
4591
4592 *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
4593 title, page, 0, error, terse);
4594 *b_size = strlen(*body);
4595 status = HTTP_STATUS_OK;
4596 }
4597 }
4598 else {
4599 /** the private timeline **/
4600 double t = history_mtime(&snac, "timeline.html_");
4601
4602 /* if enabled by admin, return a cached page if its timestamp is:
4603 a) newer than the timeline timestamp
4604 b) newer than the start time of the server
4605 */
4606 if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) {
4607 snac_debug(&snac, 1, xs_fmt("serving cached timeline"));
4608
4609 status = history_get(&snac, "timeline.html_", body, b_size,
4610 xs_dict_get(req, "if-none-match"), etag);
4611 }
4612 else {
4613 int more = 0;
4614
4615 snac_debug(&snac, 1, xs_fmt("building timeline"));
4616
4617 xs *list = timeline_list(&snac, "private", skip, show, &more);
4618
4619 *body = html_timeline(&snac, list, 0, skip, show,
4620 more, NULL, "/admin", 1, error, terse);
4621
4622 *b_size = strlen(*body);
4623 status = HTTP_STATUS_OK;
4624
4625 if (save)
4626 history_add(&snac, "timeline.html_", *body, *b_size, etag);
4627
4628 timeline_add_mark(&snac);
4629 }
4630 }
4631 }
4632 }
4633 else
4634 if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/
4635 if (!login(&snac, req)) {
4636 *body = xs_dup(uid);
4637 status = HTTP_STATUS_UNAUTHORIZED;
4638 }
4639 else {
4640 xs *l = xs_split(p_path, "/");
4641 const char *md5 = xs_list_get(l, -1);
4642
4643 if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) {
4644 xs *list0 = xs_list_append(xs_list_new(), md5);
4645 xs *list = timeline_top_level(&snac, list0);
4646
4647 *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse);
4648 *b_size = strlen(*body);
4649 status = HTTP_STATUS_OK;
4650 }
4651 }
4652 }
4653 else
4654 if (strcmp(p_path, "people") == 0) { /** the list of people **/
4655 if (!login(&snac, req)) {
4656 *body = xs_dup(uid);
4657 status = HTTP_STATUS_UNAUTHORIZED;
4658 }
4659 else {
4660 *body = html_people(&snac);
4661 *b_size = strlen(*body);
4662 status = HTTP_STATUS_OK;
4663 }
4664 }
4665 else
4666 if (xs_startswith(p_path, "people/")) { /** a single actor **/
4667 if (!login(&snac, req)) {
4668 *body = xs_dup(uid);
4669 status = HTTP_STATUS_UNAUTHORIZED;
4670 }
4671 else {
4672 xs *actor_dict = NULL;
4673 const char *actor_id = NULL;
4674 xs *actor = NULL;
4675 xs_list *page_lst = xs_split_n(p_path, "?", 2);
4676 xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
4677 xs_list *l = xs_split_n(page, "/", 3);
4678 const char *actor_md5 = xs_list_get(l, 2);
4679
4680 if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
4681 (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
4682 valid_status(actor_get(actor_id, &actor))) {
4683 int more = 0;
4684 xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
4685
4686 *body = html_people_one(&snac, actor_id, list, skip, show, more, page);
4687 *b_size = strlen(*body);
4688 status = HTTP_STATUS_OK;
4689 }
4690 else {
4691 *body = xs_dup(uid);
4692 status = HTTP_STATUS_NOT_FOUND;
4693 }
4694 }
4695 }
4696 else
4697 if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
4698 if (!login(&snac, req)) {
4699 *body = xs_dup(uid);
4700 status = HTTP_STATUS_UNAUTHORIZED;
4701 }
4702 else {
4703 *body = html_notifications(&snac, skip, show);
4704 *b_size = strlen(*body);
4705 status = HTTP_STATUS_OK;
4706 }
4707 }
4708 else
4709 if (strcmp(p_path, "instance") == 0) { /** instance timeline **/
4710 if (!login(&snac, req)) {
4711 *body = xs_dup(uid);
4712 status = HTTP_STATUS_UNAUTHORIZED;
4713 }
4714 else {
4715 xs *list = timeline_instance_list(skip, show);
4716 xs *next = timeline_instance_list(skip + show, 1);
4717
4718 *body = html_timeline(&snac, list, 0, skip, show,
4719 xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse);
4720 *b_size = strlen(*body);
4721 status = HTTP_STATUS_OK;
4722 }
4723 }
4724 else
4725 if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/
4726 if (!login(&snac, req)) {
4727 *body = xs_dup(uid);
4728 status = HTTP_STATUS_UNAUTHORIZED;
4729 }
4730 else {
4731 xs *list = pinned_list(&snac);
4732
4733 *body = html_timeline(&snac, list, 0, skip, show,
4734 0, L("Pinned posts"), "", 0, error, terse);
4735 *b_size = strlen(*body);
4736 status = HTTP_STATUS_OK;
4737 }
4738 }
4739 else
4740 if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/
4741 if (!login(&snac, req)) {
4742 *body = xs_dup(uid);
4743 status = HTTP_STATUS_UNAUTHORIZED;
4744 }
4745 else {
4746 xs *list = bookmark_list(&snac);
4747
4748 *body = html_timeline(&snac, list, 0, skip, show,
4749 0, L("Bookmarked posts"), "", 0, error, terse);
4750 *b_size = strlen(*body);
4751 status = HTTP_STATUS_OK;
4752 }
4753 }
4754 else
4755 if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/
4756 if (!login(&snac, req)) {
4757 *body = xs_dup(uid);
4758 status = HTTP_STATUS_UNAUTHORIZED;
4759 }
4760 else {
4761 xs *list = draft_list(&snac);
4762
4763 *body = html_timeline(&snac, list, 0, skip, show,
4764 0, L("Post drafts"), "", 0, error, terse);
4765 *b_size = strlen(*body);
4766 status = HTTP_STATUS_OK;
4767 }
4768 }
4769 else
4770 if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
4771 if (!login(&snac, req)) {
4772 *body = xs_dup(uid);
4773 status = HTTP_STATUS_UNAUTHORIZED;
4774 }
4775 else {
4776 xs *list = scheduled_list(&snac);
4777
4778 *body = html_timeline(&snac, list, 0, skip, show,
4779 0, L("Scheduled posts"), "", 0, error, terse);
4780 *b_size = strlen(*body);
4781 status = HTTP_STATUS_OK;
4782 }
4783 }
4784 else
4785 if (xs_startswith(p_path, "list/")) { /** list timelines **/
4786 if (!login(&snac, req)) {
4787 *body = xs_dup(uid);
4788 status = HTTP_STATUS_UNAUTHORIZED;
4789 }
4790 else {
4791 xs *l = xs_split(p_path, "/");
4792 const char *lid = xs_list_get(l, -1);
4793
4794 xs *list = list_timeline(&snac, lid, skip, show);
4795 xs *next = list_timeline(&snac, lid, skip + show, 1);
4796
4797 if (list != NULL) {
4798 xs *ttl = timeline_top_level(&snac, list);
4799
4800 xs *base = xs_fmt("/list/%s", lid);
4801 xs *name = list_maint(&snac, lid, 3);
4802 xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
4803
4804 *body = html_timeline(&snac, ttl, 0, skip, show,
4805 xs_list_len(next), title, base, 1, error, terse);
4806 *b_size = strlen(*body);
4807 status = HTTP_STATUS_OK;
4808 }
4809 }
4810 }
4811 else
4812 if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/
4813 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4814 return HTTP_STATUS_FORBIDDEN;
4815
4816 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
4817 xs *msg = NULL;
4818
4819 if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) {
4820 xs *md5 = xs_md5_hex(id, strlen(id));
4821 xs *list = xs_list_new();
4822
4823 list = xs_list_append(list, md5);
4824
4825 *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse);
4826 *b_size = strlen(*body);
4827 status = HTTP_STATUS_OK;
4828 }
4829 }
4830 else
4831 if (xs_startswith(p_path, "s/")) { /** a static file **/
4832 xs *l = xs_split(p_path, "/");
4833 const char *id = xs_list_get(l, 1);
4834 int sz;
4835
4836 if (id && *id) {
4837 status = static_get(&snac, id, body, &sz,
4838 xs_dict_get(req, "if-none-match"), etag);
4839
4840 if (valid_status(status)) {
4841 *b_size = sz;
4842 *ctype = (char *)xs_mime_by_ext(id);
4843 }
4844 }
4845 }
4846 else
4847 if (xs_startswith(p_path, "h/")) { /** an entry from the history **/
4848 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4849 return HTTP_STATUS_FORBIDDEN;
4850
4851 if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE)
4852 return HTTP_STATUS_FORBIDDEN;
4853
4854 xs *l = xs_split(p_path, "/");
4855 const char *id = xs_list_get(l, 1);
4856
4857 if (id && *id) {
4858 if (xs_endswith(id, "timeline.html_")) {
4859 /* Don't let them in */
4860 *b_size = 0;
4861 status = HTTP_STATUS_NOT_FOUND;
4862 }
4863 else
4864 status = history_get(&snac, id, body, b_size,
4865 xs_dict_get(req, "if-none-match"), etag);
4866 }
4867 }
4868 else
4869 if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/
4870 if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE)
4871 return HTTP_STATUS_FORBIDDEN;
4872
4873 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
4874
4875 xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
4876 xs *bio = xs_dup(xs_dict_get(snac.config, "bio"));
4877
4878 xs *rss_title = xs_fmt("%s (@%s@%s)",
4879 xs_dict_get(snac.config, "name"),
4880 snac.uid,
4881 xs_dict_get(srv_config, "host"));
4882 xs *rss_link = xs_fmt("%s.rss", snac.actor);
4883
4884 *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio);
4885 *b_size = strlen(*body);
4886 *ctype = "application/rss+xml; charset=utf-8";
4887 status = HTTP_STATUS_OK;
4888
4889 snac_debug(&snac, 1, xs_fmt("serving RSS"));
4890 }
4891 else
4892 if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/
4893 xs *proxy_prefix = NULL;
4894
4895 if (xs_startswith(p_path, "x/")) {
4896 /* proxy usage authorized by http basic auth */
4897 if (login(&snac, req))
4898 proxy_prefix = xs_str_new("x/");
4899 else {
4900 *body = xs_dup(uid);
4901 status = HTTP_STATUS_UNAUTHORIZED;
4902 }
4903 }
4904 else {
4905 /* proxy usage authorized by proxy_token */
4906 xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor);
4907 xs *tk = xs_md5_hex(tks, strlen(tks));
4908 xs *p = xs_fmt("y/%s/", tk);
4909
4910 if (xs_startswith(p_path, p))
4911 proxy_prefix = xs_dup(p);
4912 }
4913
4914 if (proxy_prefix) {
4915 /* pick the raw path (including optional ? arguments) */
4916 const char *raw_path = xs_dict_get(req, "raw_path");
4917
4918 /* skip to where the proxy/ string starts */
4919 raw_path += xs_str_in(raw_path, proxy_prefix);
4920
4921 xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1);
4922 xs *hdrs = xs_dict_new();
4923
4924 hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT);
4925
4926 const char *ims = xs_dict_get(req, "if-modified-since");
4927 const char *inm = xs_dict_get(req, "if-none-match");
4928
4929 if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims);
4930 if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm);
4931
4932 xs *rsp = xs_http_request("GET", url, hdrs,
4933 NULL, 0, &status, body, b_size, 0);
4934
4935 if (valid_status(status)) {
4936 const char *ct = xs_or(xs_dict_get(rsp, "content-type"), "");
4937 const char *lm = xs_dict_get(rsp, "last-modified");
4938 const char *et = xs_dict_get(rsp, "etag");
4939
4940 if (lm) *last_modified = xs_dup(lm);
4941 if (et) *etag = xs_dup(et);
4942
4943 /* find the content-type in the static mime types,
4944 and return that value instead of ct, which will
4945 be destroyed when out of scope */
4946 for (int n = 0; xs_mime_types[n]; n += 2) {
4947 if (strcmp(ct, xs_mime_types[n + 1]) == 0) {
4948 *ctype = (char *)xs_mime_types[n + 1];
4949 break;
4950 }
4951 }
4952 }
4953
4954 snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status));
4955 }
4956 }
4957 else
4958 if (strcmp(p_path, "share") == 0) { /** direct post **/
4959 if (!login(&snac, req)) {
4960 *body = xs_dup(uid);
4961 status = HTTP_STATUS_UNAUTHORIZED;
4962 }
4963 else {
4964 const char *b64 = xs_dict_get(q_vars, "content");
4965 int sz;
4966 xs *content = xs_base64_dec(b64, &sz);
4967 xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL);
4968 xs *c_msg = msg_create(&snac, msg);
4969
4970 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
4971
4972 enqueue_message(&snac, c_msg);
4973
4974 snac_debug(&snac, 1, xs_fmt("web action 'share' received"));
4975
4976 *body = xs_fmt("%s/admin", snac.actor);
4977 *b_size = strlen(*body);
4978 status = HTTP_STATUS_SEE_OTHER;
4979 }
4980 }
4981 else
4982 if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
4983 if (!login(&snac, req)) {
4984 *body = xs_dup(uid);
4985 status = HTTP_STATUS_UNAUTHORIZED;
4986 }
4987 else {
4988 status = HTTP_STATUS_NOT_FOUND;
4989
4990 const char *id = xs_dict_get(q_vars, "id");
4991 const char *action = xs_dict_get(q_vars, "action");
4992
4993 if (xs_is_string(id) && xs_is_string(action)) {
4994 if (strcmp(action, "Follow") == 0) {
4995 xs *msg = msg_follow(&snac, id);
4996
4997 if (msg != NULL) {
4998 const char *actor = xs_dict_get(msg, "object");
4999
5000 following_add(&snac, actor, msg);
5001
5002 enqueue_output_by_actor(&snac, msg, actor, 0);
5003
5004 status = HTTP_STATUS_SEE_OTHER;
5005 }
5006 }
5007 else
5008 if (xs_match(action, "Like|Boost|Announce")) {
5009 /* bring the post */
5010 xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
5011
5012 if (msg != NULL) {
5013 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
5014 enqueue_message(&snac, msg);
5015
5016 status = HTTP_STATUS_SEE_OTHER;
5017 }
5018 }
5019 }
5020
5021 if (status == HTTP_STATUS_SEE_OTHER) {
5022 *body = xs_fmt("%s/admin", snac.actor);
5023 *b_size = strlen(*body);
5024 }
5025 }
5026 }
5027 else
5028 status = HTTP_STATUS_NOT_FOUND;
5029
5030 user_free(&snac);
5031
5032 if (valid_status(status) && *ctype == NULL) {
5033 *ctype = "text/html; charset=utf-8";
5034 }
5035
5036 return status;
5037}
5038
5039
5040int html_post_handler(const xs_dict *req, const char *q_path,
5041 char *payload, int p_size,
5042 char **body, int *b_size, char **ctype)
5043{
5044 (void)p_size;
5045 (void)ctype;
5046
5047 int status = 0;
5048 const snac *user = NULL;
5049 snac snac;
5050 const char *uid;
5051 const char *p_path;
5052 const xs_dict *p_vars;
5053
5054 xs *l = xs_split_n(q_path, "/", 2);
5055
5056 uid = xs_list_get(l, 1);
5057 if (!uid || !user_open(&snac, uid)) {
5058 /* invalid user */
5059 srv_debug(1, xs_fmt("html_post_handler bad user %s", uid));
5060 return HTTP_STATUS_NOT_FOUND;
5061 }
5062
5063 p_path = xs_list_get(l, 2);
5064
5065 /* all posts must be authenticated */
5066 if (!login(&snac, req)) {
5067 user_free(&snac);
5068 *body = xs_dup(uid);
5069 return HTTP_STATUS_UNAUTHORIZED;
5070 }
5071
5072 user = &snac; /* for L() */
5073 set_user_lang(&snac);
5074
5075 p_vars = xs_dict_get(req, "p_vars");
5076
5077 if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
5078 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
5079
5080 /* post note */
5081 const char *content = xs_dict_get(p_vars, "content");
5082 const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
5083 const char *to = xs_dict_get(p_vars, "to");
5084 const char *sensitive = xs_dict_get(p_vars, "sensitive");
5085 const char *summary = xs_dict_get(p_vars, "summary");
5086 const char *edit_id = xs_dict_get(p_vars, "edit_id");
5087 const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
5088 const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
5089 const char *post_lang = xs_dict_get(p_vars, "post_lang");
5090 const char *visibility = xs_dict_get(p_vars, "visibility");
5091 int scope = SCOPE_PUBLIC; /* default to public */
5092 if (!xs_is_null(visibility)) {
5093 if (strcmp(visibility, "unlisted") == 0)
5094 scope = SCOPE_UNLISTED;
5095 else
5096 if (strcmp(visibility, "followers") == 0)
5097 scope = SCOPE_FOLLOWERS;
5098 else
5099 if (strcmp(visibility, "mentioned") == 0)
5100 scope = SCOPE_MENTIONED;
5101 }
5102 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
5103 xs *attach_list = xs_list_new();
5104
5105 /* iterate the attachments */
5106 int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
5107
5108 for (int att_n = 0; att_n < max_attachments; att_n++) {
5109 xs *url_lbl = xs_fmt("attach_url_%d", att_n);
5110 xs *att_lbl = xs_fmt("attach_%d", att_n);
5111 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
5112
5113 const char *attach_url = xs_dict_get(p_vars, url_lbl);
5114 const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
5115 const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, "");
5116
5117 if (xs_is_string(attach_url) && *attach_url != '\0') {
5118 xs *l = xs_list_new();
5119
5120 l = xs_list_append(l, attach_url);
5121 l = xs_list_append(l, alt_text);
5122
5123 attach_list = xs_list_append(attach_list, l);
5124 }
5125 else
5126 if (xs_is_list(attach_file)) {
5127 const char *fn = xs_list_get(attach_file, 0);
5128
5129 if (xs_is_string(fn) && *fn != '\0') {
5130 char rnd[32];
5131 xs_rnd_buf(rnd, sizeof(rnd));
5132
5133 const char *ext = strrchr(fn, '.');
5134 xs *hash = xs_md5_hex(rnd, sizeof(rnd));
5135 xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
5136 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
5137 int fo = xs_number_get(xs_list_get(attach_file, 1));
5138 int fs = xs_number_get(xs_list_get(attach_file, 2));
5139
5140 /* store */
5141 static_put(&snac, id, payload + fo, fs);
5142
5143 xs *l = xs_list_new();
5144
5145 l = xs_list_append(l, url);
5146 l = xs_list_append(l, alt_text);
5147
5148 attach_list = xs_list_append(attach_list, l);
5149 }
5150 }
5151 }
5152
5153 if (content != NULL) {
5154 xs *msg = NULL;
5155 xs *c_msg = NULL;
5156 xs *content_2 = xs_replace(content, "\r", "");
5157 xs *poll_opts = NULL;
5158
5159 /* is there a valid set of poll options? */
5160 const char *v = xs_dict_get(p_vars, "poll_options");
5161 if (!xs_is_null(v) && *v) {
5162 xs *v2 = xs_strip_i(xs_replace(v, "\r", ""));
5163
5164 poll_opts = xs_split(v2, "\n");
5165 }
5166
5167 if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) {
5168 /* get the rest of poll configuration */
5169 const char *p_multiple = xs_dict_get(p_vars, "poll_multiple");
5170 const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs");
5171 int multiple = 0;
5172
5173 int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60");
5174
5175 if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0)
5176 multiple = 1;
5177
5178 msg = msg_question(&snac, content_2, attach_list,
5179 poll_opts, multiple, end_secs);
5180
5181 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
5182 }
5183 else
5184 msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
5185
5186 if (sensitive != NULL) {
5187 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
5188 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
5189 }
5190
5191 if (xs_is_string(post_date) && *post_date) {
5192 xs *post_pubdate = xs_fmt("%sT%s", post_date,
5193 xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
5194
5195 time_t t = xs_parse_iso_date(post_pubdate, 0);
5196
5197 if (t != 0) {
5198 t -= xs_tz_offset(snac.tz);
5199
5200 xs *iso_date = xs_str_iso_date(t);
5201 msg = xs_dict_set(msg, "published", iso_date);
5202
5203 snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
5204 }
5205 else
5206 snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
5207 }
5208
5209 /* is the published date from the future? */
5210 int future_post = 0;
5211 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
5212
5213 if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
5214 future_post = 1;
5215
5216 if (xs_is_null(edit_id)) {
5217 /* new message */
5218 const char *id = xs_dict_get(msg, "id");
5219
5220 if (store_as_draft) {
5221 draft_add(&snac, id, msg);
5222 }
5223 else
5224 if (future_post) {
5225 schedule_add(&snac, id, msg);
5226 }
5227 else {
5228 c_msg = msg_create(&snac, msg);
5229 timeline_add(&snac, id, msg);
5230 }
5231 }
5232 else {
5233 /* an edition of a previous message */
5234 xs *p_msg = NULL;
5235
5236 if (valid_status(object_get(edit_id, &p_msg))) {
5237 /* copy relevant fields from previous version */
5238 char *fields[] = { "id", "context", "url",
5239 "to", "inReplyTo", NULL };
5240 int n;
5241
5242 for (n = 0; fields[n]; n++) {
5243 const char *v = xs_dict_get(p_msg, fields[n]);
5244 msg = xs_dict_set(msg, fields[n], v);
5245 }
5246
5247 if (store_as_draft) {
5248 draft_add(&snac, edit_id, msg);
5249 }
5250 else
5251 if (is_draft(&snac, edit_id)) {
5252 /* message was previously a draft; it's a create activity */
5253
5254 /* if the date is from the past, overwrite it with right_now */
5255 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
5256 snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
5257 msg = xs_dict_set(msg, "published", right_now);
5258 }
5259
5260 /* overwrite object */
5261 object_add_ow(edit_id, msg);
5262
5263 if (future_post) {
5264 schedule_add(&snac, edit_id, msg);
5265 }
5266 else {
5267 c_msg = msg_create(&snac, msg);
5268 timeline_add(&snac, edit_id, msg);
5269 }
5270
5271 draft_del(&snac, edit_id);
5272 }
5273 else
5274 if (is_scheduled(&snac, edit_id)) {
5275 /* editing an scheduled post; just update it */
5276 schedule_add(&snac, edit_id, msg);
5277 }
5278 else {
5279 /* ignore the (possibly changed) published date */
5280 msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
5281
5282 /* set the updated field */
5283 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
5284 msg = xs_dict_set(msg, "updated", updated);
5285
5286 /* overwrite object, not updating the indexes */
5287 object_add_ow(edit_id, msg);
5288
5289 /* index tags */
5290 tag_index(edit_id, msg);
5291
5292 /* update message */
5293 c_msg = msg_update(&snac, msg);
5294 }
5295 }
5296 else
5297 snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
5298 }
5299
5300 if (c_msg != NULL) {
5301 enqueue_message(&snac, c_msg);
5302 enqueue_webmention(msg);
5303 }
5304
5305 history_del(&snac, "timeline.html_");
5306 }
5307
5308 status = HTTP_STATUS_SEE_OTHER;
5309 }
5310 else
5311 if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/
5312 /* action on an entry */
5313 const char *id = xs_dict_get(p_vars, "id");
5314 const char *actor = xs_dict_get(p_vars, "actor");
5315 const char *action = xs_dict_get(p_vars, "action");
5316 const char *group = xs_dict_get(p_vars, "group");
5317
5318 if (action == NULL)
5319 return HTTP_STATUS_NOT_FOUND;
5320
5321 snac_debug(&snac, 1, xs_fmt("web action '%s' received", action));
5322
5323 status = HTTP_STATUS_SEE_OTHER;
5324
5325 if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
5326 const char *eid = xs_dict_get(p_vars, "eid");
5327
5328 if (eid != NULL) {
5329 xs *n_msg = msg_emoji_unreact(&snac, id, eid);
5330
5331 if (n_msg != NULL)
5332 enqueue_message(&snac, n_msg);
5333 }
5334 }
5335 else
5336 if (strcmp(action, L("EmojiReact")) == 0) { /** **/
5337 xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
5338
5339 eid = xs_strip_chars_i(eid, ":");
5340
5341 const xs_dict *ret = msg_emoji_init(&snac, id, eid);
5342 /* fails if either invalid or already reacted */
5343 if (!ret)
5344 ret = msg_emoji_unreact(&snac, id, eid);
5345 if (!ret)
5346 status = HTTP_STATUS_NOT_FOUND;
5347 }
5348 else
5349 if (strcmp(action, L("Like")) == 0) { /** **/
5350 xs *msg = msg_admiration(&snac, id, "Like");
5351
5352 if (msg != NULL) {
5353 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
5354 enqueue_message(&snac, msg);
5355 }
5356 }
5357 else
5358 if (strcmp(action, L("Boost")) == 0) { /** **/
5359 xs *msg = msg_admiration(&snac, id, "Announce");
5360
5361 if (msg != NULL) {
5362 timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
5363 enqueue_message(&snac, msg);
5364 }
5365 }
5366 else
5367 if (strcmp(action, L("Unlike")) == 0) { /** **/
5368 xs *msg = msg_repulsion(&snac, id, "Like");
5369
5370 if (msg != NULL) {
5371 enqueue_message(&snac, msg);
5372 }
5373 }
5374 else
5375 if (strcmp(action, L("Unboost")) == 0) { /** **/
5376 xs *msg = msg_repulsion(&snac, id, "Announce");
5377
5378 if (msg != NULL) {
5379 enqueue_message(&snac, msg);
5380 }
5381 }
5382 else
5383 if (strcmp(action, L("MUTE")) == 0) { /** **/
5384 mute(&snac, actor);
5385 }
5386 else
5387 if (strcmp(action, L("Unmute")) == 0) { /** **/
5388 unmute(&snac, actor);
5389 }
5390 else
5391 if (strcmp(action, L("Hide")) == 0) { /** **/
5392 if (is_draft(&snac, id))
5393 draft_del(&snac, id);
5394 else
5395 if (is_scheduled(&snac, id))
5396 schedule_del(&snac, id);
5397 else
5398 hide(&snac, id);
5399 }
5400 else
5401 if (strcmp(action, L("Limit")) == 0) { /** **/
5402 limit(&snac, actor);
5403 }
5404 else
5405 if (strcmp(action, L("Unlimit")) == 0) { /** **/
5406 unlimit(&snac, actor);
5407 }
5408 else
5409 if (strcmp(action, L("Follow")) == 0) { /** **/
5410 xs *msg = msg_follow(&snac, actor);
5411
5412 if (msg != NULL) {
5413 /* reload the actor from the message, in may be different */
5414 actor = xs_dict_get(msg, "object");
5415
5416 following_add(&snac, actor, msg);
5417
5418 enqueue_output_by_actor(&snac, msg, actor, 0);
5419 }
5420 }
5421 else
5422 if (strcmp(action, L("Unfollow")) == 0) { /** **/
5423 /* get the following object */
5424 xs *object = NULL;
5425
5426 if (valid_status(following_get(&snac, actor, &object))) {
5427 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5428
5429 following_del(&snac, actor);
5430
5431 enqueue_output_by_actor(&snac, msg, actor, 0);
5432
5433 snac_log(&snac, xs_fmt("unfollowed actor %s", actor));
5434 }
5435 else
5436 snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5437 }
5438 else
5439 if (strcmp(action, L("Follow Group")) == 0) { /** **/
5440 xs *msg = msg_follow(&snac, group);
5441
5442 if (msg != NULL) {
5443 /* reload the group from the message, in may be different */
5444 group = xs_dict_get(msg, "object");
5445
5446 following_add(&snac, group, msg);
5447
5448 enqueue_output_by_actor(&snac, msg, group, 0);
5449 }
5450 }
5451 else
5452 if (strcmp(action, L("Unfollow Group")) == 0) { /** **/
5453 /* get the following object */
5454 xs *object = NULL;
5455
5456 if (valid_status(following_get(&snac, group, &object))) {
5457 xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
5458
5459 following_del(&snac, group);
5460
5461 enqueue_output_by_actor(&snac, msg, group, 0);
5462
5463 snac_log(&snac, xs_fmt("unfollowed group %s", group));
5464 }
5465 else
5466 snac_log(&snac, xs_fmt("actor is not being followed %s", actor));
5467 }
5468 else
5469 if (strcmp(action, L("Delete")) == 0) { /** **/
5470 const char *actor_form = xs_dict_get(p_vars, "actor-form");
5471 if (actor_form != NULL) {
5472 /* delete follower */
5473 if (valid_status(follower_del(&snac, actor)))
5474 snac_log(&snac, xs_fmt("deleted follower %s", actor));
5475 else
5476 snac_log(&snac, xs_fmt("error deleting follower %s", actor));
5477 }
5478 else {
5479 /* delete an entry */
5480 if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
5481 /* it's a post by us: generate a delete */
5482 xs *msg = msg_delete(&snac, id);
5483
5484 enqueue_message(&snac, msg);
5485
5486 snac_log(&snac, xs_fmt("posted tombstone for %s", id));
5487 }
5488
5489 timeline_del(&snac, id);
5490
5491 draft_del(&snac, id);
5492
5493 schedule_del(&snac, id);
5494
5495 snac_log(&snac, xs_fmt("deleted entry %s", id));
5496 }
5497 }
5498 else
5499 if (strcmp(action, L("Pin")) == 0) { /** **/
5500 pin(&snac, id);
5501 timeline_touch(&snac);
5502 }
5503 else
5504 if (strcmp(action, L("Unpin")) == 0) { /** **/
5505 unpin(&snac, id);
5506 timeline_touch(&snac);
5507 }
5508 else
5509 if (strcmp(action, L("Bookmark")) == 0) { /** **/
5510 bookmark(&snac, id);
5511 timeline_touch(&snac);
5512 }
5513 else
5514 if (strcmp(action, L("Unbookmark")) == 0) { /** **/
5515 unbookmark(&snac, id);
5516 timeline_touch(&snac);
5517 }
5518 else
5519 if (strcmp(action, L("Approve")) == 0) { /** **/
5520 xs *fwreq = pending_get(&snac, actor);
5521
5522 if (fwreq != NULL) {
5523 xs *reply = msg_accept(&snac, fwreq, actor);
5524
5525 enqueue_message(&snac, reply);
5526
5527 if (xs_is_null(xs_dict_get(fwreq, "published"))) {
5528 /* add a date if it doesn't include one (Mastodon) */
5529 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
5530 fwreq = xs_dict_set(fwreq, "published", date);
5531 }
5532
5533 timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
5534
5535 follower_add(&snac, actor);
5536
5537 pending_del(&snac, actor);
5538
5539 snac_log(&snac, xs_fmt("new follower %s", actor));
5540 }
5541 }
5542 else
5543 if (strcmp(action, L("Discard")) == 0) { /** **/
5544 pending_del(&snac, actor);
5545 }
5546 else
5547 status = HTTP_STATUS_NOT_FOUND;
5548
5549 /* delete the cached timeline */
5550 if (status == HTTP_STATUS_SEE_OTHER)
5551 history_del(&snac, "timeline.html_");
5552 }
5553 else
5554 if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/
5555 /* change of user data */
5556 const char *v;
5557 const char *p1, *p2;
5558
5559 if ((v = xs_dict_get(p_vars, "name")) != NULL)
5560 snac.config = xs_dict_set(snac.config, "name", v);
5561 if ((v = xs_dict_get(p_vars, "avatar")) != NULL)
5562 snac.config = xs_dict_set(snac.config, "avatar", v);
5563 if ((v = xs_dict_get(p_vars, "bio")) != NULL)
5564 snac.config = xs_dict_set(snac.config, "bio", v);
5565 if ((v = xs_dict_get(p_vars, "cw")) != NULL &&
5566 strcmp(v, "on") == 0) {
5567 snac.config = xs_dict_set(snac.config, "cw", "open");
5568 } else { /* if the checkbox is not set, the parameter is missing */
5569 snac.config = xs_dict_set(snac.config, "cw", "");
5570 }
5571 if ((v = xs_dict_get(p_vars, "email")) != NULL)
5572 snac.config = xs_dict_set(snac.config, "email", v);
5573 if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL)
5574 snac.config = xs_dict_set(snac.config, "telegram_bot", v);
5575 if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL)
5576 snac.config = xs_dict_set(snac.config, "telegram_chat_id", v);
5577 if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL)
5578 snac.config = xs_dict_set(snac.config, "ntfy_server", v);
5579 if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL)
5580 snac.config = xs_dict_set(snac.config, "ntfy_token", v);
5581 if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) {
5582 xs *days = xs_number_new(atof(v));
5583 snac.config = xs_dict_set(snac.config, "purge_days", days);
5584 }
5585 if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0)
5586 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE));
5587 else
5588 snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE));
5589 if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0)
5590 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE));
5591 else
5592 snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE));
5593 if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0)
5594 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE));
5595 else
5596 snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE));
5597 if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0)
5598 snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE));
5599 else
5600 snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE));
5601 if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0)
5602 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
5603 else
5604 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
5605 if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
5606 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
5607 else
5608 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
5609 if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
5610 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
5611 else
5612 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
5613 if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
5614 snac.config = xs_dict_set(snac.config, "lang", v);
5615 if ((v = xs_dict_get(p_vars, "tz")) != NULL)
5616 snac.config = xs_dict_set(snac.config, "tz", v);
5617 if ((v = xs_dict_get(p_vars, "post_langs")) != NULL)
5618 snac.config = xs_dict_set(snac.config, "post_langs", v);
5619
5620 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
5621 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
5622
5623 snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", ""));
5624
5625 if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
5626 snac.config = xs_dict_set(snac.config, "metadata", v);
5627
5628 /* uploads */
5629 const char *uploads[] = { "avatar", "header", NULL };
5630 int n;
5631 for (n = 0; uploads[n]; n++) {
5632 xs *var_name = xs_fmt("%s_file", uploads[n]);
5633
5634 const xs_list *uploaded_file = xs_dict_get(p_vars, var_name);
5635 if (xs_type(uploaded_file) == XSTYPE_LIST) {
5636 const char *fn = xs_list_get(uploaded_file, 0);
5637
5638 if (fn && *fn) {
5639 const char *mimetype = xs_mime_by_ext(fn);
5640
5641 if (xs_startswith(mimetype, "image/")) {
5642 const char *ext = strrchr(fn, '.');
5643 xs *hash = xs_md5_hex(fn, strlen(fn));
5644 xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
5645 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
5646 int fo = xs_number_get(xs_list_get(uploaded_file, 1));
5647 int fs = xs_number_get(xs_list_get(uploaded_file, 2));
5648
5649 /* store */
5650 static_put(&snac, id, payload + fo, fs);
5651
5652 snac.config = xs_dict_set(snac.config, uploads[n], url);
5653 }
5654 }
5655 }
5656 }
5657
5658 /* delete images by removing url from user.json */
5659 for (n = 0; uploads[n]; n++) {
5660 xs *var_name = xs_fmt("%s_delete", uploads[n]);
5661 const char *delete_var = xs_dict_get(p_vars, var_name);
5662
5663 if (delete_var != NULL && strcmp(delete_var, "on") == 0) {
5664 snac.config = xs_dict_set(snac.config, uploads[n], "");
5665 }
5666 }
5667
5668 /* password change? */
5669 if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL &&
5670 (p2 = xs_dict_get(p_vars, "passwd2")) != NULL &&
5671 *p1 && strcmp(p1, p2) == 0) {
5672 xs *pw = hash_password(snac.uid, p1, NULL);
5673 snac.config = xs_dict_set(snac.config, "passwd", pw);
5674 }
5675
5676 user_persist(&snac, 1);
5677
5678 status = HTTP_STATUS_SEE_OTHER;
5679 }
5680 else
5681 if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
5682 notify_clear(&snac);
5683 timeline_touch(&snac);
5684
5685 status = HTTP_STATUS_SEE_OTHER;
5686 }
5687 else
5688 if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/
5689 const char *irt = xs_dict_get(p_vars, "irt");
5690 const char *opt = xs_dict_get(p_vars, "question");
5691 const char *actor = xs_dict_get(p_vars, "actor");
5692
5693 xs *ls = NULL;
5694
5695 /* multiple choices? */
5696 if (xs_type(opt) == XSTYPE_LIST)
5697 ls = xs_dup(opt);
5698 else
5699 if (xs_type(opt) == XSTYPE_STRING) {
5700 ls = xs_list_new();
5701 ls = xs_list_append(ls, opt);
5702 }
5703
5704 const xs_str *v;
5705 int c = 0;
5706
5707 while (xs_list_next(ls, &v, &c)) {
5708 xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL);
5709
5710 /* set the option */
5711 msg = xs_dict_append(msg, "name", v);
5712
5713 /* delete the content */
5714 msg = xs_dict_del(msg, "content");
5715
5716 xs *c_msg = msg_create(&snac, msg);
5717
5718 enqueue_message(&snac, c_msg);
5719
5720 timeline_add(&snac, xs_dict_get(msg, "id"), msg);
5721 }
5722
5723 if (ls != NULL) {
5724 /* get the poll object */
5725 xs *poll = NULL;
5726
5727 if (valid_status(object_get(irt, &poll))) {
5728 const char *date = xs_dict_get(poll, "endTime");
5729 if (xs_is_null(date))
5730 date = xs_dict_get(poll, "closed");
5731
5732 if (!xs_is_null(date)) {
5733 time_t t = xs_parse_iso_date(date, 0) - time(NULL);
5734
5735 /* request the poll when it's closed;
5736 Pleroma does not send and update when the poll closes */
5737 enqueue_object_request(&snac, irt, t + 2);
5738 }
5739 }
5740 }
5741
5742 status = HTTP_STATUS_SEE_OTHER;
5743 }
5744 else
5745 if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
5746 const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
5747
5748 if (xs_is_string(followed_hashtags)) {
5749 xs *new_hashtags = xs_list_new();
5750 xs *l = xs_split(followed_hashtags, "\n");
5751 const char *v;
5752
5753 xs_list_foreach(l, v) {
5754 xs *s1 = xs_strip_i(xs_dup(v));
5755 s1 = xs_replace_i(s1, " ", "");
5756
5757 if (*s1 == '\0')
5758 continue;
5759
5760 xs *s2 = NULL;
5761
5762 if (xs_startswith(s1, "https:/"))
5763 s2 = xs_dup(s1);
5764 else {
5765 s2 = xs_utf8_to_lower(s1);
5766
5767 if (*s2 != '#')
5768 s2 = xs_str_prepend_i(s2, "#");
5769 }
5770
5771 new_hashtags = xs_list_append(new_hashtags, s2);
5772 }
5773
5774 snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
5775 user_persist(&snac, 0);
5776 }
5777
5778 status = HTTP_STATUS_SEE_OTHER;
5779 }
5780 else
5781 if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/
5782 const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags");
5783
5784 if (xs_is_string(hashtags)) {
5785 xs *new_hashtags = xs_list_new();
5786 xs *l = xs_split(hashtags, "\n");
5787 const char *v;
5788
5789 xs_list_foreach(l, v) {
5790 xs *s1 = xs_strip_i(xs_dup(v));
5791 s1 = xs_replace_i(s1, " ", "");
5792
5793 if (*s1 == '\0')
5794 continue;
5795
5796 xs *s2 = xs_utf8_to_lower(s1);
5797 if (*s2 != '#')
5798 s2 = xs_str_prepend_i(s2, "#");
5799
5800 new_hashtags = xs_list_append(new_hashtags, s2);
5801 }
5802
5803 snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags);
5804 user_persist(&snac, 0);
5805 }
5806
5807 status = HTTP_STATUS_SEE_OTHER;
5808 }
5809
5810 if (status == HTTP_STATUS_SEE_OTHER) {
5811 const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
5812
5813 if (xs_is_string(hard_redir))
5814 *body = xs_dup(hard_redir);
5815 else {
5816 const char *redir = xs_dict_get(p_vars, "redir");
5817
5818 if (xs_is_null(redir))
5819 redir = "top";
5820
5821 *body = xs_fmt("%s/admin#%s", snac.actor, redir);
5822 }
5823
5824 *b_size = strlen(*body);
5825 }
5826
5827 user_free(&snac);
5828
5829 return status;
5830}