فهرست منبع

implementing visibility scopes

byte 10 ماه پیش
والد
کامیت
dcf5acaf53
6فایلهای تغییر یافته به همراه252 افزوده شده و 66 حذف شده
  1. 99 21
      activitypub.c
  2. 7 3
      data.c
  3. 112 32
      html.c
  4. 9 4
      main.c
  5. 17 6
      mastoapi.c
  6. 8 0
      snac.h

+ 99 - 21
activitypub.c

@@ -569,14 +569,21 @@ xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public)
         }
 
         while (xs_list_iter(&l, &v)) {
-            if (expand_public && strcmp(v, public_address) == 0) {
-                /* iterate the followers and add them */
-                xs *fwers = follower_list(snac);
-                const char *actor;
-
-                char *p = fwers;
-                while (xs_list_iter(&p, &actor))
-                    xs_set_add(&rcpts, actor);
+            if (expand_public) {
+                if (strcmp(v, public_address) == 0 ||
+                    /* check if it's a followers collection URL */
+                    (xs_type(v) == XSTYPE_STRING &&
+                        strcmp(v, xs_fmt("%s/followers", snac->actor)) == 0) ||
+                    (xs_type(v) == XSTYPE_LIST &&
+                        xs_list_in(v, xs_fmt("%s/followers", snac->actor)) != -1)) {
+                    /* iterate the followers and add them */
+                    xs *fwers = follower_list(snac);
+                    const char *actor;
+
+                    char *p = fwers;
+                    while (xs_list_iter(&p, &actor))
+                        xs_set_add(&rcpts, actor);
+                }
             }
             else
                 xs_set_add(&rcpts, v);
@@ -1840,8 +1847,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
     xs_list *p;
     const xs_val *v;
 
-    /* FIXME: implement scope 3 */
-    int priv = scope == 1;
+    const int priv = (scope == SCOPE_MENTIONED || scope == SCOPE_FOLLOWERS);
 
     if (rcpts == NULL)
         to = xs_list_new();
@@ -1901,9 +1907,13 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
             if ((v = xs_dict_get(p_msg, "conversation")))
                 msg = xs_dict_append(msg, "conversation", v);
 
-            /* if this message is public, ours will also be */
-            if (!priv && is_msg_public(p_msg) && xs_list_in(to, public_address) == -1)
+            /* if this message is public or unlisted, ours will also be */
+            const int orig_scope = get_msg_visibility(p_msg);
+            if (!priv && orig_scope == SCOPE_PUBLIC && xs_list_in(to, public_address) == -1)
                 to = xs_list_append(to, public_address);
+            else
+            if (!priv && orig_scope == SCOPE_UNLISTED && xs_list_in(cc, public_address) == -1)
+                cc = xs_list_append(cc, public_address);
         }
 
         irt = xs_dup(in_reply_to);
@@ -1947,28 +1957,50 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
     if (ctxt == NULL)
         ctxt = xs_fmt("%s#ctxt", id);
 
-    /* add all mentions to the cc */
+    /* add all mentions to the appropriate field */
     p = tag;
     while (xs_list_iter(&p, &v)) {
         if (xs_type(v) == XSTYPE_DICT) {
             const char *t;
 
             if (!xs_is_null(t = xs_dict_get(v, "type")) && strcmp(t, "Mention") == 0) {
-                if (!xs_is_null(t = xs_dict_get(v, "href")))
-                    cc = xs_list_append(cc, t);
+                if (!xs_is_null(t = xs_dict_get(v, "href"))) {
+                    if (scope == SCOPE_MENTIONED) {
+                        /* for DMs, mentions go to 'to' */
+                        to = xs_list_append(to, t);
+                    } else {
+                        /* for other visibility levels, mentions go to 'cc' */
+                        cc = xs_list_append(cc, t);
+                    }
+                }
             }
         }
     }
 
-    if (scope == 2) {
-        /* Mastodon's "quiet public": add public address to cc */
+    if (scope == SCOPE_UNLISTED) {
+        /* Mastodon's "quiet public": remove from to, add public address to cc */
+        int idx;
+        if ((idx = xs_list_in(to, public_address)) != -1)
+            to = xs_list_del(to, idx);
         if (xs_list_in(cc, public_address) == -1)
             cc = xs_list_append(cc, public_address);
     }
     else
-    /* no recipients? must be for everybody */
-    if (!priv && xs_list_len(to) == 0)
-        to = xs_list_append(to, public_address);
+    if (scope == SCOPE_FOLLOWERS) {
+        /* followers-only: add followers collection to to */
+        xs *followers_url = xs_fmt("%s/followers", snac->actor);
+        if (xs_list_in(to, followers_url) == -1)
+            to = xs_list_append(to, followers_url);
+    }
+    else
+    if (scope == SCOPE_PUBLIC) {
+        /* public: ensure public address is in to and not in cc */
+        int idx;
+        if ((idx = xs_list_in(cc, public_address)) != -1)
+            cc = xs_list_del(cc, idx);
+        if (xs_list_in(to, public_address) == -1)
+            to = xs_list_append(to, public_address);
+    }
 
     /* delete all cc recipients that also are in the to */
     p = to;
@@ -2113,6 +2145,42 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
     return msg;
 }
 
+int get_msg_visibility(const xs_dict *msg)
+/* determine visibility from message based on CC, TO and /followers mark */
+{
+    const xs_val *to = xs_dict_get(msg, "to");
+    const xs_val *cc = xs_dict_get(msg, "cc");
+
+    /* check if it's unlisted (public in cc but not in to) */
+    int pub_in_to = 0;
+    int pub_in_cc = 0;
+
+    if ((xs_type(to) == XSTYPE_STRING && strcmp(to, public_address) == 0) ||
+        (xs_type(to) == XSTYPE_LIST && xs_list_in(to, public_address) != -1))
+        pub_in_to = 1;
+
+    if ((xs_type(cc) == XSTYPE_STRING && strcmp(cc, public_address) == 0) ||
+        (xs_type(cc) == XSTYPE_LIST && xs_list_in(cc, public_address) != -1))
+        pub_in_cc = 1;
+
+    if (!pub_in_to && pub_in_cc) {
+        return SCOPE_UNLISTED;
+    }
+    if (pub_in_to && !pub_in_cc) {
+        return SCOPE_PUBLIC;
+    }
+
+    xs *followers_url = xs_fmt("%s/followers", xs_dict_get(msg, "attributedTo"));
+
+    if ((xs_type(to) == XSTYPE_STRING && strcmp(to, followers_url) == 0) ||
+        (xs_type(to) == XSTYPE_LIST && xs_list_in(to, followers_url) != -1))
+        return SCOPE_FOLLOWERS;
+    else
+        return SCOPE_MENTIONED;
+    /* should be unreachable, someone violated the protocol.                    */
+    /* better treat it as followers-only to make sure we don't leak information.*/
+    return SCOPE_FOLLOWERS;
+}
 
 int update_question(snac *user, const char *id)
 /* updates the poll counts */
@@ -3156,7 +3224,7 @@ void process_queue_item(xs_dict *q_item)
         }
 
         if (instance_failure(inbox, 0)) {
-            srv_debug(1, xs_fmt("too many failures for instance %s", inbox));
+            srv_debug(1, xs_fmt("output message error: too many failures for instance %s", inbox));
             return;
         }
 
