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