commit ab698c2

grunfink  ·  2025-09-28 13:46:56 +0000 UTC
parents 65d8680, 5610ffb
Merge pull request 'implementing scopes' (#474) from byte/snac2:feature/visibility into master

Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/474
6 files changed,  +240, -64
M data.c
M html.c
M main.c
M snac.h
+88, -20
  1@@ -569,14 +569,21 @@ xs_list *recipient_list(snac *snac, const xs_dict *msg, int expand_public)
  2         }
  3 
  4         while (xs_list_iter(&l, &v)) {
  5-            if (expand_public && strcmp(v, public_address) == 0) {
  6-                /* iterate the followers and add them */
  7-                xs *fwers = follower_list(snac);
  8-                const char *actor;
  9-
 10-                char *p = fwers;
 11-                while (xs_list_iter(&p, &actor))
 12-                    xs_set_add(&rcpts, actor);
 13+            if (expand_public) {
 14+                if (strcmp(v, public_address) == 0 ||
 15+                    /* check if it's a followers collection URL */
 16+                    (xs_type(v) == XSTYPE_STRING &&
 17+                        strcmp(v, xs_fmt("%s/followers", snac->actor)) == 0) ||
 18+                    (xs_type(v) == XSTYPE_LIST &&
 19+                        xs_list_in(v, xs_fmt("%s/followers", snac->actor)) != -1)) {
 20+                    /* iterate the followers and add them */
 21+                    xs *fwers = follower_list(snac);
 22+                    const char *actor;
 23+
 24+                    char *p = fwers;
 25+                    while (xs_list_iter(&p, &actor))
 26+                        xs_set_add(&rcpts, actor);
 27+                }
 28             }
 29             else
 30                 xs_set_add(&rcpts, v);
 31@@ -1840,8 +1847,7 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 32     xs_list *p;
 33     const xs_val *v;
 34 
 35-    /* FIXME: implement scope 3 */
 36-    int priv = scope == 1;
 37+    const int priv = (scope == SCOPE_MENTIONED || scope == SCOPE_FOLLOWERS);
 38 
 39     if (rcpts == NULL)
 40         to = xs_list_new();
 41@@ -1901,9 +1907,13 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 42             if ((v = xs_dict_get(p_msg, "conversation")))
 43                 msg = xs_dict_append(msg, "conversation", v);
 44 
 45-            /* if this message is public, ours will also be */
 46-            if (!priv && is_msg_public(p_msg) && xs_list_in(to, public_address) == -1)
 47+            /* if this message is public or unlisted, ours will also be */
 48+            const int orig_scope = get_msg_visibility(p_msg);
 49+            if (!priv && orig_scope == SCOPE_PUBLIC && xs_list_in(to, public_address) == -1)
 50                 to = xs_list_append(to, public_address);
 51+            else
 52+            if (!priv && orig_scope == SCOPE_UNLISTED && xs_list_in(cc, public_address) == -1)
 53+                cc = xs_list_append(cc, public_address);
 54         }
 55 
 56         irt = xs_dup(in_reply_to);
 57@@ -1947,28 +1957,50 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
 58     if (ctxt == NULL)
 59         ctxt = xs_fmt("%s#ctxt", id);
 60 
 61-    /* add all mentions to the cc */
 62+    /* add all mentions to the appropriate field */
 63     p = tag;
 64     while (xs_list_iter(&p, &v)) {
 65         if (xs_type(v) == XSTYPE_DICT) {
 66             const char *t;
 67 
 68             if (!xs_is_null(t = xs_dict_get(v, "type")) && strcmp(t, "Mention") == 0) {
 69-                if (!xs_is_null(t = xs_dict_get(v, "href")))
 70-                    cc = xs_list_append(cc, t);
 71+                if (!xs_is_null(t = xs_dict_get(v, "href"))) {
 72+                    if (scope == SCOPE_MENTIONED) {
 73+                        /* for DMs, mentions go to 'to' */
 74+                        to = xs_list_append(to, t);
 75+                    } else {
 76+                        /* for other visibility levels, mentions go to 'cc' */
 77+                        cc = xs_list_append(cc, t);
 78+                    }
 79+                }
 80             }
 81         }
 82     }
 83 
 84-    if (scope == 2) {
 85-        /* Mastodon's "quiet public": add public address to cc */
 86+    if (scope == SCOPE_UNLISTED) {
 87+        /* Mastodon's "quiet public": remove from to, add public address to cc */
 88+        int idx;
 89+        if ((idx = xs_list_in(to, public_address)) != -1)
 90+            to = xs_list_del(to, idx);
 91         if (xs_list_in(cc, public_address) == -1)
 92             cc = xs_list_append(cc, public_address);
 93     }
 94     else
 95-    /* no recipients? must be for everybody */
 96-    if (!priv && xs_list_len(to) == 0)
 97-        to = xs_list_append(to, public_address);
 98+    if (scope == SCOPE_FOLLOWERS) {
 99+        /* followers-only: add followers collection to to */
100+        xs *followers_url = xs_fmt("%s/followers", snac->actor);
101+        if (xs_list_in(to, followers_url) == -1)
102+            to = xs_list_append(to, followers_url);
103+    }
104+    else
105+    if (scope == SCOPE_PUBLIC) {
106+        /* public: ensure public address is in to and not in cc */
107+        int idx;
108+        if ((idx = xs_list_in(cc, public_address)) != -1)
109+            cc = xs_list_del(cc, idx);
110+        if (xs_list_in(to, public_address) == -1)
111+            to = xs_list_append(to, public_address);
112+    }
113 
114     /* delete all cc recipients that also are in the to */
115     p = to;
116@@ -2113,6 +2145,42 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
117     return msg;
118 }
119 
120+int get_msg_visibility(const xs_dict *msg)
121+/* determine visibility from message based on CC, TO and /followers mark */
122+{
123+    const xs_val *to = xs_dict_get(msg, "to");
124+    const xs_val *cc = xs_dict_get(msg, "cc");
125+
126+    /* check if it's unlisted (public in cc but not in to) */
127+    int pub_in_to = 0;
128+    int pub_in_cc = 0;
129+
130+    if ((xs_type(to) == XSTYPE_STRING && strcmp(to, public_address) == 0) ||
131+        (xs_type(to) == XSTYPE_LIST && xs_list_in(to, public_address) != -1))
132+        pub_in_to = 1;
133+
134+    if ((xs_type(cc) == XSTYPE_STRING && strcmp(cc, public_address) == 0) ||
135+        (xs_type(cc) == XSTYPE_LIST && xs_list_in(cc, public_address) != -1))
136+        pub_in_cc = 1;
137+
138+    if (!pub_in_to && pub_in_cc) {
139+        return SCOPE_UNLISTED;
140+    }
141+    if (pub_in_to && !pub_in_cc) {
142+        return SCOPE_PUBLIC;
143+    }
144+
145+    xs *followers_url = xs_fmt("%s/followers", xs_dict_get(msg, "attributedTo"));
146+
147+    if ((xs_type(to) == XSTYPE_STRING && strcmp(to, followers_url) == 0) ||
148+        (xs_type(to) == XSTYPE_LIST && xs_list_in(to, followers_url) != -1))
149+        return SCOPE_FOLLOWERS;
150+    else
151+        return SCOPE_MENTIONED;
152+    /* should be unreachable, someone violated the protocol.                    */
153+    /* better treat it as followers-only to make sure we don't leak information.*/
154+    return SCOPE_FOLLOWERS;
155+}
156 
157 int update_question(snac *user, const char *id)
158 /* updates the poll counts */
M data.c
+7, -3
 1@@ -1456,15 +1456,16 @@ void timeline_update_indexes(snac *snac, const char *id)
 2         xs *msg = NULL;
 3 
 4         if (valid_status(object_get(id, &msg))) {
 5+            const int scope = get_msg_visibility(msg);
 6             /* if its ours and is public, also store in public */
 7-            if (is_msg_public(msg)) {
 8+            if (scope == SCOPE_PUBLIC) {
 9                 if (object_user_cache_add(snac, id, "public") >= 0) {
10                     /* also add it to the instance public timeline */
11                     xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
12                     index_add(ipt, id);
13                 }
14                 else
15-                    srv_debug(1, xs_fmt("Not added to public instance index %s", id));
16+                    srv_debug(1, xs_fmt("Not added to public instance index %s, visibility %d", id, scope));
17             }
18             else
19                 /* also add it to public, it will be discarded later */
20@@ -2222,7 +2223,10 @@ void tag_index(const char *id, const xs_dict *obj)
21     const xs_list *tags = xs_dict_get(obj, "tag");
22     xs *md5_id = xs_md5_hex(id, strlen(id));
23 
24-    if (is_msg_public(obj) && xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
25+    if (get_msg_visibility(obj) != SCOPE_PUBLIC)
26+        return;
27+
28+    if (xs_type(tags) == XSTYPE_LIST && xs_list_len(tags) > 0) {
29         xs *g_tag_dir = xs_fmt("%s/tag", srv_basedir);
30 
31         mkdirx(g_tag_dir);
M html.c
+112, -32
  1@@ -165,7 +165,7 @@ xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char
  2 
  3 
  4 xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
  5-                        const char *udate, const char *url, int priv,
  6+                        const char *udate, const char *url, int scope,
  7                         int in_people, const char *proxy, const char *lang,
  8                         const char *md5)
  9 {
 10@@ -249,12 +249,35 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
 11                 xs_html_raw("🤝")));
 12     }
 13 
 14-    if (priv) {
 15-        xs_html_add(actor_icon,
 16-            xs_html_text(" "),
 17-            xs_html_tag("span",
 18-                xs_html_attr("title", "private"),
 19-                xs_html_raw("🔒")));
 20+    if (scope != -1) {
 21+        if (scope == SCOPE_FOLLOWERS) {
 22+            xs_html_add(actor_icon,
 23+                xs_html_text(" "),
 24+                xs_html_tag("span",
 25+                    xs_html_attr("title", "followers"),
 26+                    xs_html_raw("🔒"))); // emoji of a lock
 27+        }
 28+        else if (scope == SCOPE_PUBLIC) {
 29+            xs_html_add(actor_icon,
 30+                xs_html_text(" "),
 31+                xs_html_tag("span",
 32+                    xs_html_attr("title", "public"),
 33+                    xs_html_raw("🌐"))); // emoji of a globe
 34+        }
 35+        else if (scope == SCOPE_UNLISTED) {
 36+            xs_html_add(actor_icon,
 37+                xs_html_text(" "),
 38+                xs_html_tag("span",
 39+                    xs_html_attr("title", "unlisted"),
 40+                    xs_html_raw("🔓"))); // emoji of an unlocked lock
 41+        }
 42+        else if (scope == SCOPE_MENTIONED) {
 43+            xs_html_add(actor_icon,
 44+                xs_html_text(" "),
 45+                xs_html_tag("span",
 46+                    xs_html_attr("title", "mentioned"),
 47+                    xs_html_raw("✉️"))); // emoji of a mail
 48+        }
 49     }
 50 
 51     if (xs_is_null(date)) {
 52@@ -342,18 +365,17 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
 53         const char *date  = NULL;
 54         const char *udate = NULL;
 55         const char *url   = NULL;
 56-        int priv    = 0;
 57         const char *type = xs_dict_get(msg, "type");
 58+        const int scope = get_msg_visibility(msg);
 59 
 60         if (xs_match(type, POSTLIKE_OBJECT_TYPE))
 61             url = xs_dict_get(msg, "id");
 62 
 63-        priv = !is_msg_public(msg);
 64 
 65         date  = xs_dict_get(msg, "published");
 66         udate = xs_dict_get(msg, "updated");
 67 
 68-        actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang, md5);
 69+        actor_icon = html_actor_icon(user, actor, date, udate, url, scope, 0, proxy, lang, md5);
 70     }
 71 
 72     return actor_icon;
 73@@ -365,7 +387,7 @@ xs_html *html_note(snac *user, const char *summary,
 74                    const char *ta_plh, const char *ta_content,
 75                    const char *edit_id, const char *actor_id,
 76                    const xs_val *cw_yn, const char *cw_text,
 77-                   const xs_val *mnt_only, const char *redir,
 78+                   const int scope, const char *redir,
 79                    const char *in_reply_to, int poll,
 80                    const xs_list *att_files, const xs_list *att_alt_texts,
 81                    int is_draft, const char *published,
 82@@ -417,14 +439,44 @@ xs_html *html_note(snac *user, const char *summary,
 83                 xs_html_attr("name",  "to"),
 84                 xs_html_attr("value", actor_id)));
 85     else {
 86-        /* no actor_id; ask for mentioned_only */
 87         xs_html_add(form,
 88-            xs_html_tag("p", NULL),
 89-            xs_html_text(L("Only for mentioned people: ")),
 90-            xs_html_sctag("input",
 91-                xs_html_attr("type",  "checkbox"),
 92-                xs_html_attr("name",  "mentioned_only"),
 93-                xs_html_attr(xs_type(mnt_only) == XSTYPE_TRUE ? "checked" : "", NULL)));
 94+            xs_html_tag("p",
 95+                xs_html_text(L("Visibility: ")),
 96+                xs_html_tag("label",
 97+                    xs_html_sctag("input",
 98+                        xs_html_attr("type", "radio"),
 99+                        xs_html_attr("name", "visibility"),
100+                        xs_html_attr("value", "public"),
101+                        xs_html_attr(scope == SCOPE_PUBLIC ? "checked" : "", NULL)),
102+                    xs_html_text(" "),
103+                    xs_html_text(L("Public"))),
104+                xs_html_text(" "),
105+                xs_html_tag("label",
106+                    xs_html_sctag("input",
107+                        xs_html_attr("type", "radio"),
108+                        xs_html_attr("name", "visibility"),
109+                        xs_html_attr("value", "unlisted"),
110+                        xs_html_attr(scope == SCOPE_UNLISTED ? "checked" : "", NULL)),
111+                    xs_html_text(" "),
112+                    xs_html_text(L("Unlisted"))),
113+                xs_html_text(" "),
114+                xs_html_tag("label",
115+                    xs_html_sctag("input",
116+                        xs_html_attr("type", "radio"),
117+                        xs_html_attr("name", "visibility"),
118+                        xs_html_attr("value", "followers"),
119+                        xs_html_attr(scope == SCOPE_FOLLOWERS ? "checked" : "", NULL)),
120+                    xs_html_text(" "),
121+                    xs_html_text(L("Followers-only"))),
122+                xs_html_text(" "),
123+                xs_html_tag("label",
124+                    xs_html_sctag("input",
125+                        xs_html_attr("type", "radio"),
126+                        xs_html_attr("name", "visibility"),
127+                        xs_html_attr("value", "mentioned"),
128+                        xs_html_attr(scope == SCOPE_MENTIONED ? "checked" : "", NULL)),
129+                    xs_html_text(" "),
130+                    xs_html_text(L("Direct Message")))));
131     }
132 
133     if (redir)
134@@ -1236,7 +1288,7 @@ xs_html *html_top_controls(snac *user)
135             L("What's on your mind?"), "",
136             NULL, NULL,
137             xs_stock(XSTYPE_FALSE), "",
138-            xs_stock(XSTYPE_FALSE), NULL,
139+            SCOPE_PUBLIC, NULL,
140             NULL, 1, NULL, NULL, 0, NULL, NULL),
141 
142         /** operations **/
143@@ -1942,6 +1994,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
144             xs_dict_next(cmap, &note_lang, &dummy, &c);
145         }
146 
147+        const int edit_scope = get_msg_visibility(msg);
148+
149         xs_html_add(controls, xs_html_tag("div",
150             xs_html_tag("p", NULL),
151             html_note(user, L("Edit..."),
152@@ -1949,7 +2003,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
153                 "", prev_src,
154                 id, NULL,
155                 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
156-                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
157+                edit_scope, redir,
158                 NULL, 0, att_files, att_alt_texts, is_draft(user, id),
159                 xs_dict_get(msg, "published"), note_lang)),
160             xs_html_tag("p", NULL));
161@@ -1962,6 +2016,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
162         xs *form_id = xs_fmt("%s_reply_form", md5);
163         xs *redir   = xs_fmt("%s_entry", md5);
164 
165+        const int scope = get_msg_visibility(msg);
166+
167         xs_html_add(controls, xs_html_tag("div",
168             xs_html_tag("p", NULL),
169             html_note(user, L("Reply..."),
170@@ -1969,7 +2025,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
171                 "", ct,
172                 NULL, NULL,
173                 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
174-                xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
175+                scope, redir,
176                 id, 0, NULL, NULL, 0, NULL, NULL)),
177             xs_html_tag("p", NULL));
178     }
179@@ -2061,6 +2117,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
180                 xs_html_attr("name", s1)));
181     }
182 
183+    /* don't show followers-only notes from not followed users */
184+    if (user && get_msg_visibility(msg) == SCOPE_FOLLOWERS &&
185+        strcmp(user->actor, actor) != 0 && following_check(user, actor) == 0) {
186+        return NULL;
187+    }
188+
189     if ((user == NULL || strcmp(actor, user->actor) != 0)
190         && !valid_status(actor_get(actor, NULL))) {
191 
192@@ -2275,7 +2337,6 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
193 
194             has_title = 1;
195         }
196-
197         snac_content = xs_html_tag("div", NULL);
198     }
199 
200@@ -3089,8 +3150,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
201                 xs_html_text(title)));
202     }
203 
204-    xs_html_add(body,
205-        posts);
206+    xs_html_add(body, posts);
207 
208     int mark_shown = 0;
209 
210@@ -3130,8 +3190,9 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
211         if (user == NULL && is_msg_from_private_user(msg))
212             continue;
213 
214-        /* is this message a non-public reply? */
215-        if (user != NULL && !is_msg_public(msg)) {
216+        const int scope = get_msg_visibility(msg);
217+        if (user != NULL && scope != SCOPE_PUBLIC){
218+            /* is this message a non-public reply? */
219             const char *irt = get_in_reply_to(msg);
220 
221             /* is it a reply to something not in the storage? */
222@@ -3146,6 +3207,14 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
223                 }
224             }
225         }
226+        /* hide non-public posts from /instance view */
227+        if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){
228+            continue;
229+        }
230+        /* hide non-public posts viewed from outside */
231+        if (read_only && scope != SCOPE_PUBLIC){
232+            continue;
233+        }
234 
235         xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
236 
237@@ -3256,7 +3325,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
238                 xs_html_tag("div",
239                     xs_html_attr("class", "snac-post-header"),
240                     html_actor_icon(user, actor, xs_dict_get(actor, "published"),
241-                                    NULL, NULL, 0, 1, proxy, NULL, NULL)));
242+                                    NULL, NULL, -1, 1, proxy, NULL, NULL)));
243 
244             /* content (user bio) */
245             const char *c = xs_dict_get(actor, "summary");
246@@ -3357,7 +3426,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
247                     "", "",
248                     NULL, actor_id,
249                     xs_stock(XSTYPE_FALSE), "",
250-                    xs_stock(XSTYPE_FALSE), NULL,
251+                    SCOPE_MENTIONED, NULL,
252                     NULL, 0, NULL, NULL, 0, NULL, NULL),
253                 xs_html_tag("p", NULL));
254 
255@@ -3564,7 +3633,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
256                 xs_html_add(entry,
257                     xs_html_tag("div",
258                         xs_html_attr("class", "snac-post"),
259-                        html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
260+                        html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
261         }
262         else
263         if (strcmp(type, "Move") == 0) {
264@@ -3578,7 +3647,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
265                     xs_html_add(entry,
266                         xs_html_tag("div",
267                             xs_html_attr("class", "snac-post"),
268-                            html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
269+                            html_actor_icon(user, old_actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL)));
270                 }
271             }
272         }
273@@ -4445,7 +4514,18 @@ int html_post_handler(const xs_dict *req, const char *q_path,
274         const char *post_date    = xs_dict_get_def(p_vars, "post_date", "");
275         const char *post_time    = xs_dict_get_def(p_vars, "post_time", "");
276         const char *post_lang    = xs_dict_get(p_vars, "post_lang");
277-        int priv             = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
278+        const char *visibility   = xs_dict_get(p_vars, "visibility");
279+        int scope = SCOPE_PUBLIC;  /* default to public */
280+        if (!xs_is_null(visibility)) {
281+            if (strcmp(visibility, "unlisted") == 0)
282+                scope = SCOPE_UNLISTED;
283+            else
284+            if (strcmp(visibility, "followers") == 0)
285+                scope = SCOPE_FOLLOWERS;
286+            else
287+            if (strcmp(visibility, "mentioned") == 0)
288+                scope = SCOPE_MENTIONED;
289+        }
290         int store_as_draft   = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
291         xs *attach_list      = xs_list_new();
292 
293@@ -4528,7 +4608,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
294                 enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
295             }
296             else
297-                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv, post_lang, NULL);
298+                msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, scope, post_lang, NULL);
299 
300             if (sensitive != NULL) {
301                 msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
M main.c
+9, -4
 1@@ -45,6 +45,7 @@ int usage(const char *cmd)
 2         "note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"
 3         "note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n"
 4         "note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n"
 5+        "note_followers {basedir} {uid} {text} [files...] Sends a note only to followers\n"
 6         "boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"
 7         "unboost {basedir} {uid} {url}        Unboosts a post\n"
 8         "resetpwd {basedir} {uid}             Resets the password of a user\n"
 9@@ -800,7 +801,8 @@ int main(int argc, char *argv[])
10 
11     if (strcmp(cmd, "note") == 0 ||             /** **/
12         strcmp(cmd, "note_unlisted") == 0 ||    /** **/
13-        strcmp(cmd, "note_mention") == 0) {     /** **/
14+        strcmp(cmd, "note_mention") == 0 ||     /** **/
15+        strcmp(cmd, "note_followers") == 0) {   /** **/
16         xs *content = NULL;
17         xs *msg = NULL;
18         xs *c_msg = NULL;
19@@ -909,12 +911,15 @@ int main(int argc, char *argv[])
20             return 1;
21         }
22 
23-        int scope = 0;
24+        int scope = SCOPE_PUBLIC;
25         if (strcmp(cmd, "note_mention") == 0)
26-            scope = 1;
27+            scope = SCOPE_MENTIONED;
28         else
29         if (strcmp(cmd, "note_unlisted") == 0)
30-            scope = 2;
31+            scope = SCOPE_UNLISTED;
32+        else
33+        if (strcmp(cmd, "note_followers") == 0)
34+            scope = SCOPE_FOLLOWERS;
35 
36         msg = msg_note(&snac, content, NULL, in_reply_to, attl, scope, getenv("LANG"), post_date);
37 
+16, -5
 1@@ -884,8 +884,16 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 2         st = xs_dict_append(st, "content", s1);
 3     }
 4 
 5-    st = xs_dict_append(st, "visibility",
 6-        is_msg_public(msg) ? "public" : "private");
 7+    /* determine visibility: https://docs.joinmastodon.org/entities/Status/#visibility */
 8+    const int scope = get_msg_visibility(msg);
 9+    if (scope == SCOPE_PUBLIC)