@@ -3502,6 +3570,8 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
 
     p_path = xs_list_get(l, 2);
 
+    const xs_dict *q_vars = xs_dict_get(req, "q_vars");
+
     *ctype  = "application/activity+json";
 
     int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"));
@@ -3595,7 +3665,15 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
         if (!is_msg_public(obj))
             status = HTTP_STATUS_NOT_FOUND;
         else
+        if (xs_dict_get(q_vars, "page"))
             msg = msg_replies(&snac, id, 1);
+        else {
+            const xs_dict *replies = xs_dict_get(obj, "replies");
+            if (xs_is_dict(replies)) {
+                msg = xs_dup(replies);
+                msg = xs_dict_set(msg, "@context", "https:/""/www.w3.org/ns/activitystreams");
+            }
+        }
     }
     else
         status = HTTP_STATUS_NOT_FOUND;

+ 7 - 3
data.c

@@ -1456,15 +1456,16 @@ void timeline_update_indexes(snac *snac, const char *id)
         xs *msg = NULL;
 
         if (valid_status(object_get(id, &msg))) {
+            const int scope = get_msg_visibility(msg);
             /* if its ours and is public, also store in public */
-            if (is_msg_public(msg)) {
+            if (scope == SCOPE_PUBLIC) {
                 if (object_user_cache_add(snac, id, "public") >= 0) {
                     /* also add it to the instance public timeline */
                     xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
                     index_add(ipt, id);
                 }
                 else
-                    srv_debug(1, xs_fmt("Not added to public instance index %s", id));
+                    srv_debug(1, xs_fmt("Not added to public instance index %s, visibility %d", id, scope));
             }
             else
                 /* also add it to public, it will be discarded later */
@@ -2222,7 +2223,10 @@ void tag_index(const char *id, const xs_dict *obj)
     const xs_list *tags = xs_dict_get(obj, "tag");
     xs *md5_id = xs_md5_hex(id, strlen(id));
 
-    if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
+    if (get_msg_visibility(obj) != SCOPE_PUBLIC)
+        return;
+
+    if (xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
         xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
 
         mkdirx(g_tag_dir);

+ 112 - 32
html.c

@@ -165,7 +165,7 @@ xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char
 
 
 xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
-                        const char *udate, const char *url, int priv,
+                        const char *udate, const char *url, int scope,
                         int in_people, const char *proxy, const char *lang,
                         const char *md5)
 {
@@ -249,12 +249,35 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
                 xs_html_raw("🤝")));
     }
 
