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