10+        st = xs_dict_append(st, "visibility", "public");
11+    else if (scope == SCOPE_FOLLOWERS)
12+        st = xs_dict_append(st, "visibility", "private");
13+    else if (scope == SCOPE_MENTIONED)
14+        st = xs_dict_append(st, "visibility", "direct");
15+    else if (scope == SCOPE_UNLISTED)
16+        st = xs_dict_append(st, "visibility", "unlisted");
17 
18     tmp = xs_dict_get(msg, "sensitive");
19     if (xs_is_null(tmp))
20@@ -2856,12 +2864,15 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
21             }
22 
23             /* prepare the message */
24-            int scope = 1;
25+            int scope = SCOPE_MENTIONED;
26             if (strcmp(visibility, "unlisted") == 0)
27-                scope = 2;
28+                scope = SCOPE_UNLISTED;
29             else
30             if (strcmp(visibility, "public") == 0)
31-                scope = 0;
32+                scope = SCOPE_PUBLIC;
33+            else
34+            if (strcmp(visibility, "private") == 0)
35+                scope = SCOPE_FOLLOWERS;
36 
37             xs *msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language, NULL);
38 
M snac.h
+8, -0
 1@@ -78,6 +78,13 @@ typedef struct {
 2 
 3 extern srv_state *p_state;
 4 
 5+enum {
 6+    SCOPE_PUBLIC = 0,
 7+    SCOPE_MENTIONED = 1,
 8+    SCOPE_UNLISTED = 2,
 9+    SCOPE_FOLLOWERS = 3,
10+};
11+
12 void snac_log(snac *user, xs_str *str);
13 #define snac_debug(user, level, str) do { if (dbglevel >= (level)) \
14     { snac_log((user), (str)); } } while (0)
15@@ -367,6 +374,7 @@ xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
16 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
17                       const xs_list *opts, int multiple, int end_secs);
18 xs_dict *msg_replies(snac *user, const char *id, int fill);
19+int get_msg_visibility(const xs_dict *msg);
20 
21 int activitypub_request(snac *snac, const char *url, xs_dict **data);
22 int actor_request(snac *user, const char *actor, xs_dict **data);