-    if (priv) {
-        xs_html_add(actor_icon,
-            xs_html_text(" "),
-            xs_html_tag("span",
-                xs_html_attr("title", "private"),
-                xs_html_raw("🔒")));
+    if (scope != -1) {
+        if (scope == SCOPE_FOLLOWERS) {
+            xs_html_add(actor_icon,
+                xs_html_text(" "),
+                xs_html_tag("span",
+                    xs_html_attr("title", "followers"),
+                    xs_html_raw("🔒"))); // emoji of a lock
+        }
+        else if (scope == SCOPE_PUBLIC) {
+            xs_html_add(actor_icon,
+                xs_html_text(" "),
+                xs_html_tag("span",
+                    xs_html_attr("title", "public"),
+                    xs_html_raw("🌐"))); // emoji of a globe
+        }
+        else if (scope == SCOPE_UNLISTED) {
+            xs_html_add(actor_icon,
+                xs_html_text(" "),
+                xs_html_tag("span",
+                    xs_html_attr("title", "unlisted"),
+                    xs_html_raw("🔓"))); // emoji of an unlocked lock
+        }
+        else if (scope == SCOPE_MENTIONED) {
+            xs_html_add(actor_icon,
+                xs_html_text(" "),
+                xs_html_tag("span",
+                    xs_html_attr("title", "mentioned"),
+                    xs_html_raw("✉️"))); // emoji of a mail
+        }
     }
 
     if (xs_is_null(date)) {
@@ -342,18 +365,17 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
         const char *date  = NULL;
         const char *udate = NULL;
         const char *url   = NULL;
-        int priv    = 0;
         const char *type = xs_dict_get(msg, "type");
+        const int scope = get_msg_visibility(msg);
 
         if (xs_match(type, POSTLIKE_OBJECT_TYPE))
             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(user, actor, date, udate, url, priv, 0, proxy, lang, md5);
+        actor_icon = html_actor_icon(user, actor, date, udate, url, scope, 0, proxy, lang, md5);
     }
 
     return actor_icon;
@@ -365,7 +387,7 @@ xs_html *html_note(snac *user, const char *summary,
                    const char *ta_plh, const char *ta_content,
                    const char *edit_id, const char *actor_id,
                    const xs_val *cw_yn, const char *cw_text,
-                   const xs_val *mnt_only, const char *redir,
+                   const int scope, const char *redir,
                    const char *in_reply_to, int poll,
                    const xs_list *att_files, const xs_list *att_alt_texts,
                    int is_draft, const char *published,
@@ -417,14 +439,44 @@ xs_html *html_note(snac *user, const char *summary,
                 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)));
