/* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2023 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 "snac.h" int login(snac *snac, 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) { logged_in = check_password( xs_list_get(l1, 0), xs_list_get(l1, 1), xs_dict_get(snac->config, "passwd")); } } if (logged_in) lastlog_write(snac, "web"); return logged_in; } xs_str *actor_name(xs_dict *actor) /* gets the actor name */ { xs_list *p; char *v; xs_str *name; 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"; } } name = encode_html(v); /* replace the :shortnames: */ if (!xs_is_null(p = xs_dict_get(actor, "tag"))) { xs *tag = NULL; if (xs_type(p) == XSTYPE_DICT) { /* not a list */ tag = xs_list_new(); tag = xs_list_append(tag, p); } else { /* is a list */ tag = xs_dup(p); } xs_list *tags = tag; /* iterate the tags */ while (xs_list_iter(&tags, &v)) { char *t = xs_dict_get(v, "type"); if (t && strcmp(t, "Emoji") == 0) { char *n = xs_dict_get(v, "name"); char *i = xs_dict_get(v, "icon"); if (n && i) { char *u = xs_dict_get(i, "url"); xs *img = xs_fmt("", u); name = xs_replace_i(name, n, img); } } } } return name; } xs_html *html_actor_icon(xs_dict *actor, const char *date, const char *udate, const char *url, int priv) { xs_html *actor_icon = xs_html_tag("p", NULL); xs *avatar = NULL; char *v; xs *name = actor_name(actor); /* get the avatar */ if ((v = xs_dict_get(actor, "icon")) != NULL && (v = xs_dict_get(v, "url")) != NULL) { avatar = xs_dup(v); } if (avatar == NULL) avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64()); xs_html_add(actor_icon, xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("class", "snac-avatar"), xs_html_attr("src", avatar), xs_html_attr("alt", "")), xs_html_tag("a", xs_html_attr("href", xs_dict_get(actor, "id")), xs_html_attr("class", "p-author h-card snac-author"), xs_html_raw(name))); /* name is already html-escaped */ if (!xs_is_null(url)) { xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("a", xs_html_attr("href", (char *)url), xs_html_text("»"))); } if (priv) { xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("span", xs_html_attr("title", "private"), xs_html_raw("🔒"))); } if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("span", xs_html_attr("title", "bot"), xs_html_raw("🤖"))); } if (xs_is_null(date)) { xs_html_add(actor_icon, xs_html_raw(" ")); } else { xs *date_label = xs_crop_i(xs_dup(date), 0, 10); xs *date_title = xs_dup(date); if (!xs_is_null(udate)) { xs *sd = xs_crop_i(xs_dup(udate), 0, 10); date_label = xs_str_cat(date_label, " / ", sd); date_title = xs_str_cat(date_title, " / ", udate); } xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("time", xs_html_attr("class", "dt-published snac-pubdate"), xs_html_attr("title", date_title), xs_html_text(date_label))); } { char *username, *id; if (xs_is_null(username = xs_dict_get(actor, "preferredUsername")) || *username == '\0') { /* This should never be reached */ username = "anonymous"; } if (xs_is_null(id = xs_dict_get(actor, "id")) || *id == '\0') { /* This should never be reached */ id = "https://social.example.org/anonymous"; } /* "LIKE AN ANIMAL" */ xs *domain = xs_split(id, "/"); xs *user = xs_fmt("@%s@%s", username, xs_list_get(domain, 2)); xs_html_add(actor_icon, xs_html_sctag("br", NULL), xs_html_tag("a", xs_html_attr("href", xs_dict_get(actor, "id")), xs_html_attr("class", "p-author-tag h-card snac-author-tag"), xs_html_text(user))); } return actor_icon; } xs_str *html_msg_icon(xs_str *os, const xs_dict *msg) { char *actor_id; xs *actor = NULL; if ((actor_id = xs_dict_get(msg, "attributedTo")) == NULL) actor_id = xs_dict_get(msg, "actor"); if (actor_id && valid_status(actor_get(actor_id, &actor))) { char *date = NULL; char *udate = NULL; char *url = NULL; int priv = 0; const char *type = xs_dict_get(msg, "type"); if (xs_match(type, "Note|Question|Page|Article")) url = xs_dict_get(msg, "id"); priv = !is_msg_public(msg); date = xs_dict_get(msg, "published"); udate = xs_dict_get(msg, "updated"); xs_html *actor_icon = html_actor_icon(actor, date, udate, url, priv); xs *s1 = xs_html_render(actor_icon); os = xs_str_cat(os, s1); } return os; } xs_str *html_base_header(xs_str *s) { xs_list *p; xs_str *v; s = xs_str_cat(s, "\n\n
\n"); s = xs_str_cat(s, "\n"); s = xs_str_cat(s, "\n"); /* add server CSS */ p = xs_dict_get(srv_config, "cssurls"); while (xs_list_iter(&p, &v)) { xs *s1 = xs_fmt("\n", v); s = xs_str_cat(s, s1); } return s; } xs_str *html_instance_header(xs_str *s, char *tag) { s = html_base_header(s); { FILE *f; xs *g_css_fn = xs_fmt("%s/style.css", srv_basedir); if ((f = fopen(g_css_fn, "r")) != NULL) { xs *css = xs_readall(f); fclose(f); xs *s1 = xs_fmt("\n", css); s = xs_str_cat(s, s1); } } const char *host = xs_dict_get(srv_config, "host"); const char *title = xs_dict_get(srv_config, "title"); const char *sdesc = xs_dict_get(srv_config, "short_description"); const char *email = xs_dict_get(srv_config, "admin_email"); const char *acct = xs_dict_get(srv_config, "admin_account"); { xs *s1 = xs_fmt("%s
\n" "@%s@%s
\n"; xs *es1 = encode_html(xs_dict_get(snac->config, "name")); xs *es2 = encode_html(xs_dict_get(snac->config, "uid")); xs *es3 = encode_html(xs_dict_get(srv_config, "host")); xs *s1 = xs_fmt(_tmpl, es1, es2, es3); s = xs_str_cat(s, s1); if (local) { xs *es1 = encode_html(xs_dict_get(snac->config, "bio")); xs *bio1 = not_really_markdown(es1, NULL); xs *tags = xs_list_new(); xs *bio2 = process_tags(snac, bio1, &tags); xs *s1 = xs_fmt("\n" "
\n" "
\n" "
%s
\n"
"
" "\n", L("Edit..."), md5, snac->actor, md5, prev_src, id, L("Sensitive content"), xs_type(sensitive) == XSTYPE_TRUE ? "checked" : "", L("Sensitive content description"), xs_is_null(summary) ? "" : summary, L("Only for mentioned people"), L("Attach..."), L("File"), L("File description"), md5, L("Post") ); s = xs_str_cat(s, s1); } { /** reply **/ /* the post textarea */ xs *ct = build_mentions(snac, msg); const xs_val *sensitive = xs_dict_get(msg, "sensitive"); const char *summary = xs_dict_get(msg, "summary"); xs *s1 = xs_fmt( "
%s
\n"
"
" "\n", L("Reply..."), md5, snac->actor, md5, ct, id, L("Sensitive content"), xs_type(sensitive) == XSTYPE_TRUE ? "checked" : "", L("Sensitive content description"), xs_is_null(summary) ? "" : summary, L("Only for mentioned people"), L("Attach..."), L("File"), L("File description"), md5, L("Post") ); s = xs_str_cat(s, s1); } s = xs_str_cat(s, "
%s
"); if (!xs_startswith(c, "
")) { xs *s1 = c; c = xs_fmt("
%s
", s1); } /* replace the :shortnames: */ if (!xs_is_null(p = xs_dict_get(msg, "tag"))) { xs *tag = NULL; if (xs_type(p) == XSTYPE_DICT) { /* not a list */ tag = xs_list_new(); tag = xs_list_append(tag, p); } else if (xs_type(p) == XSTYPE_LIST) tag = xs_dup(p); else tag = xs_list_new(); xs_list *tags = tag; /* iterate the tags */ while (xs_list_iter(&tags, &v)) { char *t = xs_dict_get(v, "type"); if (t && strcmp(t, "Emoji") == 0) { char *n = xs_dict_get(v, "name"); char *i = xs_dict_get(v, "icon"); if (n && i) { char *u = xs_dict_get(i, "url"); xs *img = xs_fmt("", u, n); c = xs_replace_i(c, n, img); } } } } if (strcmp(type, "Question") == 0) { /** question content **/ xs_list *oo = xs_dict_get(msg, "oneOf"); xs_list *ao = xs_dict_get(msg, "anyOf"); xs_list *p; xs_dict *v; int closed = 0; if (xs_dict_get(msg, "closed")) closed = 2; else if (user && xs_startswith(id, user->actor)) closed = 1; /* we questioned; closed for us */ else if (user && was_question_voted(user, id)) closed = 1; /* we already voted; closed for us */ /* get the appropriate list of options */ p = oo != NULL ? oo : ao; if (closed || user == NULL) { /* closed poll */ c = xs_str_cat(c, "%s: | %d |
%s
\n", L("Closed")); c = xs_str_cat(c, s1); } else { /* show when the poll closes */ const char *end_time = xs_dict_get(msg, "endTime"); 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 *es1 = encode_html(p); xs *s1 = xs_fmt("%s %s
", L("Closes in"), es1); c = xs_str_cat(c, s1); } } } } s = xs_str_cat(s, c); } s = xs_str_cat(s, "\n"); /* add the attachments */ v = xs_dict_get(msg, "attachment"); if (!xs_is_null(v)) { /** attachments **/ xs *attach = NULL; /* ensure it's a list */ if (xs_type(v) == XSTYPE_DICT) { attach = xs_list_new(); attach = xs_list_append(attach, v); } else if (xs_type(v) == XSTYPE_LIST) attach = xs_dup(v); else attach = xs_list_new(); /* does the message have an image? */ if (xs_type(v = xs_dict_get(msg, "image")) == XSTYPE_DICT) { /* add it to the attachment list */ attach = xs_list_append(attach, v); } /* make custom css for attachments easier */ s = xs_str_cat(s, " \n"); } /* 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 *es1 = encode_html(audience); xs *s1 = xs_fmt("(%s)
\n", audience, L("Source channel or community"), es1); s = xs_str_cat(s, s1); } if (sensitive) s = xs_str_cat(s, "\n"); s = xs_str_cat(s, "
\n"); if (level < 4) ss = xs_str_cat(ss, "
%s
")) 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); } /* buttons */ xs *btn_form_action = xs_fmt("%s/admin/action", snac->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", "actor-form"), xs_html_attr("value", "yes"))); xs_html_add(snac_controls, form); if (following_check(snac, actor_id)) { xs_html_add(form, html_button_2("unfollow", L("Unfollow"), L("Stop following this user's activity"))); if (is_limited(snac, actor_id)) xs_html_add(form, html_button_2("unlimit", L("Unlimit"), L("Allow announces (boosts) from this user"))); else xs_html_add(form, html_button_2("limit", L("Limit"), L("Block announces (boosts) from this user"))); } else { xs_html_add(form, html_button_2("follow", L("Follow"), L("Start following this user's activity"))); if (follower_check(snac, actor_id)) xs_html_add(form, html_button_2("delete", L("Delete"), L("Delete this user"))); } if (is_muted(snac, actor_id)) xs_html_add(form, html_button_2("unmute", L("Unmute"), L("Stop blocking activities from this user"))); else xs_html_add(form, html_button_2("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_action = xs_fmt("%s/admin/note", snac->actor); xs *dm_form_id = xs_fmt("%s_reply_form", md5); xs_html *dm_textarea = xs_html_tag("div", xs_html_tag("details", xs_html_tag("summary", xs_html_text(L("Direct Message..."))), xs_html_tag("p", NULL), xs_html_tag("div", xs_html_attr("class", "snac-note"), xs_html_attr("id", dm_div_id), xs_html_tag("form", xs_html_attr("autocomplete", "off"), xs_html_attr("method", "post"), xs_html_attr("action", dm_action), xs_html_attr("enctype", "multipart/form-data"), xs_html_attr("id", dm_form_id), xs_html_tag("textarea", xs_html_attr("class", "snac-textarea"), xs_html_attr("name", "content"), xs_html_attr("rows", "4"), xs_html_attr("wrap", "virtual"), xs_html_attr("required", "required"), xs_html_attr("placeholder", ""), xs_html_text("")), xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id)), xs_html_tag("p", NULL), xs_html_tag("details", xs_html_tag("summary", xs_html_text(L("Attachment..."))), xs_html_tag("p", NULL), xs_html_sctag("input", xs_html_attr("type", "file"), xs_html_attr("name", "attach")), xs_html_sctag("input", xs_html_attr("type", "text"), xs_html_attr("name", "alt_text"), xs_html_attr("placeholder", L("Attachment description")))), xs_html_tag("p", NULL), xs_html_sctag("input", xs_html_attr("type", "submit"), xs_html_attr("class", "button"), xs_html_attr("value", L("Post")))), xs_html_tag("p", NULL))), xs_html_tag("p", NULL)); xs_html_add(snac_controls, dm_textarea); xs_html_add(snac_post, snac_controls); { xs *s1 = xs_html_render_s(snac_post, xs_dup("\n")); s = xs_str_cat(s, s1); } } } s = xs_str_cat(s, "