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