+            xs_html_tag("p",
+                xs_html_text(L("Visibility: ")),
+                xs_html_tag("label",
+                    xs_html_sctag("input",
+                        xs_html_attr("type", "radio"),
+                        xs_html_attr("name", "visibility"),
+                        xs_html_attr("value", "public"),
+                        xs_html_attr(scope == SCOPE_PUBLIC ? "checked" : "", NULL)),
+                    xs_html_text(" "),
+                    xs_html_text(L("Public"))),
+                xs_html_text(" "),
+                xs_html_tag("label",
+                    xs_html_sctag("input",
+                        xs_html_attr("type", "radio"),
+                        xs_html_attr("name", "visibility"),
+                        xs_html_attr("value", "unlisted"),
+                        xs_html_attr(scope == SCOPE_UNLISTED ? "checked" : "", NULL)),
+                    xs_html_text(" "),
+                    xs_html_text(L("Unlisted"))),
+                xs_html_text(" "),
+                xs_html_tag("label",
+                    xs_html_sctag("input",
+                        xs_html_attr("type", "radio"),
+                        xs_html_attr("name", "visibility"),
+                        xs_html_attr("value", "followers"),
+                        xs_html_attr(scope == SCOPE_FOLLOWERS ? "checked" : "", NULL)),
+                    xs_html_text(" "),
+                    xs_html_text(L("Followers-only"))),
+                xs_html_text(" "),
+                xs_html_tag("label",
+                    xs_html_sctag("input",
+                        xs_html_attr("type", "radio"),
+                        xs_html_attr("name", "visibility"),
+                        xs_html_attr("value", "mentioned"),
+                        xs_html_attr(scope == SCOPE_MENTIONED ? "checked" : "", NULL)),
+                    xs_html_text(" "),
+                    xs_html_text(L("Direct Message")))));
     }
 
     if (redir)
@@ -1236,7 +1288,7 @@ xs_html *html_top_controls(snac *user)
             L("What's on your mind?"), "",
             NULL, NULL,
             xs_stock(XSTYPE_FALSE), "",
-            xs_stock(XSTYPE_FALSE), NULL,
+            SCOPE_PUBLIC, NULL,
             NULL, 1, NULL, NULL, 0, NULL, NULL),
 
         /** operations **/
@@ -1942,6 +1994,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
             xs_dict_next(cmap, &note_lang, &dummy, &c);
         }
 
+        const int edit_scope = get_msg_visibility(msg);
+
         xs_html_add(controls, xs_html_tag("div",
             xs_html_tag("p", NULL),
             html_note(user, L("Edit..."),
@@ -1949,7 +2003,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
                 "", prev_src,
                 id, NULL,
                 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
-                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
+                edit_scope, redir,
                 NULL, 0, att_files, att_alt_texts, is_draft(user, id),
                 xs_dict_get(msg, "published"), note_lang)),
             xs_html_tag("p", NULL));
@@ -1962,6 +2016,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
         xs *form_id = xs_fmt("%s_reply_form", md5);
         xs *redir   = xs_fmt("%s_entry", md5);
 
