/* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" #include "xs_json.h" #include "xs_regex.h" #include "xs_set.h" #include "xs_openssl.h" #include "xs_time.h" #include "xs_mime.h" #include "xs_match.h" #include "xs_html.h" #include "xs_curl.h" #include "xs_unicode.h" #include "xs_url.h" #include "xs_random.h" #include "xs_http.h" #include "xs_list_tools.h" #include "snac.h" int login(snac *user, const xs_dict *headers) /* tries a login */ { int logged_in = 0; const char *auth = xs_dict_get(headers, "authorization"); if (auth && xs_startswith(auth, "Basic ")) { int sz; xs *s1 = xs_crop_i(xs_dup(auth), 6, 0); xs *s2 = xs_base64_dec(s1, &sz); xs *l1 = xs_split_n(s2, ":", 1); if (xs_list_len(l1) == 2) { const char *uid = xs_list_get(l1, 0); const char *pwd = xs_list_get(l1, 1); const char *addr = xs_or(xs_dict_get(headers, "remote-addr"), xs_dict_get(headers, "x-forwarded-for")); if (badlogin_check(uid, addr)) { logged_in = check_password(uid, pwd, xs_dict_get(user->config, "passwd")); if (!logged_in) badlogin_inc(uid, addr); } } } if (logged_in) lastlog_write(user, "web"); return logged_in; } xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy, const xs_list *cl, const char *act_o) /* replace but also adds a class list and an actor in its alt text. * Used for emoji reactions */ { if (!xs_is_null(tag)) { xs *tag_list = NULL; if (xs_type(tag) == XSTYPE_DICT) { /* not a list */ tag_list = xs_list_new(); tag_list = xs_list_append(tag_list, tag); } else { /* is a list */ tag_list = xs_dup(tag); } xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems); xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems); if (cl) { xs *l = xs_join(cl, " "); class = xs_str_cat(class, " ", l); } int c = 0; const xs_val *v; c = 0; xs_set rep_emoji; xs_set_init(&rep_emoji); while (xs_list_next(tag_list, &v, &c)) { const char *t = xs_dict_get(v, "type"); if (t && strcmp(t, "Emoji") == 0) { const char *n = xs_dict_get(v, "name"); const xs_dict *i = xs_dict_get(v, "icon"); /* avoid repeated emojis (Misskey seems to return this) */ if (xs_set_add(&rep_emoji, n) == 0) continue; if (xs_is_string(n) && xs_is_dict(i)) { const char *u = xs_dict_get(i, "url"); const char *mt = xs_dict_get(i, "mediaType"); if (xs_is_string(u)) { // on akkoma instances mediaType is not present. // but we need to to know if the image is an svg or not. // for now, i just use the file extention, which may not be the most reliable... if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n); if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); else { xs *url = make_url(u, proxy, 0); xs_html *img = xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("src", url), xs_html_attr("alt", act), xs_html_attr("title", act), xs_html_attr("class", class), xs_html_attr("style", style)); xs *s1 = xs_html_render(img); s = xs_replace_i(s, n, s1); } } else s = xs_replace_i(s, n, ""); } } } xs_set_free(&rep_emoji); } return s; } xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy) /* replaces all the :shortnames: with the emojis in tag */ { return _replace_shortnames(s, tag, ems, proxy, NULL, NULL); } xs_str *actor_name(xs_dict *actor, const char *proxy) /* gets the actor name */ { const char *v; if (xs_is_null((v = xs_dict_get(actor, "name"))) || *v == '\0') { if (xs_is_null(v = xs_dict_get(actor, "preferredUsername")) || *v == '\0') { v = "anonymous"; } } return replace_shortnames(xs_html_encode(v), xs_dict_get(actor, "tag"), 1, proxy); } xs_str *actor_pronouns(xs_dict *actor) /* gets the actor name */ { const xs_list *attachment; const xs_dict *d; const char *pronouns = ""; xs_str *ret; if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) { xs_list_foreach(attachment, d) { xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); /* make sure that we are reading the correct metadata */ if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { pronouns = xs_dict_get(d, "value"); break; } } } /*
breaks page, cannot nest them */ ret = xs_replace_i(xs_dup(pronouns), "
", ""); ret = xs_replace_i(ret, "
", ""); return ret; } xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) /* needed when we have local text with no tags attached */ { xs *tags = xs_list_new(); xs *name1 = not_really_markdown(text, NULL, &tags); xs_str *name3; if (user) { xs *name2 = process_tags(user, name1, &tags); name3 = sanitize(name2); } else { name3 = sanitize(name1); name3 = xs_replace_i(name3, ""); if (is_emoji == 0) c = xs_str_cat(c, "
");
/* replace the :shortnames: */
c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
/* Peertube videos content is in markdown */
const char *mtype = xs_dict_get(msg, "mediaType");
if (xs_type(mtype) == XSTYPE_STRING && strcmp(mtype, "text/markdown") == 0) {
/* a full conversion could be better */
c = xs_replace_i(c, "\r", "");
c = xs_replace_i(c, "\n", "
");
}
/* c contains sanitized HTML */
xs_html_add(snac_content,
xs_html_raw(c));
/* quoted post */
const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl"));
if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/
xs *quoted_post = NULL;
if (valid_status(object_get(quoted_id, "ed_post))) {
xs_html_add(snac_content,
xs_html_tag("blockquote",
xs_html_attr("class", "snac-quoted-post"),
html_entry(user, quoted_post, 1, 1, NULL, 1)));
}
else
if (user)
enqueue_object_request(user, quoted_id, 0);
}
}
if (strcmp(type, "Question") == 0) { /** question content **/
const xs_list *oo = xs_dict_get(msg, "oneOf");
const xs_list *ao = xs_dict_get(msg, "anyOf");
const xs_list *p;
const xs_dict *v;
int closed = 0;
const char *f_closed = NULL;
xs_html *poll = xs_html_tag("div", NULL);
if (read_only)
closed = 1; /* non-identified page; show as closed */
else
if (user && is_msg_mine(user, id))
closed = 1; /* we questioned; closed for us */
else
if (user && was_question_voted(user, id))
closed = 1; /* we already voted; closed for us */
if ((f_closed = xs_dict_get(msg, "closed")) != NULL) {
/* it has a closed date... but is it in the past? */
time_t t0 = time(NULL);
time_t t1 = xs_parse_iso_date(f_closed, 0);
if (t1 < t0)
closed = 2;
}
/* get the appropriate list of options */
p = oo != NULL ? oo : ao;
if (closed || user == NULL) {
/* closed poll */
xs_html *poll_result = xs_html_tag("table",
xs_html_attr("class", "snac-poll-result"));
int c = 0;
while (xs_list_next(p, &v, &c)) {
const char *name = xs_dict_get(v, "name");
const xs_dict *replies = xs_dict_get(v, "replies");
if (xs_is_string(name) && xs_is_dict(replies)) {
const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
if (xs_is_string(ti))
xs_html_add(poll_result,
xs_html_tag("tr",
xs_html_tag("td",
xs_html_text(name),
xs_html_text(":")),
xs_html_tag("td",
xs_html_text(ti))));
}
}
xs_html_add(poll,
poll_result);
}
else {
/* poll still active */
xs *vote_action = xs_fmt("%s/admin/vote", user->actor);
xs_html *form;
xs_html *poll_form = xs_html_tag("div",
xs_html_attr("class", "snac-poll-form"),
form = xs_html_tag("form",
xs_html_attr("autocomplete", "off"),
xs_html_attr("method", "post"),
xs_html_attr("action", vote_action),
xs_html_sctag("input",
xs_html_attr("type", "hidden"),
xs_html_attr("name", "actor"),
xs_html_attr("value", actor)),
xs_html_sctag("input",
xs_html_attr("type", "hidden"),
xs_html_attr("name", "irt"),
xs_html_attr("value", id))));
int c = 0;
while (xs_list_next(p, &v, &c)) {
const char *name = xs_dict_get(v, "name");
const xs_dict *replies = xs_dict_get(v, "replies");
if (name) {
char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems"));
xs_html *btn = xs_html_sctag("input",
xs_html_attr("id", name),
xs_html_attr("value", name),
xs_html_attr("name", "question"));
if (!xs_is_null(oo)) {
xs_html_add(btn,
xs_html_attr("type", "radio"),
xs_html_attr("required", "required"));
}
else
xs_html_add(btn,
xs_html_attr("type", "checkbox"));
xs_html_add(form,
btn,
xs_html_text(" "),
xs_html_tag("span",
xs_html_attr("title", ti),
xs_html_text(name)),
xs_html_sctag("br", NULL));
}
}
xs_html_add(form,
xs_html_tag("p", NULL),
xs_html_sctag("input",
xs_html_attr("type", "submit"),
xs_html_attr("class", "button"),
xs_html_attr("value", L("Vote"))));
xs_html_add(poll,
poll_form);
}
/* if it's *really* closed, say it */
if (closed == 2) {
xs_html_add(poll,
xs_html_tag("p",
xs_html_text(L("Closed"))));
}
else {
/* show when the poll closes */
const char *end_time = xs_dict_get(msg, "endTime");
/* Pleroma does not have an endTime field;
it has a closed time in the future */
if (xs_is_null(end_time))
end_time = xs_dict_get(msg, "closed");
if (!xs_is_null(end_time)) {
time_t t0 = time(NULL);
time_t t1 = xs_parse_iso_date(end_time, 0);
if (t1 > 0 && t1 > t0) {
time_t diff_time = t1 - t0;
xs *tf = xs_str_time_diff(diff_time);
char *p = tf;
/* skip leading zeros */
for (; *p == '0' || *p == ':'; p++);
xs_html_add(poll,
xs_html_tag("p",
xs_html_text(L("Closes in")),
xs_html_text(" "),
xs_html_text(p)));
}
}
}
xs_html_add(snac_content,
poll);
}
/** attachments **/
xs *attach = get_attachments(msg);
{
/* make custom css for attachments easier */
xs_html *content_attachments = xs_html_tag("div",
xs_html_attr("class", "snac-content-attachments"));
xs_html_add(snac_content,
content_attachments);
const char *content = xs_dict_get(msg, "content");
int c = 0;
const xs_dict *a;
while (xs_list_next(attach, &a, &c)) {
const char *type = xs_dict_get(a, "type");
const char *o_href = xs_dict_get(a, "href");
const char *name = xs_dict_get(a, "name");
if (!xs_is_string(type) || !xs_is_string(o_href))
continue;
/* if this URL is already in the post content, skip */
if (content && xs_str_in(content, o_href) != -1)
continue;
if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
continue;
/* do this attachment include an icon? */
const xs_dict *icon = xs_dict_get(a, "icon");
if (xs_type(icon) == XSTYPE_DICT) {
const char *icon_mtype = xs_dict_get(icon, "mediaType");
const char *icon_url = xs_dict_get(icon, "url");
if (icon_mtype && icon_url && xs_startswith(icon_mtype, "image/")) {
xs_html_add(content_attachments,
xs_html_tag("a",
xs_html_attr("href", icon_url),
xs_html_attr("target", "_blank"),
xs_html_sctag("img",
xs_html_attr("loading", "lazy"),
xs_html_attr("src", icon_url))));
}
}
xs *href = make_url(o_href, proxy, 0);
if (xs_startswith(type, "image/") || strcmp(type, "Image") == 0) {
xs_html_add(content_attachments,
xs_html_tag("a",
xs_html_attr("href", href),
xs_html_attr("target", "_blank"),
xs_html_sctag("img",
xs_html_attr("loading", "lazy"),
xs_html_attr("src", href),
xs_html_attr("alt", name),
xs_html_attr("title", name))));
}
else
if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
xs_html_add(content_attachments,
xs_html_tag("video",
xs_html_attr("preload", "none"),
xs_html_attr("style", "width: 100%"),
xs_html_attr("class", "snac-embedded-video"),
xs_html_attr("controls", NULL),
xs_html_attr("src", href),
xs_html_text(L("Video")),
xs_html_text(": "),
xs_html_tag("a",
xs_html_attr("href", href),
xs_html_text(name))));
}
else
if (xs_startswith(type, "audio/")) {
xs_html_add(content_attachments,
xs_html_tag("audio",
xs_html_attr("preload", "none"),
xs_html_attr("style", "width: 100%"),
xs_html_attr("class", "snac-embedded-audio"),
xs_html_attr("controls", NULL),
xs_html_attr("src", href),
xs_html_text(L("Audio")),
xs_html_text(": "),
xs_html_tag("a",
xs_html_attr("href", href),
xs_html_text(name))));
}
else
if (strcmp(type, "Link") == 0) {
xs_html_add(content_attachments,
xs_html_tag("p",
xs_html_tag("a",
xs_html_attr("href", o_href),
xs_html_text(href))));
/* do not generate an Alt... */
name = NULL;
}
else {
xs *d_href = xs_dup(o_href);
if (strlen(d_href) > 64) {
d_href[64] = '\0';
d_href = xs_str_cat(d_href, "...");
}
xs_html_add(content_attachments,
xs_html_tag("p",
xs_html_tag("a",
xs_html_attr("href", o_href),
xs_html_text(L("Attachment")),
xs_html_text(": "),
xs_html_text(d_href))));
/* do not generate an Alt... */
name = NULL;
}
if (name != NULL && *name) {
xs_html_add(content_attachments,
xs_html_tag("p",
xs_html_attr("class", "snac-alt-text"),
xs_html_tag("details",
xs_html_tag("summary",
xs_html_text(L("Alt..."))),
xs_html_text(name))));
}
}
}
/* has this message an audience (i.e., comes from a channel or community)? */
const char *audience = xs_dict_get(msg, "audience");
if (strcmp(type, "Page") == 0 && !xs_is_null(audience)) {
xs_html *au_tag = xs_html_tag("p",
xs_html_text("("),
xs_html_tag("a",
xs_html_attr("href", audience),
xs_html_attr("title", L("Source channel or community")),
xs_html_text(audience)),
xs_html_text(")"));
xs_html_add(snac_content_wrap,
au_tag);
}
/* does it have a location? */
const xs_dict *location = xs_dict_get(msg, "location");
if (xs_type(location) == XSTYPE_DICT) {
const xs_number *latitude = xs_dict_get(location, "latitude");
const xs_number *longitude = xs_dict_get(location, "longitude");
const char *name = xs_dict_get(location, "name");
const char *address = xs_dict_get(location, "address");
xs *label_list = xs_list_new();
if (xs_type(name) == XSTYPE_STRING)
label_list = xs_list_append(label_list, name);
if (xs_type(address) == XSTYPE_STRING)
label_list = xs_list_append(label_list, address);
if (xs_list_len(label_list)) {
const char *url = xs_dict_get(location, "url");
xs *label = xs_join(label_list, ", ");
if (xs_type(url) == XSTYPE_STRING) {
xs_html_add(snac_content_wrap,
xs_html_tag("p",
xs_html_text(L("Location: ")),
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("target", "_blank"),
xs_html_text(label))));
}
else
if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
xs_number_str(latitude), xs_number_str(longitude));
xs_html_add(snac_content_wrap,
xs_html_tag("p",
xs_html_text(L("Location: ")),
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("target", "_blank"),
xs_html_text(label))));
}
else
xs_html_add(snac_content_wrap,
xs_html_tag("p",
xs_html_text(L("Location: ")),
xs_html_text(label)));
}
}
if (strcmp(type, "Event") == 0) { /** Event start and end times **/
const char *s_time = xs_dict_get(msg, "startTime");
if (xs_is_string(s_time) && strlen(s_time) > 20) {
const char *e_time = xs_dict_get(msg, "endTime");
const char *tz = xs_dict_get(msg, "timezone");
xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
xs *e = NULL;
if (xs_is_string(e_time) && strlen(e_time) > 20)
e = xs_replace_i(xs_dup(e_time), "T", " ");
/* if the event has a timezone, crop the offsets */
if (xs_is_string(tz)) {
s = xs_crop_i(s, 0, 19);
if (e)
e = xs_crop_i(e, 0, 19);
}
else
tz = "";
/* if start and end share the same day, crop it from the end */
if (e && memcmp(s, e, 11) == 0)
e = xs_crop_i(e, 11, 0);
if (e)
s = xs_str_cat(s, " / ", e);
if (*tz)
s = xs_str_cat(s, " (", tz, ")");
/* replace ugly decimals */
s = xs_replace_i(s, ".000", "");
xs_html_add(snac_content_wrap,
xs_html_tag("p",
xs_html_text(L("Time: ")),
xs_html_text(s)));
}
}
/* show all hashtags that has not been shown previously in the content */
const xs_list *tags = xs_dict_get(msg, "tag");
const char *o_content = xs_dict_get_def(msg, "content", "");
if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
xs *content = xs_utf8_to_lower(o_content);
const xs_dict *tag;
xs_html *add_hashtags = xs_html_tag("ul",
xs_html_attr("class", "snac-more-hashtags"));
xs_list_foreach(tags, tag) {
const char *type = xs_dict_get(tag, "type");
if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
const char *o_href = xs_dict_get(tag, "href");
const char *name = xs_dict_get(tag, "name");
if (xs_is_string(o_href) && xs_is_string(name)) {
xs *href = xs_utf8_to_lower(o_href);
if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
/* not in the content: add here */
xs_html_add(add_hashtags,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", href),
xs_html_text(name),
xs_html_text(" "))));
}
}
}
}
xs_html_add(snac_content_wrap,
add_hashtags);
}
/** controls **/
if (!read_only && user) {
xs_html_add(entry,
html_entry_controls(user, actor, msg, md5));
}
/** children **/
if (!hide_children) {
xs *children = object_children(id);
int left = xs_list_len(children);
if (left) {
xs_html *ch_details = xs_html_tag("details",
xs_html_attr(collapse_threads ? "" : "open", NULL),
xs_html_tag("summary",
xs_html_text("...")));
xs_html_add(entry,
ch_details);
xs_html *fch_container = xs_html_tag("div",
xs_html_attr("class", "snac-thread-cont"));
xs_html_add(ch_details,
fch_container);
xs_html *ch_container = xs_html_tag("div",
xs_html_attr("class", level < 4 ? "snac-children" : "snac-children-too-deep"));
xs_html_add(ch_details,
ch_container);
xs_html *ch_older = NULL;
if (left > 3) {
xs_html_add(ch_container,
ch_older = xs_html_tag("details",
xs_html_tag("summary",
xs_html_text(L("Older...")))));
}
int ctxt = 0;
const char *cmd5;
int cnt = 0;
int o_cnt = 0;
int f_cnt = 0;
/* get the first child */
xs_list_next(children, &cmd5, &ctxt);
xs *f_chd = NULL;
if (user)
timeline_get_by_md5(user, cmd5, &f_chd);
else
object_get_by_md5(cmd5, &f_chd);
if (f_chd != NULL && xs_is_null(xs_dict_get(f_chd, "name"))) {
const char *p_author = get_atto(msg);
const char *author = get_atto(f_chd);
/* is the first child from the same author? */
if (xs_is_string(p_author) && xs_is_string(author) && strcmp(p_author, author) == 0) {
/* then, don't add it to the children container,
so that it appears unindented just before the parent
like a fucking Twitter-like thread */
xs_html_add(fch_container,
html_entry(user, f_chd, read_only, level + 1, cmd5, hide_children));
cnt++;
f_cnt++;
left--;
}
else
ctxt = 0; /* restart from the beginning */
}
while (xs_list_next(children, &cmd5, &ctxt)) {
xs *chd = NULL;
if (user)
timeline_get_by_md5(user, cmd5, &chd);
else
object_get_by_md5(cmd5, &chd);
if (chd != NULL) {
if (xs_is_null(xs_dict_get(chd, "name"))) {
xs_html *che = html_entry(user, chd, read_only,
level + 1, cmd5, hide_children);
if (che != NULL) {
if (left > 3) {
xs_html_add(ch_older,
che);
o_cnt++;
}
else
xs_html_add(ch_container,
che);
cnt++;
}
}
left--;
}
else
srv_debug(2, xs_fmt("cannot read child %s", cmd5));
}
/* if no children were finally added, hide the details */
if (cnt == 0)
xs_html_add(ch_details,
xs_html_attr("style", "display: none"));
if (o_cnt == 0 && ch_older)
xs_html_add(ch_older,
xs_html_attr("style", "display: none"));
if (f_cnt == 0)
xs_html_add(fch_container,
xs_html_attr("style", "display: none"));
}
}
/* add an invisible hr, to help differentiate between posts in text browsers */
xs_html_add(entry_top,
xs_html_sctag("hr",
xs_html_attr("hidden", NULL)));
return entry_top;
}
xs_html *html_footer(const snac *user)
{
return xs_html_tag("div",
xs_html_attr("class", "snac-footer"),
xs_html_tag("a",
xs_html_attr("href", srv_baseurl),
xs_html_text(L("about this site"))),
xs_html_text(" - "),
xs_html_text(L("powered by ")),
xs_html_tag("a",
xs_html_attr("href", WHAT_IS_SNAC_URL),
xs_html_tag("abbr",
xs_html_attr("title", "Social Networks Are Crap"),
xs_html_text(USER_AGENT))));
}
xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
int skip, int show, int show_more,
const char *title, const char *page,
int utl, const char *error, int terse)
/* returns the HTML for the timeline */
{
xs_list *p = (xs_list *)list;
const char *v;
double t = ftime();
int hide_children = xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")) && read_only;
xs *desc = NULL;
xs *alternate = NULL;
if (xs_list_len(list) == 1) {
/* only one element? pick the description from the source */
const char *id = xs_list_get(list, 0);
xs *d = NULL;
object_get_by_md5(id, &d);
const char *sc = xs_dict_get(d, "sourceContent");
if (d && sc != NULL)
desc = xs_dup(sc);
alternate = xs_dup(xs_dict_get(d, "id"));
}
xs_html *head;
xs_html *body;
if (user) {
head = html_user_head(user, desc, alternate);
if (terse)
body = xs_html_tag("body", NULL);
else
body = html_user_body(user, read_only);
}
else {
head = html_instance_head();
body = html_instance_body();
}
xs_html *html = xs_html_tag("html",
head,
body);
if (user && !read_only)
xs_html_add(body,
html_top_controls(user));
if (error != NULL) {
xs_html_add(body,
xs_html_tag("dialog",
xs_html_attr("open", NULL),
xs_html_tag("p",
xs_html_text(error)),
xs_html_tag("form",
xs_html_attr("method", "dialog"),
xs_html_sctag("input",
xs_html_attr("type", "submit"),
xs_html_attr("value", L("Dismiss"))))));
}
/* show links to the available lists */
if (user && !read_only) {
xs_html *lol = xs_html_tag("ul",
xs_html_attr("class", "snac-list-of-lists"));
xs_html_add(body, lol);
xs *lists = list_maint(user, NULL, 0); /* get list of lists */
int ct = 0;
const char *v;
while (xs_list_next(lists, &v, &ct)) {
const char *lname = xs_list_get(v, 1);
xs *url = xs_fmt("%s/list/%s", user->actor, xs_list_get(v, 0));
xs *ttl = xs_fmt(L("Timeline for list '%s'"), lname);
xs_html_add(lol,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_attr("title", ttl),
xs_html_text(lname))));
}
{
/* show the list of pinned posts */
xs *url = xs_fmt("%s/pinned", user->actor);
xs_html_add(lol,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_attr("title", L("Pinned posts")),
xs_html_text(L("pinned")))));
}
{
/* show the list of bookmarked posts */
xs *url = xs_fmt("%s/bookmarks", user->actor);
xs_html_add(lol,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_attr("title", L("Bookmarked posts")),
xs_html_text(L("bookmarks")))));
}
{
/* show the list of drafts */
xs *url = xs_fmt("%s/drafts", user->actor);
xs_html_add(lol,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_attr("title", L("Post drafts")),
xs_html_text(L("drafts")))));
}
{
/* show the list of scheduled posts */
xs *url = xs_fmt("%s/sched", user->actor);
xs_html_add(lol,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_attr("title", L("Scheduled posts")),
xs_html_text(L("scheduled posts")))));
}
/* the list of followed hashtags */
const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
xs_html *loht = xs_html_tag("ul",
xs_html_attr("class", "snac-list-of-lists"));
xs_html_add(body, loht);
const char *ht;
xs_list_foreach(followed_hashtags, ht) {
xs *url = NULL;
if (!xs_startswith(ht, "https:/""/"))
url = xs_fmt("%s/admin?q=%s", user->actor, ht);
else
url = xs_dup(ht);
url = xs_replace_i(url, "#", "%23");
xs_html_add(loht,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("class", "snac-list-link"),
xs_html_text(ht))));
}
}
}
xs_html_add(body,
xs_html_tag("a",
xs_html_attr("name", "snac-posts")));
xs_html *posts = xs_html_tag("div",
xs_html_attr("class", "snac-posts"));
if (title) {
xs_html_add(posts,
xs_html_tag("h2",
xs_html_attr("class", "snac-header"),
xs_html_text(title)));
}
xs_html_add(body, posts);
int mark_shown = 0;
while (xs_list_iter(&p, &v)) {
xs *msg = NULL;
int status;
/* "already seen" mark? */
if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
if (skip == 0 && !mark_shown) {
xs *s = xs_fmt("%s/admin", user->actor);
xs_html_add(posts,
xs_html_tag("div",
xs_html_attr("class", "snac-no-more-unseen-posts"),
xs_html_text(L("No more unseen posts")),
xs_html_text(" - "),
xs_html_tag("a",
xs_html_attr("href", s),
xs_html_text(L("Back to top")))));
}
mark_shown = 1;
continue;
}
if (utl && user && !is_pinned_by_md5(user, v))
status = timeline_get_by_md5(user, v, &msg);
else
status = object_get_by_md5(v, &msg);
if (!valid_status(status))
continue;
/* if it's an instance page, discard messages from private users */
if (user == NULL && is_msg_from_private_user(msg))
continue;
const int scope = get_msg_visibility(msg);
if (user != NULL && scope != SCOPE_PUBLIC){
/* is this message a non-public reply? */
const char *irt = get_in_reply_to(msg);
/* is it a reply to something not in the storage? */
if (!xs_is_null(irt) && !object_here(irt)) {
/* is it for me? */
const xs_list *to = xs_dict_get_def(msg, "to", xs_stock(XSTYPE_LIST));
const xs_list *cc = xs_dict_get_def(msg, "cc", xs_stock(XSTYPE_LIST));
if (xs_list_in(to, user->actor) == -1 && xs_list_in(cc, user->actor) == -1) {
snac_debug(user, 1, xs_fmt("skipping non-public reply to an unknown post %s", v));
continue;
}
}
}
/* hide non-public posts from /instance view */
if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){
continue;
}
/* hide non-public posts viewed from outside */
if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) {
continue;
}
xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
if (entry != NULL)
xs_html_add(posts,
entry);
}
if (list && user && read_only) {
/** history **/
if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
xs_html *ul = xs_html_tag("ul", NULL);
xs_html *history = xs_html_tag("div",
xs_html_attr("class", "snac-history"),
xs_html_tag("p",
xs_html_attr("class", "snac-history-title"),
xs_html_text(L("History"))),
ul);
xs *list = history_list(user);
xs_list *p = list;
const char *v;
while (xs_list_iter(&p, &v)) {
xs *fn = xs_replace(v, ".html", "");
xs *url = xs_fmt("%s/h/%s", user->actor, v);
xs_html_add(ul,
xs_html_tag("li",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_text(fn))));
}
xs_html_add(body,
history);
}
}
{
xs *s1 = xs_fmt("\n\n", ftime() - t);
xs_html_add(body,
xs_html_raw(s1));
}
if (show_more) {
xs *m = NULL;
xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
if (page != NULL)
url = xs_str_cat(url, page);
if (xs_str_in(url, "?") != -1)
m = xs_fmt("%s&%s", url, ss);
else
m = xs_fmt("%s?%s", url, ss);
xs_html *more_links = xs_html_tag("p",
xs_html_tag("a",
xs_html_attr("href", url),
xs_html_attr("name", "snac-more"),
xs_html_text(L("Back to top"))),
xs_html_text(" - "),
xs_html_tag("a",
xs_html_attr("href", m),
xs_html_attr("name", "snac-more"),
xs_html_text(L("More..."))));
xs_html_add(body,
more_links);
}
xs_html_add(body,
html_footer(user));
return xs_html_render_s(html, "\n");
}
xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
{
xs_html *snac_posts;
xs_html *people = xs_html_tag("div",
xs_html_tag("h2",
xs_html_attr("class", "snac-header"),
xs_html_text(header)),
snac_posts = xs_html_tag("details",
xs_html_attr("open", NULL),
xs_html_tag("summary",
xs_html_text("..."))));
xs *redir = xs_fmt("%s/people", user->actor);
const char *actor_id;
xs_list_foreach(list, actor_id) {
xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
xs *actor = NULL;
if (valid_status(actor_get(actor_id, &actor))) {
xs_html *snac_post = xs_html_tag("div",
xs_html_attr("class", "snac-post"),
xs_html_tag("a",
xs_html_attr("name", md5)),
xs_html_tag("div",
xs_html_attr("class", "snac-post-header"),
html_actor_icon(user, actor, xs_dict_get(actor, "published"),
NULL, NULL, -1, 1, proxy, NULL, NULL)));
/* content (user bio) */
const char *c = xs_dict_get(actor, "summary");
const xs_val *tag = xs_dict_get(actor, "tag");
if (!xs_is_null(c)) {
xs *sc = sanitize(c);
sc = replace_shortnames(sc, tag, 2, proxy);
xs_html *snac_content = xs_html_tag("div",
xs_html_attr("class", "snac-content"));
if (xs_startswith(sc, "
")) xs_html_add(snac_content, xs_html_raw(sc)); /* already sanitized */ else xs_html_add(snac_content, xs_html_tag("p", xs_html_raw(sc))); /* already sanitized */ xs_html_add(snac_post, snac_content); } /* add user metadata */ xs_html *snac_metadata = xs_html_tag("div", xs_html_attr("class", "snac-metadata")); int count = 0; const xs_val *address = xs_dict_get(actor, "vcard:Address"); if (xs_is_string(address)) { xs_html_add(snac_metadata, xs_html_tag("span", xs_html_attr("class", "snac-property-name"), xs_html_raw("📍 Location")), xs_html_text(":"), xs_html_raw(" "), xs_html_tag("span", xs_html_attr("class", "snac-property-value p-adr"), xs_html_text(address)), xs_html_sctag("br", NULL)); count++; } const xs_val *birthday = xs_dict_get(actor, "vcard:bday"); if (xs_is_string(birthday)) { xs_html_add(snac_metadata, xs_html_tag("span", xs_html_attr("class", "snac-property-name"), xs_html_raw("🎂 Birthday")), xs_html_text(":"), xs_html_raw(" "), xs_html_tag("time", xs_html_attr("class", "snac-property-value dt-bday"), xs_html_text(birthday)), xs_html_sctag("br", NULL)); count++; } const xs_list *attachment = xs_dict_get(actor, "attachment"); if (count > 0 && xs_list_len(attachment) > 0) { xs_html_add(snac_metadata, xs_html_sctag("hr", xs_html_attr("class", "snac-property-divider"))); } const xs_val *v; xs_list_foreach(attachment, v) { const char *type = xs_dict_get(v, "type"); const char *name = xs_dict_get(v, "name"); const char *value = xs_dict_get(v, "value"); if (!xs_is_null(type) && !xs_is_null(name) && !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) { /* both the name and the value can contain emoji */ xs *nam = sanitize(name); nam = replace_shortnames(nam, tag, 1, proxy); /* todo: sometimes the value is transmitted as markdown and not html ._. */ xs *val = sanitize(value); val = replace_shortnames(val, tag, 1, proxy); /* delete
tags, because some software sends them */ val = xs_replace_i(val, "
", ""); val = xs_replace_i(val, "
", ""); xs_html_add(snac_metadata, xs_html_tag("span", xs_html_attr("class", "snac-property-name"), xs_html_raw(nam)), xs_html_text(":"), xs_html_raw(" "), xs_html_tag("span", xs_html_attr("class", "snac-property-value"), xs_html_raw(val)), xs_html_sctag("br", NULL)); count++; } } if (count > 0) { xs_html_add(snac_post, snac_metadata); } else { /* free the html, by rendering it... */ xs_free(xs_html_render(snac_metadata)); } /* buttons */ xs *btn_form_action = xs_fmt("%s/admin/action", user->actor); xs_html *snac_controls = xs_html_tag("div", xs_html_attr("class", "snac-controls")); xs_html *form = xs_html_tag("form", xs_html_attr("autocomplete", "off"), xs_html_attr("method", "post"), xs_html_attr("action", btn_form_action), xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "actor"), xs_html_attr("value", actor_id)), xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "hard-redir"), xs_html_attr("value", redir)), xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "actor-form"), xs_html_attr("value", "yes"))); xs_html_add(snac_controls, form); if (following_check(user, actor_id)) { xs_html_add(form, html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"))); if (is_limited(user, actor_id)) xs_html_add(form, html_button("unlimit", L("Unlimit"), L("Allow announces (boosts) from this user"))); else xs_html_add(form, html_button("limit", L("Limit"), L("Block announces (boosts) from this user"))); } else { xs_html_add(form, html_button("follow", L("Follow"), L("Start following this user's activity"))); if (follower_check(user, actor_id)) xs_html_add(form, html_button("delete", L("Delete"), L("Delete this user"))); } if (pending_check(user, actor_id)) { xs_html_add(form, html_button("approve", L("Approve"), L("Approve this follow request"))); xs_html_add(form, html_button("discard", L("Discard"), L("Discard this follow request"))); } if (is_muted(user, actor_id)) xs_html_add(form, html_button("unmute", L("Unmute"), L("Stop blocking activities from this user"))); else xs_html_add(form, html_button("mute", L("MUTE"), L("Block any activity from this user"))); /* the post textarea */ xs *dm_div_id = xs_fmt("%s_%s_dm", md5, t); xs *dm_form_id = xs_fmt("%s_reply_form", md5); xs_html_add(snac_controls, xs_html_tag("p", NULL), html_note(user, L("Direct Message..."), dm_div_id, dm_form_id, "", "", NULL, actor_id, xs_stock(XSTYPE_FALSE), "", SCOPE_MENTIONED, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL), xs_html_tag("p", NULL)); xs_html_add(snac_post, snac_controls); xs_html_add(snac_posts, snac_post); } } return people; } xs_str *html_people(snac *user) { const char *proxy = NULL; if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; xs *wing = following_list(user); xs *wers = follower_list(user); xs *pending = pending_list(user); xs_html *lists = xs_html_tag("div", xs_html_attr("class", "snac-posts")); if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { xs_html_add(lists, html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); } xs_html_add(lists, html_people_list(user, wing, L("People you follow"), "i", proxy), html_people_list(user, wers, L("People that follow you"), "e", proxy)); xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), xs_html_add(html_user_body(user, 0), lists, html_footer(user))); return xs_html_render_s(html, "\n"); } /* Filter list to display only posts by actor. We'll probably show fewer than show posts. Should we try harder to find some? */ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, int skip, int show, int show_more, const char *page) { const char *proxy = NULL; xs_list *p = (xs_list *)list; const char *v; if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; xs_html *body = html_user_body(user, 0); xs_html *lists = xs_html_tag("div", xs_html_attr("class", "snac-posts")); xs *foll = xs_list_append(xs_list_new(), actor); xs_html_add(lists, html_people_list(user, foll, L("Contact's posts"), "p", proxy)); xs_html_add(body, lists); while (xs_list_iter(&p, &v)) { xs *msg = NULL; int status; status = timeline_get_by_md5(user, v, &msg); if (!valid_status(status)) continue; const char *id = xs_dict_get(msg, "id"); const char *by = get_atto(msg); xs *actor_md5 = NULL; xs_list *boosts = NULL; xs_list *likes = NULL; xs_list *reacts = NULL; /* Besides actor's posts, also show actor's boosts, and also posts by user with likes or reacts by actor. I.e., any actor's actions that user could have seen in the timeline or in notifications. */ if (!(by && strcmp(actor, by) == 0) && xs_list_in((boosts = object_announces(id)), (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 && (!(by && strcmp(user->actor, by) == 0) || (xs_list_in((likes = object_likes(id)), actor_md5) == -1 && xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1))) continue; xs_html *entry = html_entry(user, msg, 0, 0, v, 1); if (entry != NULL) xs_html_add(lists, entry); } if (show_more) { xs *m = NULL; xs *m10 = NULL; xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show); xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor); if (page != NULL) url = xs_str_cat(url, page); if (xs_str_in(url, "?") != -1) m = xs_fmt("%s&%s", url, ss); else m = xs_fmt("%s?%s", url, ss); m10 = xs_fmt("%s0", m); xs_html *more_links = xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", url), xs_html_attr("name", "snac-more"), xs_html_text(L("Back to top"))), xs_html_text(" - "), xs_html_tag("a", xs_html_attr("href", m), xs_html_attr("name", "snac-more"), xs_html_text(L("More..."))), xs_html_text(" - "), xs_html_tag("a", xs_html_attr("href", m10), xs_html_attr("name", "snac-more"), xs_html_text(L("More (x 10)...")))); xs_html_add(body, more_links); } xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), xs_html_add(body, html_footer(user))); return xs_html_render_s(html, "\n"); } xs_str *html_notifications(snac *user, int skip, int show) { const char *proxy = NULL; if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; xs *n_list = notify_list(user, skip, show); xs *n_time = notify_check_time(user, 0); xs_html *body = html_user_body(user, 0); xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), body); xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor); xs_html_add(body, xs_html_tag("form", xs_html_attr("autocomplete", "off"), xs_html_attr("method", "post"), xs_html_attr("action", clear_all_action), xs_html_attr("id", "clear"), xs_html_sctag("input", xs_html_attr("type", "submit"), xs_html_attr("class", "snac-btn-like"), xs_html_attr("value", L("Clear all"))))); xs_html *noti_new = NULL; xs_html *noti_seen = NULL; xs_html *posts = xs_html_tag("div", xs_html_attr("class", "snac-posts")); xs_html_add(body, posts); xs_set rep; xs_set_init(&rep); /* dict to store previous notification labels */ xs *admiration_labels = xs_dict_new(); const xs_str *v; xs_list_foreach(n_list, v) { xs *noti = notify_get(user, v); if (noti == NULL) continue; xs *obj = NULL; const char *type = xs_dict_get(noti, "type"); const char *utype = xs_dict_get(noti, "utype"); const char *id = xs_dict_get(noti, "objid"); const char *date = xs_dict_get(noti, "date"); const char *id2 = xs_dict_get_path(noti, "msg.id"); xs *wrk = NULL; if (xs_is_null(id)) continue; if (is_hidden(user, id)) continue; if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) continue; if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) continue; object_get(id, &obj); const char *msg_id = NULL; if (xs_is_dict(obj)) msg_id = xs_dict_get(obj, "id"); const char *actor_id = xs_dict_get(noti, "actor"); xs *actor = NULL; xs *a_name = NULL; if (valid_status(actor_get(actor_id, &actor))) a_name = actor_name(actor, proxy); else { a_name = xs_dup(actor_id); /* actor not here: request it */ enqueue_actor_refresh(user, actor_id, 0); } xs *label_sanitized = sanitize(type); const char *label = label_sanitized; if (strcmp(type, "Create") == 0) label = L("Mention"); else if (strcmp(type, "Update") == 0 && strcmp(utype, "Question") == 0) label = L("Finished poll"); else if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0) label = L("Unfollow"); else if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) { const char *content = xs_dict_get_path(noti, "msg.content"); xs *cd = xs_dup(content); const char *sna = cd; const xs_dict *tag = xs_dict_get_path(noti, "msg.tag"); unsigned int utf = xs_utf8_dec((const char **)&sna); int isEmoji = 0; if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0)) isEmoji = 1; if (xs_type(content) == XSTYPE_STRING) { xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy); wrk = xs_fmt("%s (%s️)", isEmoji ? "EmojiReact" : "Like", emoji); label = wrk; } } else if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id)) label = L("Follow Request"); xs *s_date = xs_crop_i(xs_dup(date), 0, 10); xs_html *this_html_label = xs_html_container( xs_html_tag("b", xs_html_raw(label), xs_html_text(" by "), xs_html_tag("a", xs_html_attr("href", actor_id), xs_html_raw(a_name))), /* a_name is already sanitized */ xs_html_text(" "), xs_html_tag("time", xs_html_attr("class", "dt-published snac-pubdate"), xs_html_attr("title", date), xs_html_text(s_date))); xs_html *html_label = NULL; if (xs_is_string(msg_id)) { const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id); if (xs_type(prev_label) == XSTYPE_DATA) { /* there is a previous list of admiration labels! */ xs_data_get(&html_label, prev_label); xs_html_add(html_label, xs_html_sctag("br", NULL), this_html_label); continue; } } xs_html *entry = NULL; html_label = xs_html_tag("p", this_html_label); /* store in the admiration labels dict */ xs *pl = xs_data_new(&html_label, sizeof(html_label)); if (xs_is_string(msg_id)) admiration_labels = xs_dict_set(admiration_labels, msg_id, pl); entry = xs_html_tag("div", xs_html_attr("class", "snac-post-with-desc"), html_label); if (strcmp(type, "Block") == 0) { if (actor) xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL))); } else if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) { if (actor) { xs *action = xs_fmt("%s/admin/action", user->actor); xs_html *button = NULL; if (following_check(user, actor_id)) button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")); else button = html_button("follow", L("Follow"), L("Start following this user's activity")); xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL), xs_html_tag("form", xs_html_attr("method", "post"), xs_html_attr("action", action), xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "actor"), xs_html_attr("value", actor_id)), button, xs_html_sctag("br", NULL)))); } } else if (strcmp(type, "Move") == 0) { const xs_dict *o_msg = xs_dict_get(noti, "msg"); const char *target; if (xs_type(o_msg) == XSTYPE_DICT && (target = xs_dict_get(o_msg, "target"))) { xs *old_actor = NULL; if (valid_status(actor_get(target, &old_actor))) { xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL))); } } } else if (obj != NULL) { xs *md5 = xs_md5_hex(id, strlen(id)); xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5); xs_html *h = html_entry(user, obj, 0, 0, md5, 1); if (h != NULL) { xs_html_add(entry, xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", ctxt), xs_html_text(L("Context")))), h); } else xs_html_add(entry, xs_html_tag("p", xs_html_text(L("Location: ")), xs_html_tag("a", xs_html_attr("href", id), xs_html_text(id)))); } if (strcmp(v, n_time) > 0) { /* unseen notification */ if (noti_new == NULL) { noti_new = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), xs_html_text(L("New")))); xs_html_add(posts, noti_new); } xs_html_add(noti_new, entry); } else { /* already seen notification */ if (noti_seen == NULL) { noti_seen = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), xs_html_text(L("Already seen")))); xs_html_add(posts, noti_seen); } xs_html_add(noti_seen, entry); } } if (noti_new == NULL && noti_seen == NULL) xs_html_add(body, xs_html_tag("h2", xs_html_attr("class", "snac-header"), xs_html_text(L("None")))); /* add the navigation footer */ xs *next_p = notify_list(user, skip + show, 1); if (xs_list_len(next_p)) { xs *url = xs_fmt("%s/notifications?skip=%d&show=%d", user->actor, skip + show, show); xs_html_add(body, xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", url), xs_html_text(L("More..."))))); } xs_set_free(&rep); xs_html_add(body, html_footer(user)); /* set the check time to now */ xs *dummy = notify_check_time(user, 1); dummy = xs_free(dummy); timeline_touch(user); return xs_html_render_s(html, "\n"); } void set_user_lang(snac *user) /* sets the language dict according to user configuration */ { user->lang = NULL; const char *lang = xs_dict_get(user->config, "lang"); if (xs_is_string(lang)) user->lang = xs_dict_get(srv_langs, lang); } int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, xs_str **etag, xs_str **last_modified) { const char *accept = xs_dict_get(req, "accept"); int status = HTTP_STATUS_NOT_FOUND; const snac *user = NULL; snac snac; xs *uid = NULL; const char *p_path; int cache = 1; int save = 1; int proxy = 0; int terse = 0; const char *v; const xs_dict *q_vars = xs_dict_get(req, "q_vars"); xs *l = xs_split_n(q_path, "/", 2); v = xs_list_get(l, 1); if (xs_is_null(v)) { srv_log(xs_fmt("html_get_handler bad query '%s'", q_path)); return HTTP_STATUS_NOT_FOUND; } if (!xs_is_null(xs_dict_get(q_vars, "terse"))) terse = 1; if (strcmp(v, "share-bridge") == 0) { /* temporary redirect for a post */ const char *login = xs_dict_get(q_vars, "login"); const char *content = xs_dict_get(q_vars, "content"); if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) { xs *b64 = xs_base64_enc(content, strlen(content)); srv_log(xs_fmt("share-bridge for user '%s'", login)); *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64); return HTTP_STATUS_SEE_OTHER; } else return HTTP_STATUS_NOT_FOUND; } else if (strcmp(v, "auth-int-bridge") == 0) { const char *login = xs_dict_get(q_vars, "login"); const char *id = xs_dict_get(q_vars, "id"); const char *action = xs_dict_get(q_vars, "action"); if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) { *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s", srv_baseurl, login, action, id); return HTTP_STATUS_SEE_OTHER; } else return HTTP_STATUS_NOT_FOUND; } uid = xs_dup(v); /* rss extension? */ if (xs_endswith(uid, ".rss")) { uid = xs_crop_i(uid, 0, -4); p_path = ".rss"; } else p_path = xs_list_get(l, 2); if (!uid || !user_open(&snac, uid)) { /* invalid user */ status = grave(uid, 0) ? HTTP_STATUS_GONE : HTTP_STATUS_NOT_FOUND; srv_debug(1, xs_fmt("html_get_handler bad user %s %d", uid, status)); return status; } user = &snac; /* for L() */ set_user_lang(&snac); if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = 1; /* return the RSS if requested by Accept header */ if (accept != NULL) { if (xs_str_in(accept, "text/xml") != -1 || xs_str_in(accept, "application/rss+xml") != -1) p_path = ".rss"; } /* check if server config variable 'disable_cache' is set */ if ((v = xs_dict_get(srv_config, "disable_cache")) && xs_type(v) == XSTYPE_TRUE) cache = 0; int skip = 0; const char *max_show_default = "50"; int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries", max_show_default)); int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", xs_dict_get_def(srv_config, "max_timeline_entries", max_show_default))); int show = def_show; if ((v = xs_dict_get(q_vars, "skip")) != NULL) skip = atoi(v), cache = 0, save = 0; if ((v = xs_dict_get(q_vars, "show")) != NULL) show = atoi(v), cache = 0, save = 0; if ((v = xs_dict_get(q_vars, "da")) != NULL) { /* user dismissed an announcement */ if (login(&snac, req)) { double ts = atof(v); xs *timestamp = xs_number_new(ts); srv_log(xs_fmt("user dismissed announcements until %d", ts)); snac.config = xs_dict_set(snac.config, "last_announcement", timestamp); user_persist(&snac, 0); } } /* get a possible error message */ const char *error = xs_dict_get(q_vars, "error"); if (error != NULL) cache = 0; /* a show of 0 has no sense */ if (show == 0) show = def_show; if (show > max_show) show = max_show; if (p_path == NULL) { /** public timeline **/ xs *h = xs_str_localtime(0, "%Y-%m.html"); if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) { /** empty public timeline for private users **/ *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } else if (cache && history_mtime(&snac, h) > timeline_mtime(&snac)) { snac_debug(&snac, 1, xs_fmt("serving cached local timeline")); status = history_get(&snac, h, body, b_size, xs_dict_get(req, "if-none-match"), etag); } else { xs *list = NULL; int more = 0; if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) list = timeline_simple_list(&snac, "public", skip, show, &more); else list = timeline_list(&snac, "public", skip, show, &more); xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; if (save) history_add(&snac, h, *body, *b_size, etag); } } else if (strcmp(p_path, "admin") == 0) { /** private timeline **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { const char *q = NULL; xs *cq = xs_dup(xs_dict_get(q_vars, "q")); xs *url_acct = NULL; if (xs_is_string(cq)) { cq = xs_strip_i(cq); q = cq; } /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ xs *actor_obj = NULL; if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) { /* it's an actor; do the dirty trick of changing q to the account name */ q = url_acct; } else { /* bring it to the user's timeline */ xs *object = NULL; int status; status = activitypub_request(&snac, q, &object); snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status)); if (valid_status(status)) { /* got it; also request the actor */ const char *attr_to = get_atto(object); if (!xs_is_null(attr_to)) { status = actor_request(&snac, attr_to, &actor_obj); if (valid_status(status)) { /* reset the query string to be the real id */ url_acct = xs_dup(xs_dict_get(object, "id")); q = url_acct; /* add the post to the timeline */ if (!timeline_here(&snac, q)) timeline_add(&snac, q, object); } } else { /* retry webfinger, this time with the 'official' id */ const char *id = xs_dict_get(object, "id"); if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) && xs_is_string(url_acct)) q = url_acct; } } } /* fall through */ } if (q && *q) { if (xs_regex_match(q, "^@?[a-zA-Z0-9._]+@[a-zA-Z0-9-]+\\.")) { /** search account **/ xs *actor = NULL; xs *acct = NULL; xs *l = xs_list_new(); xs_html *page = NULL; if (valid_status(webfinger_request(q, &actor, &acct))) { xs *actor_obj = NULL; if (valid_status(actor_request(&snac, actor, &actor_obj))) { actor_add(actor, actor_obj); /* create a people list with only one element */ l = xs_list_append(l, actor); xs *title = xs_fmt(L("Search results for account %s"), q); page = html_people_list(&snac, l, title, "wf", NULL); } } if (page == NULL) { xs *title = xs_fmt(L("Account %s not found"), q); page = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), xs_html_text(title))); } xs_html *html = xs_html_tag("html", html_user_head(&snac, NULL, NULL), xs_html_add(html_user_body(&snac, 0), page, html_footer(user))); *body = xs_html_render_s(html, "\n"); *b_size = strlen(*body); status = HTTP_STATUS_OK; } else if (*q == '#') { /** search by tag **/ xs *tl = tag_search(q, skip, show + 1); int more = 0; if (xs_list_len(tl) >= show + 1) { /* drop the last one */ tl = xs_list_del(tl, -1); more = 1; } xs *page = xs_fmt("/admin?q=%%23%s", q + 1); xs *title = xs_fmt(xs_list_len(tl) ? L("Search results for tag %s") : L("Nothing found for tag %s"), q); *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } else { /** search by content **/ int to = 0; int msecs = atoi(xs_dict_get_def(q_vars, "msecs", "0")); xs *tl = content_search(&snac, q, 1, skip, show, msecs, &to); xs *title = NULL; xs *page = xs_fmt("/admin?q=%s&msecs=%d", q, msecs + 10); int tl_len = xs_list_len(tl); if (to) title = xs_fmt(L("Search results for '%s' (may be more)"), q); else if (tl_len) title = xs_fmt(L("Search results for '%s'"), q); else if (skip) title = xs_fmt(L("No more matches for '%s'"), q); else title = xs_fmt(L("Nothing found for '%s'"), q); *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else { /** the private timeline **/ double t = history_mtime(&snac, "timeline.html_"); /* if enabled by admin, return a cached page if its timestamp is: a) newer than the timeline timestamp b) newer than the start time of the server */ if (cache && t > timeline_mtime(&snac) && t > p_state->srv_start_time) { snac_debug(&snac, 1, xs_fmt("serving cached timeline")); status = history_get(&snac, "timeline.html_", body, b_size, xs_dict_get(req, "if-none-match"), etag); } else { int more = 0; snac_debug(&snac, 1, xs_fmt("building timeline")); xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, more, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; if (save) history_add(&snac, "timeline.html_", *body, *b_size, etag); timeline_add_mark(&snac); } } } } else if (xs_startswith(p_path, "admin/p/")) { /** unique post by md5 **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *l = xs_split(p_path, "/"); const char *md5 = xs_list_get(l, -1); if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) { xs *list0 = xs_list_append(xs_list_new(), md5); xs *list = timeline_top_level(&snac, list0); *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } } else if (strcmp(p_path, "people") == 0) { /** the list of people **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { *body = html_people(&snac); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (xs_startswith(p_path, "people/")) { /** a single actor **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *actor_dict = NULL; const char *actor_id = NULL; xs *actor = NULL; xs_list *page_lst = xs_split_n(p_path, "?", 2); xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0)); xs_list *l = xs_split_n(page, "/", 3); const char *actor_md5 = xs_list_get(l, 2); if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) && (actor_id = xs_dict_get(actor_dict, "id")) != NULL && valid_status(actor_get(actor_id, &actor))) { int more = 0; xs *list = timeline_simple_list(&snac, "private", skip, show, &more); *body = html_people_one(&snac, actor_id, list, skip, show, more, page); *b_size = strlen(*body); status = HTTP_STATUS_OK; } else { *body = xs_dup(uid); status = HTTP_STATUS_NOT_FOUND; } } } else if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { *body = html_notifications(&snac, skip, show); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (strcmp(p_path, "instance") == 0) { /** instance timeline **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *list = timeline_instance_list(skip, show); xs *next = timeline_instance_list(skip + show, 1); *body = html_timeline(&snac, list, 0, skip, show, xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (strcmp(p_path, "pinned") == 0) { /** list of pinned posts **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *list = pinned_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, 0, L("Pinned posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (strcmp(p_path, "bookmarks") == 0) { /** list of bookmarked posts **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *list = bookmark_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, 0, L("Bookmarked posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (strcmp(p_path, "drafts") == 0) { /** list of drafts **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *list = draft_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, 0, L("Post drafts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *list = scheduled_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, 0, L("Scheduled posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (xs_startswith(p_path, "list/")) { /** list timelines **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { xs *l = xs_split(p_path, "/"); const char *lid = xs_list_get(l, -1); xs *list = list_timeline(&snac, lid, skip, show); xs *next = list_timeline(&snac, lid, skip + show, 1); if (list != NULL) { xs *ttl = timeline_top_level(&snac, list); xs *base = xs_fmt("/list/%s", lid); xs *name = list_maint(&snac, lid, 3); xs *title = xs_fmt(L("Showing timeline for list '%s'"), name); *body = html_timeline(&snac, ttl, 0, skip, show, xs_list_len(next), title, base, 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } } else if (xs_startswith(p_path, "p/")) { /** a timeline with just one entry **/ if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) return HTTP_STATUS_FORBIDDEN; xs *id = xs_fmt("%s/%s", snac.actor, p_path); xs *msg = NULL; if (valid_status(object_get(id, &msg)) && is_msg_public(msg)) { xs *md5 = xs_md5_hex(id, strlen(id)); xs *list = xs_list_new(); list = xs_list_append(list, md5); *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } } else if (xs_startswith(p_path, "s/")) { /** a static file **/ xs *l = xs_split(p_path, "/"); const char *id = xs_list_get(l, 1); int sz; if (id && *id) { status = static_get(&snac, id, body, &sz, xs_dict_get(req, "if-none-match"), etag); if (valid_status(status)) { *b_size = sz; *ctype = (char *)xs_mime_by_ext(id); } } } else if (xs_startswith(p_path, "h/")) { /** an entry from the history **/ if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) return HTTP_STATUS_FORBIDDEN; if (xs_type(xs_dict_get(srv_config, "disable_history")) == XSTYPE_TRUE) return HTTP_STATUS_FORBIDDEN; xs *l = xs_split(p_path, "/"); const char *id = xs_list_get(l, 1); if (id && *id) { if (xs_endswith(id, "timeline.html_")) { /* Don't let them in */ *b_size = 0; status = HTTP_STATUS_NOT_FOUND; } else status = history_get(&snac, id, body, b_size, xs_dict_get(req, "if-none-match"), etag); } } else if (strcmp(p_path, ".rss") == 0) { /** public timeline in RSS format **/ if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) return HTTP_STATUS_FORBIDDEN; int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL); xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); xs *rss_title = xs_fmt("%s (@%s@%s)", xs_dict_get(snac.config, "name"), snac.uid, xs_dict_get(srv_config, "host")); xs *rss_link = xs_fmt("%s.rss", snac.actor); *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio); *b_size = strlen(*body); *ctype = "application/rss+xml; charset=utf-8"; status = HTTP_STATUS_OK; snac_debug(&snac, 1, xs_fmt("serving RSS")); } else if (proxy && (xs_startswith(p_path, "x/") || xs_startswith(p_path, "y/"))) { /** remote media by proxy **/ xs *proxy_prefix = NULL; if (xs_startswith(p_path, "x/")) { /* proxy usage authorized by http basic auth */ if (login(&snac, req)) proxy_prefix = xs_str_new("x/"); else { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } } else { /* proxy usage authorized by proxy_token */ xs *tks = xs_fmt("%s:%s", srv_proxy_token_seed, snac.actor); xs *tk = xs_md5_hex(tks, strlen(tks)); xs *p = xs_fmt("y/%s/", tk); if (xs_startswith(p_path, p)) proxy_prefix = xs_dup(p); } if (proxy_prefix) { /* pick the raw path (including optional ? arguments) */ const char *raw_path = xs_dict_get(req, "raw_path"); /* skip to where the proxy/ string starts */ raw_path += xs_str_in(raw_path, proxy_prefix); xs *url = xs_replace_n(raw_path, proxy_prefix, "https:/" "/", 1); xs *hdrs = xs_dict_new(); hdrs = xs_dict_append(hdrs, "user-agent", USER_AGENT); const char *ims = xs_dict_get(req, "if-modified-since"); const char *inm = xs_dict_get(req, "if-none-match"); if (ims) hdrs = xs_dict_append(hdrs, "if-modified-since", ims); if (inm) hdrs = xs_dict_append(hdrs, "if-none-match", inm); xs *rsp = xs_http_request("GET", url, hdrs, NULL, 0, &status, body, b_size, 0); if (valid_status(status)) { const char *ct = xs_or(xs_dict_get(rsp, "content-type"), ""); const char *lm = xs_dict_get(rsp, "last-modified"); const char *et = xs_dict_get(rsp, "etag"); if (lm) *last_modified = xs_dup(lm); if (et) *etag = xs_dup(et); /* find the content-type in the static mime types, and return that value instead of ct, which will be destroyed when out of scope */ for (int n = 0; xs_mime_types[n]; n += 2) { if (strcmp(ct, xs_mime_types[n + 1]) == 0) { *ctype = (char *)xs_mime_types[n + 1]; break; } } } snac_debug(&snac, 1, xs_fmt("Proxy for %s %d", url, status)); } } else if (strcmp(p_path, "share") == 0) { /** direct post **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { const char *b64 = xs_dict_get(q_vars, "content"); int sz; xs *content = xs_base64_dec(b64, &sz); xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL, NULL); xs *c_msg = msg_create(&snac, msg); timeline_add(&snac, xs_dict_get(msg, "id"), msg); enqueue_message(&snac, c_msg); snac_debug(&snac, 1, xs_fmt("web action 'share' received")); *body = xs_fmt("%s/admin", snac.actor); *b_size = strlen(*body); status = HTTP_STATUS_SEE_OTHER; } } else if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; } else { status = HTTP_STATUS_NOT_FOUND; const char *id = xs_dict_get(q_vars, "id"); const char *action = xs_dict_get(q_vars, "action"); if (xs_is_string(id) && xs_is_string(action)) { if (strcmp(action, "Follow") == 0) { xs *msg = msg_follow(&snac, id); if (msg != NULL) { const char *actor = xs_dict_get(msg, "object"); following_add(&snac, actor, msg); enqueue_output_by_actor(&snac, msg, actor, 0); status = HTTP_STATUS_SEE_OTHER; } } else if (xs_match(action, "Like|Boost|Announce")) { /* bring the post */ xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce"); if (msg != NULL) { timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg); enqueue_message(&snac, msg); status = HTTP_STATUS_SEE_OTHER; } } } if (status == HTTP_STATUS_SEE_OTHER) { *body = xs_fmt("%s/admin", snac.actor); *b_size = strlen(*body); } } } else status = HTTP_STATUS_NOT_FOUND; user_free(&snac); if (valid_status(status) && *ctype == NULL) { *ctype = "text/html; charset=utf-8"; } return status; } int html_post_handler(const xs_dict *req, const char *q_path, char *payload, int p_size, char **body, int *b_size, char **ctype) { (void)p_size; (void)ctype; int status = 0; const snac *user = NULL; snac snac; const char *uid; const char *p_path; const xs_dict *p_vars; xs *l = xs_split_n(q_path, "/", 2); uid = xs_list_get(l, 1); if (!uid || !user_open(&snac, uid)) { /* invalid user */ srv_debug(1, xs_fmt("html_post_handler bad user %s", uid)); return HTTP_STATUS_NOT_FOUND; } p_path = xs_list_get(l, 2); /* all posts must be authenticated */ if (!login(&snac, req)) { user_free(&snac); *body = xs_dup(uid); return HTTP_STATUS_UNAUTHORIZED; } user = &snac; /* for L() */ set_user_lang(&snac); p_vars = xs_dict_get(req, "p_vars"); if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/ snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path)); /* post note */ const char *content = xs_dict_get(p_vars, "content"); const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to"); const char *to = xs_dict_get(p_vars, "to"); const char *sensitive = xs_dict_get(p_vars, "sensitive"); const char *summary = xs_dict_get(p_vars, "summary"); const char *edit_id = xs_dict_get(p_vars, "edit_id"); const char *post_date = xs_dict_get_def(p_vars, "post_date", ""); const char *post_time = xs_dict_get_def(p_vars, "post_time", ""); const char *post_lang = xs_dict_get(p_vars, "post_lang"); const char *visibility = xs_dict_get(p_vars, "visibility"); int scope = SCOPE_PUBLIC; /* default to public */ if (!xs_is_null(visibility)) { if (strcmp(visibility, "unlisted") == 0) scope = SCOPE_UNLISTED; else if (strcmp(visibility, "followers") == 0) scope = SCOPE_FOLLOWERS; else if (strcmp(visibility, "mentioned") == 0) scope = SCOPE_MENTIONED; } int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft")); xs *attach_list = xs_list_new(); /* iterate the attachments */ int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4")); for (int att_n = 0; att_n < max_attachments; att_n++) { xs *url_lbl = xs_fmt("attach_url_%d", att_n); xs *att_lbl = xs_fmt("attach_%d", att_n); xs *alt_lbl = xs_fmt("alt_text_%d", att_n); const char *attach_url = xs_dict_get(p_vars, url_lbl); const xs_list *attach_file = xs_dict_get(p_vars, att_lbl); const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, ""); if (xs_is_string(attach_url) && *attach_url != '\0') { xs *l = xs_list_new(); l = xs_list_append(l, attach_url); l = xs_list_append(l, alt_text); attach_list = xs_list_append(attach_list, l); } else if (xs_is_list(attach_file)) { const char *fn = xs_list_get(attach_file, 0); if (xs_is_string(fn) && *fn != '\0') { char rnd[32]; xs_rnd_buf(rnd, sizeof(rnd)); const char *ext = strrchr(fn, '.'); xs *hash = xs_md5_hex(rnd, sizeof(rnd)); xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); xs *url = xs_fmt("%s/s/%s", snac.actor, id); int fo = xs_number_get(xs_list_get(attach_file, 1)); int fs = xs_number_get(xs_list_get(attach_file, 2)); /* store */ static_put(&snac, id, payload + fo, fs); xs *l = xs_list_new(); l = xs_list_append(l, url); l = xs_list_append(l, alt_text); attach_list = xs_list_append(attach_list, l); } } } if (content != NULL) { xs *msg = NULL; xs *c_msg = NULL; xs *content_2 = xs_replace(content, "\r", ""); xs *poll_opts = NULL; /* is there a valid set of poll options? */ const char *v = xs_dict_get(p_vars, "poll_options"); if (!xs_is_null(v) && *v) { xs *v2 = xs_strip_i(xs_replace(v, "\r", "")); poll_opts = xs_split(v2, "\n"); } if (!xs_is_null(poll_opts) && xs_list_len(poll_opts)) { /* get the rest of poll configuration */ const char *p_multiple = xs_dict_get(p_vars, "poll_multiple"); const char *p_end_secs = xs_dict_get(p_vars, "poll_end_secs"); int multiple = 0; int end_secs = atoi(!xs_is_null(p_end_secs) ? p_end_secs : "60"); if (!xs_is_null(p_multiple) && strcmp(p_multiple, "on") == 0) multiple = 1; msg = msg_question(&snac, content_2, attach_list, poll_opts, multiple, end_secs); enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs); } else msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL); if (sensitive != NULL) { msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary); } if (xs_is_string(post_date) && *post_date) { xs *post_pubdate = xs_fmt("%sT%s", post_date, xs_is_string(post_time) && *post_time ? post_time : "00:00:00"); time_t t = xs_parse_iso_date(post_pubdate, 0); if (t != 0) { t -= xs_tz_offset(snac.tz); xs *iso_date = xs_str_iso_date(t); msg = xs_dict_set(msg, "published", iso_date); snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date)); } else snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate)); } /* is the published date from the future? */ int future_post = 0; xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC); if (strcmp(xs_dict_get(msg, "published"), right_now) > 0) future_post = 1; if (xs_is_null(edit_id)) { /* new message */ const char *id = xs_dict_get(msg, "id"); if (store_as_draft) { draft_add(&snac, id, msg); } else if (future_post) { schedule_add(&snac, id, msg); } else { c_msg = msg_create(&snac, msg); timeline_add(&snac, id, msg); } } else { /* an edition of a previous message */ xs *p_msg = NULL; if (valid_status(object_get(edit_id, &p_msg))) { /* copy relevant fields from previous version */ char *fields[] = { "id", "context", "url", "to", "inReplyTo", NULL }; int n; for (n = 0; fields[n]; n++) { const char *v = xs_dict_get(p_msg, fields[n]); msg = xs_dict_set(msg, fields[n], v); } if (store_as_draft) { draft_add(&snac, edit_id, msg); } else if (is_draft(&snac, edit_id)) { /* message was previously a draft; it's a create activity */ /* if the date is from the past, overwrite it with right_now */ if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) { snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now)); msg = xs_dict_set(msg, "published", right_now); } /* overwrite object */ object_add_ow(edit_id, msg); if (future_post) { schedule_add(&snac, edit_id, msg); } else { c_msg = msg_create(&snac, msg); timeline_add(&snac, edit_id, msg); } draft_del(&snac, edit_id); } else if (is_scheduled(&snac, edit_id)) { /* editing an scheduled post; just update it */ schedule_add(&snac, edit_id, msg); } else { /* ignore the (possibly changed) published date */ msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published")); /* set the updated field */ xs *updated = xs_str_utctime(0, ISO_DATE_SPEC); msg = xs_dict_set(msg, "updated", updated); /* overwrite object, not updating the indexes */ object_add_ow(edit_id, msg); /* index tags */ tag_index(edit_id, msg); /* update message */ c_msg = msg_update(&snac, msg); } } else snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id)); } if (c_msg != NULL) { enqueue_message(&snac, c_msg); enqueue_webmention(msg); } history_del(&snac, "timeline.html_"); } status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/action") == 0) { /** **/ /* action on an entry */ const char *id = xs_dict_get(p_vars, "id"); const char *actor = xs_dict_get(p_vars, "actor"); const char *action = xs_dict_get(p_vars, "action"); const char *group = xs_dict_get(p_vars, "group"); if (action == NULL) return HTTP_STATUS_NOT_FOUND; snac_debug(&snac, 1, xs_fmt("web action '%s' received", action)); status = HTTP_STATUS_SEE_OTHER; if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/ const char *eid = xs_dict_get(p_vars, "eid"); if (eid != NULL) { xs *n_msg = msg_emoji_unreact(&snac, id, eid); if (n_msg != NULL) enqueue_message(&snac, n_msg); } } else if (strcmp(action, L("EmojiReact")) == 0) { /** **/ xs *eid = xs_dup(xs_dict_get(p_vars, "eid")); eid = xs_strip_chars_i(eid, ":"); const xs_dict *ret = msg_emoji_init(&snac, id, eid); /* fails if either invalid or already reacted */ if (!ret) ret = msg_emoji_unreact(&snac, id, eid); if (!ret) status = HTTP_STATUS_NOT_FOUND; } else if (strcmp(action, L("Like")) == 0) { /** **/ xs *msg = msg_admiration(&snac, id, "Like"); if (msg != NULL) { timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg); enqueue_message(&snac, msg); } } else if (strcmp(action, L("Boost")) == 0) { /** **/ xs *msg = msg_admiration(&snac, id, "Announce"); if (msg != NULL) { timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg); enqueue_message(&snac, msg); } } else if (strcmp(action, L("Unlike")) == 0) { /** **/ xs *msg = msg_repulsion(&snac, id, "Like"); if (msg != NULL) { enqueue_message(&snac, msg); } } else if (strcmp(action, L("Unboost")) == 0) { /** **/ xs *msg = msg_repulsion(&snac, id, "Announce"); if (msg != NULL) { enqueue_message(&snac, msg); } } else if (strcmp(action, L("MUTE")) == 0) { /** **/ mute(&snac, actor); } else if (strcmp(action, L("Unmute")) == 0) { /** **/ unmute(&snac, actor); } else if (strcmp(action, L("Hide")) == 0) { /** **/ if (is_draft(&snac, id)) draft_del(&snac, id); else if (is_scheduled(&snac, id)) schedule_del(&snac, id); else hide(&snac, id); } else if (strcmp(action, L("Limit")) == 0) { /** **/ limit(&snac, actor); } else if (strcmp(action, L("Unlimit")) == 0) { /** **/ unlimit(&snac, actor); } else if (strcmp(action, L("Follow")) == 0) { /** **/ xs *msg = msg_follow(&snac, actor); if (msg != NULL) { /* reload the actor from the message, in may be different */ actor = xs_dict_get(msg, "object"); following_add(&snac, actor, msg); enqueue_output_by_actor(&snac, msg, actor, 0); } } else if (strcmp(action, L("Unfollow")) == 0) { /** **/ /* get the following object */ xs *object = NULL; if (valid_status(following_get(&snac, actor, &object))) { xs *msg = msg_undo(&snac, xs_dict_get(object, "object")); following_del(&snac, actor); enqueue_output_by_actor(&snac, msg, actor, 0); snac_log(&snac, xs_fmt("unfollowed actor %s", actor)); } else snac_log(&snac, xs_fmt("actor is not being followed %s", actor)); } else if (strcmp(action, L("Follow Group")) == 0) { /** **/ xs *msg = msg_follow(&snac, group); if (msg != NULL) { /* reload the group from the message, in may be different */ group = xs_dict_get(msg, "object"); following_add(&snac, group, msg); enqueue_output_by_actor(&snac, msg, group, 0); } } else if (strcmp(action, L("Unfollow Group")) == 0) { /** **/ /* get the following object */ xs *object = NULL; if (valid_status(following_get(&snac, group, &object))) { xs *msg = msg_undo(&snac, xs_dict_get(object, "object")); following_del(&snac, group); enqueue_output_by_actor(&snac, msg, group, 0); snac_log(&snac, xs_fmt("unfollowed group %s", group)); } else snac_log(&snac, xs_fmt("actor is not being followed %s", actor)); } else if (strcmp(action, L("Delete")) == 0) { /** **/ const char *actor_form = xs_dict_get(p_vars, "actor-form"); if (actor_form != NULL) { /* delete follower */ if (valid_status(follower_del(&snac, actor))) snac_log(&snac, xs_fmt("deleted follower %s", actor)); else snac_log(&snac, xs_fmt("error deleting follower %s", actor)); } else { /* delete an entry */ if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) { /* it's a post by us: generate a delete */ xs *msg = msg_delete(&snac, id); enqueue_message(&snac, msg); snac_log(&snac, xs_fmt("posted tombstone for %s", id)); } timeline_del(&snac, id); draft_del(&snac, id); schedule_del(&snac, id); snac_log(&snac, xs_fmt("deleted entry %s", id)); } } else if (strcmp(action, L("Pin")) == 0) { /** **/ pin(&snac, id); timeline_touch(&snac); } else if (strcmp(action, L("Unpin")) == 0) { /** **/ unpin(&snac, id); timeline_touch(&snac); } else if (strcmp(action, L("Bookmark")) == 0) { /** **/ bookmark(&snac, id); timeline_touch(&snac); } else if (strcmp(action, L("Unbookmark")) == 0) { /** **/ unbookmark(&snac, id); timeline_touch(&snac); } else if (strcmp(action, L("Approve")) == 0) { /** **/ xs *fwreq = pending_get(&snac, actor); if (fwreq != NULL) { xs *reply = msg_accept(&snac, fwreq, actor); enqueue_message(&snac, reply); if (xs_is_null(xs_dict_get(fwreq, "published"))) { /* add a date if it doesn't include one (Mastodon) */ xs *date = xs_str_utctime(0, ISO_DATE_SPEC); fwreq = xs_dict_set(fwreq, "published", date); } timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq); follower_add(&snac, actor); pending_del(&snac, actor); snac_log(&snac, xs_fmt("new follower %s", actor)); } } else if (strcmp(action, L("Discard")) == 0) { /** **/ pending_del(&snac, actor); } else status = HTTP_STATUS_NOT_FOUND; /* delete the cached timeline */ if (status == HTTP_STATUS_SEE_OTHER) history_del(&snac, "timeline.html_"); } else if (p_path && strcmp(p_path, "admin/user-setup") == 0) { /** **/ /* change of user data */ const char *v; const char *p1, *p2; if ((v = xs_dict_get(p_vars, "name")) != NULL) snac.config = xs_dict_set(snac.config, "name", v); if ((v = xs_dict_get(p_vars, "avatar")) != NULL) snac.config = xs_dict_set(snac.config, "avatar", v); if ((v = xs_dict_get(p_vars, "bio")) != NULL) snac.config = xs_dict_set(snac.config, "bio", v); if ((v = xs_dict_get(p_vars, "cw")) != NULL && strcmp(v, "on") == 0) { snac.config = xs_dict_set(snac.config, "cw", "open"); } else { /* if the checkbox is not set, the parameter is missing */ snac.config = xs_dict_set(snac.config, "cw", ""); } if ((v = xs_dict_get(p_vars, "email")) != NULL) snac.config = xs_dict_set(snac.config, "email", v); if ((v = xs_dict_get(p_vars, "telegram_bot")) != NULL) snac.config = xs_dict_set(snac.config, "telegram_bot", v); if ((v = xs_dict_get(p_vars, "telegram_chat_id")) != NULL) snac.config = xs_dict_set(snac.config, "telegram_chat_id", v); if ((v = xs_dict_get(p_vars, "ntfy_server")) != NULL) snac.config = xs_dict_set(snac.config, "ntfy_server", v); if ((v = xs_dict_get(p_vars, "ntfy_token")) != NULL) snac.config = xs_dict_set(snac.config, "ntfy_token", v); if ((v = xs_dict_get(p_vars, "purge_days")) != NULL) { xs *days = xs_number_new(atof(v)); snac.config = xs_dict_set(snac.config, "purge_days", days); } if ((v = xs_dict_get(p_vars, "drop_dm_from_unknown")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "drop_dm_from_unknown", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "bot")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "bot", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "private")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "private", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "auto_boost")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "auto_boost", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "collapse_threads")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0) snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE)); if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL) snac.config = xs_dict_set(snac.config, "lang", v); if ((v = xs_dict_get(p_vars, "tz")) != NULL) snac.config = xs_dict_set(snac.config, "tz", v); if ((v = xs_dict_get(p_vars, "post_langs")) != NULL) snac.config = xs_dict_set(snac.config, "post_langs", v); snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", "")); if ((v = xs_dict_get(p_vars, "metadata")) != NULL) snac.config = xs_dict_set(snac.config, "metadata", v); /* uploads */ const char *uploads[] = { "avatar", "header", NULL }; int n; for (n = 0; uploads[n]; n++) { xs *var_name = xs_fmt("%s_file", uploads[n]); const xs_list *uploaded_file = xs_dict_get(p_vars, var_name); if (xs_type(uploaded_file) == XSTYPE_LIST) { const char *fn = xs_list_get(uploaded_file, 0); if (fn && *fn) { const char *mimetype = xs_mime_by_ext(fn); if (xs_startswith(mimetype, "image/")) { const char *ext = strrchr(fn, '.'); xs *hash = xs_md5_hex(fn, strlen(fn)); xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : ""); xs *url = xs_fmt("%s/s/%s", snac.actor, id); int fo = xs_number_get(xs_list_get(uploaded_file, 1)); int fs = xs_number_get(xs_list_get(uploaded_file, 2)); /* store */ static_put(&snac, id, payload + fo, fs); snac.config = xs_dict_set(snac.config, uploads[n], url); } } } } /* delete images by removing url from user.json */ for (n = 0; uploads[n]; n++) { xs *var_name = xs_fmt("%s_delete", uploads[n]); const char *delete_var = xs_dict_get(p_vars, var_name); if (delete_var != NULL && strcmp(delete_var, "on") == 0) { snac.config = xs_dict_set(snac.config, uploads[n], ""); } } /* password change? */ if ((p1 = xs_dict_get(p_vars, "passwd1")) != NULL && (p2 = xs_dict_get(p_vars, "passwd2")) != NULL && *p1 && strcmp(p1, p2) == 0) { xs *pw = hash_password(snac.uid, p1, NULL); snac.config = xs_dict_set(snac.config, "passwd", pw); } user_persist(&snac, 1); status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/ notify_clear(&snac); timeline_touch(&snac); status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/vote") == 0) { /** **/ const char *irt = xs_dict_get(p_vars, "irt"); const char *opt = xs_dict_get(p_vars, "question"); const char *actor = xs_dict_get(p_vars, "actor"); xs *ls = NULL; /* multiple choices? */ if (xs_type(opt) == XSTYPE_LIST) ls = xs_dup(opt); else if (xs_type(opt) == XSTYPE_STRING) { ls = xs_list_new(); ls = xs_list_append(ls, opt); } const xs_str *v; int c = 0; while (xs_list_next(ls, &v, &c)) { xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL, NULL); /* set the option */ msg = xs_dict_append(msg, "name", v); /* delete the content */ msg = xs_dict_del(msg, "content"); xs *c_msg = msg_create(&snac, msg); enqueue_message(&snac, c_msg); timeline_add(&snac, xs_dict_get(msg, "id"), msg); } if (ls != NULL) { /* get the poll object */ xs *poll = NULL; if (valid_status(object_get(irt, &poll))) { const char *date = xs_dict_get(poll, "endTime"); if (xs_is_null(date)) date = xs_dict_get(poll, "closed"); if (!xs_is_null(date)) { time_t t = xs_parse_iso_date(date, 0) - time(NULL); /* request the poll when it's closed; Pleroma does not send and update when the poll closes */ enqueue_object_request(&snac, irt, t + 2); } } } status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/ const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags"); if (xs_is_string(followed_hashtags)) { xs *new_hashtags = xs_list_new(); xs *l = xs_split(followed_hashtags, "\n"); const char *v; xs_list_foreach(l, v) { xs *s1 = xs_strip_i(xs_dup(v)); s1 = xs_replace_i(s1, " ", ""); if (*s1 == '\0') continue; xs *s2 = NULL; if (xs_startswith(s1, "https:/")) s2 = xs_dup(s1); else { s2 = xs_utf8_to_lower(s1); if (*s2 != '#') s2 = xs_str_prepend_i(s2, "#"); } new_hashtags = xs_list_append(new_hashtags, s2); } snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags); user_persist(&snac, 0); } status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/blocked-hashtags") == 0) { /** **/ const char *hashtags = xs_dict_get(p_vars, "blocked_hashtags"); if (xs_is_string(hashtags)) { xs *new_hashtags = xs_list_new(); xs *l = xs_split(hashtags, "\n"); const char *v; xs_list_foreach(l, v) { xs *s1 = xs_strip_i(xs_dup(v)); s1 = xs_replace_i(s1, " ", ""); if (*s1 == '\0') continue; xs *s2 = xs_utf8_to_lower(s1); if (*s2 != '#') s2 = xs_str_prepend_i(s2, "#"); new_hashtags = xs_list_append(new_hashtags, s2); } snac.config = xs_dict_set(snac.config, "blocked_hashtags", new_hashtags); user_persist(&snac, 0); } status = HTTP_STATUS_SEE_OTHER; } else if (p_path && strcmp(p_path, "admin/muted-words") == 0) { const char *words = xs_dict_get(p_vars, "muted_words"); if (xs_is_string(words)) { xs *new_words = xs_list_new(); xs *l = xs_split(words, "\n"); const char *v; xs_list_foreach(l, v) { xs *s1 = xs_strip_i(xs_dup(v)); s1 = xs_replace_i(s1, " ", ""); if (*s1 == '\0') continue; xs *s2 = xs_utf8_to_lower(s1); new_words = xs_list_insert_sorted(new_words, s2); } snac.config = xs_dict_set(snac.config, "muted_words", new_words); user_persist(&snac, 0); } status = HTTP_STATUS_SEE_OTHER; } if (status == HTTP_STATUS_SEE_OTHER) { const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); if (xs_is_string(hard_redir)) *body = xs_dup(hard_redir); else { const char *redir = xs_dict_get(p_vars, "redir"); if (xs_is_null(redir)) redir = "top"; *body = xs_fmt("%s/admin#%s", snac.actor, redir); } *b_size = strlen(*body); } user_free(&snac); return status; }