/* 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_html *html_msg_icon(const xs_dict *msg) { char *actor_id; xs *actor = NULL; xs_html *actor_icon = 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"); actor_icon = html_actor_icon(actor, date, udate, url, priv); } return actor_icon; } xs_html *html_note(snac *user, char *summary, char *div_id, char *form_id, char *ta_plh, char *ta_content, char *edit_id, char *actor_id, xs_val *cw_yn, char *cw_text, xs_val *mnt_only, char *redir, char *in_reply_to, int poll) { xs *action = xs_fmt("%s/admin/note", user->actor); xs_html *form; xs_html *note = xs_html_tag("div", xs_html_tag("details", xs_html_tag("summary", xs_html_text(summary)), xs_html_tag("p", NULL), xs_html_tag("div", xs_html_attr("class", "snac-note"), xs_html_attr("id", div_id), form = xs_html_tag("form", xs_html_attr("autocomplete", "off"), xs_html_attr("method", "post"), xs_html_attr("action", action), xs_html_attr("enctype", "multipart/form-data"), xs_html_attr("id", 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", ta_plh), xs_html_text(ta_content)), xs_html_tag("p", NULL), xs_html_text(L("Sensitive content: ")), xs_html_sctag("input", xs_html_attr("type", "checkbox"), xs_html_attr("name", "sensitive"), xs_html_attr(xs_type(cw_yn) == XSTYPE_TRUE ? "checked" : "", NULL)), xs_html_sctag("input", xs_html_attr("type", "text"), xs_html_attr("name", "summary"), xs_html_attr("placeholder", L("Sensitive content description")), xs_html_attr("value", xs_is_null(cw_text) ? "" : cw_text)))))); if (actor_id) xs_html_add(form, xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id))); else { /* no actor_id; ask for mentioned_only */ xs_html_add(form, xs_html_tag("p", NULL), xs_html_text(L("Only for mentioned people: ")), xs_html_sctag("input", xs_html_attr("type", "checkbox"), xs_html_attr("name", "mentioned_only"), xs_html_attr(xs_type(mnt_only) == XSTYPE_TRUE ? "checked" : "", NULL))); } if (redir) xs_html_add(form, xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "redir"), xs_html_attr("value", redir))); if (in_reply_to) xs_html_add(form, xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "in_reply_to"), xs_html_attr("value", in_reply_to))); if (edit_id) xs_html_add(form, xs_html_sctag("input", xs_html_attr("type", "hidden"), xs_html_attr("name", "edit_id"), xs_html_attr("value", edit_id))); xs_html_add(form, 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"))))); /* add poll controls */ if (poll) { xs_html_add(form, xs_html_tag("p", NULL), xs_html_tag("details", xs_html_tag("summary", xs_html_text(L("Poll..."))), xs_html_tag("p", xs_html_text(L("Poll options (one per line, up to 8):")), xs_html_sctag("br", NULL), xs_html_tag("textarea", xs_html_attr("class", "snac-textarea"), xs_html_attr("name", "poll_options"), xs_html_attr("rows", "4"), xs_html_attr("wrap", "virtual"), xs_html_attr("placeholder", "Option 1...\nOption 2...\nOption 3...\n..."))), xs_html_tag("select", xs_html_attr("name", "poll_multiple"), xs_html_tag("option", xs_html_attr("value", "off"), xs_html_text(L("One choice"))), xs_html_tag("option", xs_html_attr("value", "on"), xs_html_text(L("Multiple choices")))), xs_html_text(" "), xs_html_tag("select", xs_html_attr("name", "poll_end_secs"), xs_html_tag("option", xs_html_attr("value", "300"), xs_html_text(L("End in 5 minutes"))), xs_html_tag("option", xs_html_attr("value", "3600"), xs_html_attr("selected", NULL), xs_html_text(L("End in 1 hour"))), xs_html_tag("option", xs_html_attr("value", "86400"), xs_html_text(L("End in 1 day")))))); } 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("Post"))), xs_html_tag("p", NULL)); return note; } static xs_html *html_base_head(void) { xs_html *head = xs_html_tag("head", xs_html_sctag("meta", xs_html_attr("name", "viewport"), xs_html_attr("content", "width=device-width, initial-scale=1")), xs_html_sctag("meta", xs_html_attr("name", "generator"), xs_html_attr("content", USER_AGENT))); /* add server CSS */ xs_list *p = xs_dict_get(srv_config, "cssurls"); char *v; while (xs_list_iter(&p, &v)) { xs_html_add(head, xs_html_sctag("link", xs_html_attr("rel", "stylesheet"), xs_html_attr("type", "text/css"), xs_html_attr("href", v))); } return head; } xs_html *html_instance_head(void) { xs_html *head = html_base_head(); { 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_html_add(head, xs_html_tag("style", xs_html_text(css))); } } char *host = xs_dict_get(srv_config, "host"); char *title = xs_dict_get(srv_config, "title"); xs_html_add(head, xs_html_tag("title", xs_html_text(title && *title ? title : host))); return head; } static xs_html *html_instance_body(char *tag) { char *host = xs_dict_get(srv_config, "host"); char *sdesc = xs_dict_get(srv_config, "short_description"); char *email = xs_dict_get(srv_config, "admin_email"); char *acct = xs_dict_get(srv_config, "admin_account"); xs *blurb = xs_replace(snac_blurb, "%host%", host); xs_html *dl; xs_html *body = xs_html_tag("body", xs_html_tag("div", xs_html_attr("class", "snac-instance-blurb"), xs_html_raw(blurb), /* pure html */ dl = xs_html_tag("dl", NULL))); if (sdesc && *sdesc) { xs_html_add(dl, xs_html_tag("di", xs_html_tag("dt", xs_html_text(L("Site description"))), xs_html_tag("dd", xs_html_text(sdesc)))); } if (email && *email) { xs *mailto = xs_fmt("mailto:%s", email); xs_html_add(dl, xs_html_tag("di", xs_html_tag("dt", xs_html_text(L("Admin email"))), xs_html_tag("dd", xs_html_tag("a", xs_html_attr("href", mailto), xs_html_text(email))))); } if (acct && *acct) { xs *url = xs_fmt("%s/%s", srv_baseurl, acct); xs *handle = xs_fmt("@%s@%s", acct, host); xs_html_add(dl, xs_html_tag("di", xs_html_tag("dt", xs_html_text(L("Admin account"))), xs_html_tag("dd", xs_html_tag("a", xs_html_attr("href", url), xs_html_text(handle))))); } { xs *l = tag ? xs_fmt(L("Search results for #%s"), tag) : xs_dup(L("Recent posts by users in this instance")); xs_html_add(body, xs_html_tag("h2", xs_html_attr("class", "snac-header"), xs_html_text(l))); } return body; } static xs_str *html_instance_header(xs_str *s, char *tag) /* TO BE REPLACED BY html_instance_body() */ { xs_html *head = html_instance_head(); char *host = xs_dict_get(srv_config, "host"); char *sdesc = xs_dict_get(srv_config, "short_description"); char *email = xs_dict_get(srv_config, "admin_email"); char *acct = xs_dict_get(srv_config, "admin_account"); { xs *s1 = xs_html_render(head); s = xs_str_cat(s, "\n", s1); } s = xs_str_cat(s, "
\n"); 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; xs_html *poll = xs_html_tag("div", NULL); 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 */ xs_html *poll_result = xs_html_tag("table", xs_html_attr("class", "snac-poll-result")); while (xs_list_iter(&p, &v)) { char *name = xs_dict_get(v, "name"); xs_dict *replies = xs_dict_get(v, "replies"); if (name && replies) { char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems")); 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)))); while (xs_list_iter(&p, &v)) { char *name = xs_dict_get(v, "name"); xs_dict *replies = xs_dict_get(v, "replies"); if (name) { char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems")); xs_html_add(form, xs_html_sctag("input", xs_html_attr("type", !xs_is_null(oo) ? "radio" : "checkbox"), xs_html_attr("id", name), xs_html_attr("value", name), xs_html_attr("name", "question")), 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 */ 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_html_add(poll, xs_html_tag("p", xs_html_text(L("Closes in")), xs_html_text(" "), xs_html_text(p))); } } } xs *s1 = xs_html_render(poll); 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 */ xs_html *content_attachments = xs_html_tag("div", xs_html_attr("class", "snac-content-attachments")); xs_list *p = attach; while (xs_list_iter(&p, &v)) { char *t = xs_dict_get(v, "mediaType"); if (xs_is_null(t)) t = xs_dict_get(v, "type"); if (xs_is_null(t)) continue; char *url = xs_dict_get(v, "url"); if (xs_is_null(url)) url = xs_dict_get(v, "href"); if (xs_is_null(url)) continue; /* infer MIME type from non-specific attachments */ if (xs_list_len(attach) < 2 && xs_match(t, "Link|Document")) { char *mt = (char *)xs_mime_by_ext(url); if (xs_match(mt, "image/*|audio/*|video/*")) /* */ t = mt; } char *name = xs_dict_get(v, "name"); if (xs_is_null(name)) name = xs_dict_get(msg, "name"); if (xs_is_null(name)) name = L("No description"); if (xs_startswith(t, "image/") || strcmp(t, "Image") == 0) { xs_html_add(content_attachments, xs_html_tag("a", xs_html_attr("href", url), xs_html_attr("target", "_blank"), xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("src", url), xs_html_attr("alt", name), xs_html_attr("title", name)))); } else if (xs_startswith(t, "video/")) { xs_html_add(content_attachments, xs_html_tag("video", xs_html_attr("style", "width: 100%"), xs_html_attr("class", "snac-embedded-video"), xs_html_attr("controls", NULL), xs_html_attr("src", url), xs_html_text(L("Video")), xs_html_text(": "), xs_html_tag("a", xs_html_attr("href", url), xs_html_text(name)))); } else if (xs_startswith(t, "audio/")) { xs_html_add(content_attachments, xs_html_tag("audio", xs_html_attr("style", "width: 100%"), xs_html_attr("class", "snac-embedded-audio"), xs_html_attr("controls", NULL), xs_html_attr("src", url), xs_html_text(L("Audio")), xs_html_text(": "), xs_html_tag("a", xs_html_attr("href", url), xs_html_text(name)))); } else if (strcmp(t, "Link") == 0) { xs_html_add(content_attachments, xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", url), xs_html_text(url)))); } else { xs_html_add(content_attachments, xs_html_tag("p", xs_html_tag("a", xs_html_attr("href", url), xs_html_text(L("Attachment")), xs_html_text(": "), xs_html_text(url)))); } } { xs *s1 = xs_html_render(content_attachments); s = xs_str_cat(s, s1); } } /* 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("unfollow", L("Unfollow"), L("Stop following this user's activity"))); if (is_limited(snac, 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(snac, actor_id)) xs_html_add(form, html_button("delete", L("Delete"), L("Delete this user"))); } if (is_muted(snac, 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(snac, L("Direct Message..."), dm_div_id, dm_form_id, "", "", NULL, actor_id, xs_stock_false, "", xs_stock_false, NULL, NULL, 0), 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) { xs *wing = following_list(user); xs *wers = follower_list(user); xs_html *html = xs_html_tag("html", html_user_head(user), xs_html_add(html_user_body(user, 0), html_people_list(user, wing, L("People you follow"), "i"), html_people_list(user, wers, L("People that follow you"), "e"), html_footer())); return xs_html_render_s(html, "\n"); } xs_str *html_notifications(snac *snac) { xs_str *s = xs_str_new(NULL); xs *n_list = notify_list(snac, 0); xs *n_time = notify_check_time(snac, 0); xs_list *p = n_list; xs_str *v; enum { NHDR_NONE, NHDR_NEW, NHDR_OLD } stage = NHDR_NONE; s = html_user_header(snac, s, 0); xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", snac->actor); xs_html *clear_all_form = 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 *s1 = xs_html_render(clear_all_form); s = xs_str_cat(s, s1); } while (xs_list_iter(&p, &v)) { xs *noti = notify_get(snac, 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"); if (xs_is_null(id) || !valid_status(object_get(id, &obj))) continue; if (is_hidden(snac, id)) continue; const char *actor_id = xs_dict_get(noti, "actor"); xs *actor = NULL; if (!valid_status(actor_get(actor_id, &actor))) continue; xs *a_name = actor_name(actor); if (strcmp(v, n_time) > 0) { /* unseen notification */ if (stage == NHDR_NONE) { xs *s1 = xs_fmt("
%s by %s:
\n", es1, actor_id, a_name); s = xs_str_cat(s, s1); if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) { xs_html *div = xs_html_tag("div", xs_html_attr("class", "snac-post"), html_actor_icon(actor, NULL, NULL, NULL, 0)); xs *s1 = xs_html_render(div); s = xs_str_cat(s, s1); } else { xs *md5 = xs_md5_hex(id, strlen(id)); s = html_entry(snac, s, obj, 0, 0, md5, 1); } s = xs_str_cat(s, "