+        const int scope = get_msg_visibility(msg);
+
         xs_html_add(controls, xs_html_tag("div",
             xs_html_tag("p", NULL),
             html_note(user, L("Reply..."),
@@ -1969,7 +2025,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
                 "", ct,
                 NULL, NULL,
                 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
-                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
+                scope, redir,
                 id, 0, NULL, NULL, 0, NULL, NULL)),
             xs_html_tag("p", NULL));
     }
@@ -2061,6 +2117,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
                 xs_html_attr("name", s1)));
     }
 
+    /* don't show followers-only notes from not followed users */
+    if (user && get_msg_visibility(msg) == SCOPE_FOLLOWERS &&
+        strcmp(user->actor, actor) != 0 && following_check(user, actor) == 0) {
+        return NULL;
+    }
+
     if ((user == NULL || strcmp(actor, user->actor) != 0)
         && !valid_status(actor_get(actor, NULL))) {
 
@@ -2275,7 +2337,6 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
 
             has_title = 1;
         }
-
         snac_content = xs_html_tag("div", NULL);
     }
 
@@ -3089,8 +3150,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
                 xs_html_text(title)));
     }
 
-    xs_html_add(body,
-        posts);
+    xs_html_add(body, posts);
 
     int mark_shown = 0;
 
@@ -3130,8 +3190,9 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
         if (user == NULL && is_msg_from_private_user(msg))
             continue;
 
-        /* is this message a non-public reply? */
-        if (user != NULL && !is_msg_public(msg)) {
+        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? */
@@ -3146,6 +3207,14 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
                 }
             }
         }
+        /* 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){
+            continue;
+        }
 
         xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
 
@@ -3256,7 +3325,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
                 xs_html_tag("div",
                     xs_html_attr("class", "snac-post-header"),
                     html_actor_icon(user, actor, xs_dict_get(actor, "published"),
-                                    NULL, NULL, 0, 1, proxy, NULL, NULL)));
+                                    NULL, NULL, -1, 1, proxy, NULL, NULL)));
 
             /* content (user bio) */
             const char *c = xs_dict_get(actor, "summary");
@@ -3357,7 +3426,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
                     "", "",
                     NULL, actor_id,
                     xs_stock(XSTYPE_FALSE), "",
-                    xs_stock(XSTYPE_FALSE), NULL,
+                    SCOPE_MENTIONED, NULL,
                     NULL, 0, NULL, NULL, 0, NULL, NULL),
                 xs_html_tag("p", NULL));
 
@@ -3564,7 +3633,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
                 xs_html_add(entry,
                     xs_html_tag("div",
                         xs_html_attr("class", "snac-post"),
-                        html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
+                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
         }
         else
         if (strcmp(type, "Move") == 0) {
@@ -3578,7 +3647,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
                     xs_html_add(entry,
                         xs_html_tag("div",
                             xs_html_attr("class", "snac-post"),
-                            html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
+                            html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
                 }
             }
         }
@@ -4445,7 +4514,18 @@ int html_post_handler(const xs_dict *req, const char *q_path,
         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");
-        int priv             = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
+        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();
 
@@ -4528,7 +4608,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
                 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
             }
             else
-                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv, post_lang, NULL);
+                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));

+ 9 - 4
main.c

@@ -45,6 +45,7 @@ int usage(const char *cmd)
         "note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"
         "note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n"
         "note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n"
+        "note_followers {basedir} {uid} {text} [files...] Sends a note only to followers\n"
         "boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"
         "unboost {basedir} {uid} {url}        Unboosts a post\n"
         "resetpwd {basedir} {uid}             Resets the password of a user\n"
