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