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