@@ -800,7 +801,8 @@ int main(int argc, char *argv[])
 
     if (strcmp(cmd, "note") == 0 ||             /** **/
         strcmp(cmd, "note_unlisted") == 0 ||    /** **/
-        strcmp(cmd, "note_mention") == 0) {     /** **/
+        strcmp(cmd, "note_mention") == 0 ||     /** **/
+        strcmp(cmd, "note_followers") == 0) {   /** **/
         xs *content = NULL;
         xs *msg = NULL;
         xs *c_msg = NULL;
@@ -909,12 +911,15 @@ int main(int argc, char *argv[])
             return 1;
         }
 
-        int scope = 0;
+        int scope = SCOPE_PUBLIC;
         if (strcmp(cmd, "note_mention") == 0)
-            scope = 1;
+            scope = SCOPE_MENTIONED;
         else
         if (strcmp(cmd, "note_unlisted") == 0)
-            scope = 2;
+            scope = SCOPE_UNLISTED;
+        else
+        if (strcmp(cmd, "note_followers") == 0)
+            scope = SCOPE_FOLLOWERS;
 
         msg = msg_note(&snac, content, NULL, in_reply_to, attl, scope, getenv("LANG"), post_date);
 

+ 17 - 6
mastoapi.c

@@ -884,8 +884,16 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
         st = xs_dict_append(st, "content", s1);
     }
 
-    st = xs_dict_append(st, "visibility",
-        is_msg_public(msg) ? "public" : "private");
+    /* determine visibility: https://docs.joinmastodon.org/entities/Status/#visibility */
+    const int scope = get_msg_visibility(msg);
+    if (scope == SCOPE_PUBLIC)
+        st = xs_dict_append(st, "visibility", "public");
+    else if (scope == SCOPE_FOLLOWERS)
+        st = xs_dict_append(st, "visibility", "private");
+    else if (scope == SCOPE_MENTIONED)
+        st = xs_dict_append(st, "visibility", "direct");
+    else if (scope == SCOPE_UNLISTED)
+        st = xs_dict_append(st, "visibility", "unlisted");
 
     tmp = xs_dict_get(msg, "sensitive");
     if (xs_is_null(tmp))
@@ -1767,7 +1775,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                         if (valid_status(timeline_get_by_md5(&snac2, v, &msg))) {
                             /* add only posts by the author */
                             if (strcmp(xs_dict_get(msg, "type"), "Note") == 0 &&
-                                xs_startswith(xs_dict_get(msg, "id"), snac2.actor)) {
+                                xs_startswith(xs_dict_get(msg, "id"), snac2.actor) && is_msg_public(msg)) {
                                 xs *st = mastoapi_status(&snac2, msg);
 
                                 if (st)
@@ -2856,12 +2864,15 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
             }
 
             /* prepare the message */
-            int scope = 1;
+            int scope = SCOPE_MENTIONED;
             if (strcmp(visibility, "unlisted") == 0)
-                scope = 2;
+                scope = SCOPE_UNLISTED;
             else
             if (strcmp(visibility, "public") == 0)
-                scope = 0;
+                scope = SCOPE_PUBLIC;
+            else
+            if (strcmp(visibility, "private") == 0)
+                scope = SCOPE_FOLLOWERS;
 
             xs *msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language, NULL);
 

+ 8 - 0
snac.h

@@ -78,6 +78,13 @@ typedef struct {
 
 extern srv_state *p_state;
 
+enum {
+    SCOPE_PUBLIC = 0,
+    SCOPE_MENTIONED = 1,
+    SCOPE_UNLISTED = 2,
+    SCOPE_FOLLOWERS = 3,
+};
+
 void snac_log(snac *user, xs_str *str);
 #define snac_debug(user, level, str) do { if (dbglevel >= (level)) \
     { snac_log((user), (str)); } } while (0)
@@ -366,6 +373,7 @@ xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
                       const xs_list *opts, int multiple, int end_secs);
 xs_dict *msg_replies(snac *user, const char *id, int fill);
+int get_msg_visibility(const xs_dict *msg);
 
 int activitypub_request(snac *snac, const char *url, xs_dict **data);
 int actor_request(snac *user, const char *actor, xs_dict **data);