commit 4f5b49b

daltux  ·  2025-09-04 18:30:59 +0000 UTC
parents 0f1b676, 6dae96d
Merge branch 'master' of https://codeberg.org/daltux/snac2 into docker_tzdata
8 files changed,  +262, -40
M data.c
M main.c
M snac.h
+5, -1
 1@@ -1,6 +1,6 @@
 2 # Release Notes
 3 
 4-## UNRELEASED
 5+## 2.82
 6 
 7 The language in which a post is written can now be set from the UI; you must configure the list of languages you usually post in in the User Settings.
 8 
 9@@ -16,6 +16,10 @@ Added nodeinfo 2.1 support.
10 
11 Fixed boosts from the command line not showing in the public timeline (contributed by xvello).
12 
13+Updated several language files (contributed by zen and daltux).
14+
15+Retrieving a post's replies is now possible via ActivityPub.
16+
17 ## 2.81
18 
19 If the `propagate_local_purge` configuration variable is set to `true` in `server.json`, purged local posts generate a `Delete` activity that is sent everywhere, instead of only deleted from the filesystem.
+212, -24
  1@@ -946,36 +946,33 @@ void collect_replies(snac *user, const char *id)
  2         return;
  3     }
  4 
  5-    const char *next = xs_dict_get_path(obj, "replies.first.next");
  6-    if (!xs_is_string(next)) {
  7-        snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have a replies.first.next URL", id));
  8+    const xs_dict *replies_first = xs_dict_get_path(obj, "replies.first");
  9+    if (!xs_is_dict(replies_first)) {
 10+        snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have replies.first", id));
 11         return;
 12     }
 13 
 14-    /* pick the first level replies (may be empty) */
 15-    const xs_list *level0_replies = xs_dict_get_path(obj, "replies.first.items");
 16+    const xs_list *level0_replies = xs_dict_get(replies_first, "items");
 17+    const xs_list *level1_replies = NULL;
 18 
 19+    const char *next = xs_dict_get(replies_first, "next");
 20     xs *reply_obj = NULL;
 21 
 22-    if (!valid_status(object_get(next, &reply_obj))) {
 23-        if (!valid_status(activitypub_request(user, next, &reply_obj))) {
 24-            snac_debug(user, 1, xs_fmt("collect_replies: cannot get replies object '%s'", next));
 25-            return;
 26-        }
 27-    }
 28-
 29-    const xs_list *level1_replies = xs_dict_get(reply_obj, "items");
 30-    if (!xs_is_list(level1_replies)) {
 31-        snac_debug(user, 1, xs_fmt("collect_replies: cannot get reply items from object '%s'", next));
 32+    if (xs_is_string(next) && !valid_status(activitypub_request(user, next, &reply_obj))) {
 33+        snac_debug(user, 1, xs_fmt("collect_replies: error getting next replies object '%s'", next));
 34         return;
 35     }
 36 
 37-    xs *items = NULL;
 38+    if (xs_is_dict(reply_obj))
 39+        level1_replies = xs_dict_get(reply_obj, "items");
 40+
 41+    xs *items = xs_list_new();
 42 
 43     if (xs_is_list(level0_replies))
 44-        items = xs_list_cat(xs_dup(level0_replies), level1_replies);
 45-    else
 46-        items = xs_dup(level1_replies);
 47+        items = xs_list_cat(items, level0_replies);
 48+
 49+    if (xs_is_list(level1_replies))
 50+        items = xs_list_cat(items, level1_replies);
 51 
 52     const xs_val *v;
 53 
 54@@ -1024,6 +1021,107 @@ void collect_replies(snac *user, const char *id)
 55 }
 56 
 57 
 58+void collect_outbox(snac *user, const char *actor_id)
 59+/* gets an actor's outbox and inserts a bunch of posts in a user's timeline */
 60+{
 61+    int status;
 62+    xs *actor = NULL;
 63+
 64+    if (!valid_status(status = actor_request(user, actor_id, &actor))) {
 65+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor object '%s' %d", actor_id, status));
 66+        return;
 67+    }
 68+
 69+    xs *outbox = NULL;
 70+    const char *outbox_url = xs_dict_get(actor, "outbox");
 71+
 72+    if (!xs_is_string(outbox_url))
 73+        return;
 74+
 75+    if (!valid_status(status = activitypub_request(user, outbox_url, &outbox))) {
 76+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor outbox '%s' %d", outbox_url, status));
 77+        return;
 78+    }
 79+
 80+    const xs_list *ordered_items = xs_dict_get(outbox, "orderedItems");
 81+
 82+    if (!xs_is_list(ordered_items)) {
 83+        /* the list is not here; does it have a 'first'? */
 84+        const char *first = xs_dict_get(outbox, "first");
 85+
 86+        if (xs_is_string(first)) {
 87+            /* download this instead */
 88+            xs *first2 = xs_dup(first);
 89+            xs_free(outbox);
 90+
 91+            if (!valid_status(status = activitypub_request(user, first2, &outbox))) {
 92+                snac_debug(user, 1, xs_fmt("collect_outbox: cannot get first page of outbox '%s' %d", first2, status));
 93+                return;
 94+            }
 95+
 96+            /* last chance */
 97+            ordered_items = xs_dict_get(outbox, "orderedItems");
 98+        }
 99+    }
100+
101+    if (!xs_is_list(ordered_items)) {
102+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get list of posts for actor '%s' outbox", actor_id));
103+        return;
104+    }
105+
106+    /* well, ok, then */
107+    int max = 4;
108+    const xs_val *v;
109+
110+    xs_list_foreach(ordered_items, v) {
111+        if (max == 0)
112+            break;
113+
114+        xs *post = NULL;
115+
116+        if (xs_is_string(v)) {
117+            /* it's probably the post url */
118+            if (!valid_status(activitypub_request(user, v, &post)))
119+                continue;
120+        }
121+        else
122+        if (xs_is_dict(v))
123+            post = xs_dup(v);
124+
125+        if (post == NULL)
126+            continue;
127+
128+        const char *type = xs_dict_get(post, "type");
129+
130+        if (!xs_is_string(type) || strcmp(type, "Create")) {
131+            /* not a post */
132+            continue;
133+        }
134+
135+        const xs_dict *object = xs_dict_get(post, "object");
136+
137+        if (!xs_is_dict(object))
138+            continue;
139+
140+        type = xs_dict_get(object, "type");
141+        const char *id = xs_dict_get(object, "id");
142+        const char *attr_to = get_atto(object);
143+
144+        if (!xs_is_string(type) || !xs_is_string(id) || !xs_is_string(attr_to))
145+            continue;
146+
147+        if (!timeline_here(user, id)) {
148+            timeline_add(user, id, object);
149+            snac_log(user, xs_fmt("new '%s' (collect_outbox) %s %s", type, attr_to, id));
150+        }
151+        else
152+            snac_debug(user, 1, xs_fmt("collect_outbox: post '%s' already here", id));
153+
154+        max--;
155+    }
156+}
157+
158+
159 void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg)
160 /* notifies the user of relevant events */
161 {
162@@ -1267,6 +1365,45 @@ xs_dict *msg_collection(snac *snac, const char *id, int items)
163 }
164 
165 
166+xs_dict *msg_replies(snac *user, const char *id, int fill)
167+/* creates a CollectionPage with replies of id */
168+{
169+    xs *r_id = xs_replace(id, "/p/", "/r/");
170+    xs *r_idp = xs_fmt("%s#page", r_id);
171+    xs *r_idh = xs_fmt("%s#hdr", r_id);
172+
173+    xs_dict *msg = msg_base(user, "CollectionPage", r_idp, NULL, NULL, NULL);
174+
175+    msg = xs_dict_set(msg, "partOf", r_idh);
176+
177+    xs *items = xs_list_new();
178+    if (fill) {
179+        xs *children = object_children(id);
180+        const char *md5;
181+
182+        xs_list_foreach(children, md5) {
183+            xs *obj = NULL;
184+
185+            if (valid_status(object_get_by_md5(md5, &obj)) && is_msg_public(obj)) {
186+                const char *c_id = xs_dict_get(obj, "id");
187+
188+                if (xs_is_string(c_id))
189+                    items = xs_list_append(items, c_id);
190+            }
191+        }
192+    }
193+    else {
194+        msg = xs_dict_del(msg, "@context");
195+        msg = xs_dict_del(msg, "id");
196+        msg = xs_dict_set(msg, "next", r_idp);
197+    }
198+
199+    msg = xs_dict_set(msg, "items", items);
200+
201+    return msg;
202+}
203+
204+
205 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
206 /* creates an Accept message (as a response to a Follow) */
207 {
208@@ -1871,6 +2008,20 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
209         }
210     }
211 
212+    if (!priv) {
213+        /* create the replies object */
214+        xs *replies = xs_dict_new();
215+        xs *r_id = xs_replace(id, "/p/", "/r/");
216+        xs *h_id = xs_fmt("%s#hdr", r_id);
217+        xs *rp = msg_replies(snac, id, 0);
218+
219+        replies = xs_dict_set(replies, "id", h_id);
220+        replies = xs_dict_set(replies, "type", "Collection");
221+        replies = xs_dict_set(replies, "first", rp);
222+
223+        msg = xs_dict_set(msg, "replies", replies);
224+    }
225+
226     return msg;
227 }
228 
229@@ -2437,6 +2588,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
230             if (following_check(snac, actor)) {
231                 following_add(snac, actor, msg);
232                 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
233+
234+                /* request a bit of this fellow's outbox */
235+                enqueue_collect_outbox(snac, actor);
236             }
237             else
238                 snac_log(snac, xs_fmt("spurious follow accept from %s", actor));
239@@ -2538,10 +2692,14 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
240                 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
241             else
242             if (object_here(id)) {
243-                object_add_ow(id, object);
244-                timeline_touch(snac);
245+                if (xs_startswith(id, srv_baseurl) && !xs_startswith(id, actor))
246+                    snac_log(snac, xs_fmt("ignored incorrect 'Update' %s %s", actor, id));
247+                else {
248+                    object_add_ow(id, object);
249+                    timeline_touch(snac);
250 
251-                snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
252+                    snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
253+                }
254             }
255             else
256                 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id));
257@@ -2578,8 +2736,12 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
258             snac_log(snac, xs_fmt("malformed message: no 'id' field"));
259         else
260         if (object_here(object)) {
261-            timeline_del(snac, object);
262-            snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
263+            if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor))
264+                snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object));
265+            else {
266+                timeline_del(snac, object);
267+                snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
268+            }
269         }
270         else
271             snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
272@@ -2920,6 +3082,12 @@ void process_user_queue_item(snac *user, xs_dict *q_item)
273 
274         collect_replies(user, post);
275     }
276+    else
277+    if (strcmp(type, "collect_outbox") == 0) {
278+        const char *actor_id = xs_dict_get(q_item, "message");
279+
280+        collect_outbox(user, actor_id);
281+    }
282     else
283         snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
284 }
285@@ -3390,6 +3558,26 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
286         if (valid_status(status) && !is_msg_public(msg))
287             status = HTTP_STATUS_NOT_FOUND;
288     }
289+    else
290+    if (xs_startswith(p_path, "r/")) {
291+        /* replies to a post */
292+        xs *s = xs_dup(p_path);
293+        s[0] = 'p';
294+
295+        xs *id = xs_fmt("%s/%s", snac.actor, s);
296+
297+        xs *obj = NULL;
298+        status = object_get(id, &obj);
299+
300+        /* don't return non-public objects */
301+        if (!valid_status(status))
302+            status = HTTP_STATUS_NOT_FOUND;
303+        else
304+        if (!is_msg_public(obj))
305+            status = HTTP_STATUS_NOT_FOUND;
306+        else
307+            msg = msg_replies(&snac, id, 1);
308+    }
309     else
310         status = HTTP_STATUS_NOT_FOUND;
311 
M data.c
+16, -3
 1@@ -2429,8 +2429,8 @@ xs_list *list_timeline(snac *user, const char *list, int skip, int show)
 2 }
 3 
 4 
 5-xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
 6-/* list content management */
 7+xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
 8+/* list member management */
 9 {
10     xs_val *l = NULL;
11 
12@@ -2443,7 +2443,7 @@ xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op
13     xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
14 
15     switch (op) {
16-    case 0: /** list content **/
17+    case 0: /** list members **/
18         l = index_list(fn, XS_ALL);
19 
20         break;
21@@ -3589,6 +3589,19 @@ void enqueue_collect_replies(snac *user, const char *post)
22 }
23 
24 
25+void enqueue_collect_outbox(snac *user, const char *actor_id)
26+/* enqueues a collect outbox request */
27+{
28+    xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
29+    const char *ntid = xs_dict_get(qmsg, "ntid");
30+    xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
31+
32+    qmsg = _enqueue_put(fn, qmsg);
33+
34+    snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
35+}
36+
37+
38 int was_question_voted(snac *user, const char *id)
39 /* returns true if the user voted in this poll */
40 {
+2, -0
1@@ -282,6 +282,8 @@ necessary information will be prompted for.
2 Deletes a user, unfollowing all accounts first.
3 .It Cm resetpwd Ar basedir Ar uid
4 Resets a user's password to a new, random one.
5+.It Cm update Ar basedir Ar uid
6+Sends a user's updated profile to following instances.
7 .It Cm queue Ar basedir Ar uid
8 Processes the output queue of the specified user, sending all
9 enqueued messages and re-enqueing the failing ones. This command
M main.c
+9, -3
 1@@ -30,6 +30,7 @@ int usage(const char *cmd)
 2         "upgrade {basedir}                    Upgrade to a new version\n"
 3         "adduser {basedir} [{uid}]            Adds a new user\n"
 4         "deluser {basedir} {uid}              Deletes a user\n"
 5+        "update {basedir} {uid}               Sends a user's updated profile\n"
 6         "httpd {basedir}                      Starts the HTTPD daemon\n"
 7         "purge {basedir}                      Purges old data\n"
 8         "state {basedir}                      Prints server state\n"
 9@@ -354,7 +355,7 @@ int main(int argc, char *argv[])
10         xs *lid = list_maint(&snac, url, 4);
11 
12         if (lid != NULL) {
13-            xs *lcont = list_content(&snac, lid, NULL, 0);
14+            xs *lcont = list_members(&snac, lid, NULL, 0);
15             const char *md5;
16 
17             xs_list_foreach(lcont, md5) {
18@@ -410,7 +411,7 @@ int main(int argc, char *argv[])
19                 if (valid_status(webfinger_request(account, &actor, &uid))) {
20                     xs *md5 = xs_md5_hex(actor, strlen(actor));
21 
22-                    list_content(&snac, lid, md5, 1);
23+                    list_members(&snac, lid, md5, 1);
24                     printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid);
25                 }
26                 else
27@@ -433,7 +434,7 @@ int main(int argc, char *argv[])
28             if (lid != NULL) {
29                 xs *md5 = xs_md5_hex(account, strlen(account));
30 
31-                list_content(&snac, lid, md5, 2);
32+                list_members(&snac, lid, md5, 2);
33                 printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid);
34             }
35             else
36@@ -738,6 +739,11 @@ int main(int argc, char *argv[])
37         return 0;
38     }
39 
40+    if (strcmp(cmd, "collect_outbox") == 0) { /** **/
41+        enqueue_collect_outbox(&snac, url);
42+        return 0;
43+    }
44+
45     if (strcmp(cmd, "insert") == 0) { /** **/
46         int status;
47         xs *data = NULL;
+4, -4
 1@@ -1560,7 +1560,7 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid)
 2         const char *list_id    = xs_list_get(li, 0);
 3         const char *list_title = xs_list_get(li, 1);
 4         if (uid) {
 5-            xs *users = list_content(user, list_id, NULL, 0);
 6+            xs *users = list_members(user, list_id, NULL, 0);
 7             if (xs_list_in(users, actor_md5) == -1)
 8                 continue;
 9         }
10@@ -2087,7 +2087,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
11                     p = xs_list_get(l, -2);
12 
13                     if (p && xs_is_hex(p)) {
14-                        xs *actors = list_content(&snac1, p, NULL, 0);
15+                        xs *actors = list_members(&snac1, p, NULL, 0);
16                         xs *out = xs_list_new();
17                         int c = 0;
18                         const char *v;
19@@ -3297,7 +3297,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
20                     const char *v;
21 
22                     while (xs_list_next(accts, &v, &c)) {
23-                        list_content(&snac, id, v, 1);
24+                        list_members(&snac, id, v, 1);
25                     }
26 
27                     xs *out = xs_dict_new();
28@@ -3507,7 +3507,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
29                     const char *v;
30 
31                     while (xs_list_next(accts, &v, &c)) {
32-                        list_content(&snac, p, v, 2);
33+                        list_members(&snac, p, v, 2);
34                     }
35                 }
36                 else {
M snac.h
+5, -2
 1@@ -1,7 +1,7 @@
 2 /* snac - A simple, minimalistic ActivityPub instance */
 3 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 4 
 5-#define VERSION "2.82-dev"
 6+#define VERSION "2.82"
 7 
 8 #define USER_AGENT "snac/" VERSION
 9 
10@@ -233,7 +233,7 @@ xs_list *tag_search(const char *tag, int skip, int show);
11 xs_val *list_maint(snac *user, const char *list, int op);
12 xs_str *list_timeline_fn(snac *user, const char *list);
13 xs_list *list_timeline(snac *user, const char *list, int skip, int show);
14-xs_val *list_content(snac *user, const char *list_id, const char *actor_md5, int op);
15+xs_val *list_members(snac *user, const char *list_id, const char *actor_md5, int op);
16 void list_distribute(snac *user, const char *who, const xs_dict *post);
17 
18 int actor_add(const char *actor, const xs_dict *msg);
19@@ -298,6 +298,7 @@ void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs);
20 void enqueue_webmention(const xs_dict *msg);
21 void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries);
22 void enqueue_collect_replies(snac *user, const char *post);
23+void enqueue_collect_outbox(snac *user, const char *actor_id);
24 
25 int was_question_voted(snac *user, const char *id);
26 
27@@ -336,6 +337,7 @@ const char *default_avatar_base64(void);
28 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
29 
30 void collect_replies(snac *user, const char *id);
31+void collect_outbox(snac *user, const char *actor_id);
32 
33 const char *get_atto(const xs_dict *msg);
34 const char *get_in_reply_to(const xs_dict *msg);
35@@ -360,6 +362,7 @@ xs_dict *msg_move(snac *user, const char *new_account);
36 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
37 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
38                       const xs_list *opts, int multiple, int end_secs);
39+xs_dict *msg_replies(snac *user, const char *id, int fill);
40 
41 int activitypub_request(snac *snac, const char *url, xs_dict **data);
42 int actor_request(snac *user, const char *actor, xs_dict **data);
+9, -3
 1@@ -229,6 +229,9 @@ int snac_init(const char *basedir)
 2     xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
 3     mkdirx(ibdir);
 4 
 5+    xs *langdir = xs_fmt("%s/lang", srv_basedir);
 6+    mkdirx(langdir);
 7+
 8     xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
 9     if ((f = fopen(gfn, "w")) == NULL) {
10         printf("ERROR: cannot create '%s'\n", gfn);
11@@ -253,7 +256,10 @@ int snac_init(const char *basedir)
12     xs_json_dump(srv_config, 4, f);
13     fclose(f);
14 
15-    printf("Done.\n");
16+    printf("Done.\n\n");
17+
18+    printf("Wanted web UI language files (.po) must be copied manually to %s\n", langdir);
19+
20     return 0;
21 }
22 
23@@ -681,7 +687,7 @@ void export_csv(snac *user)
24             const char *lid = xs_list_get(li, 0);
25             const char *ltitle = xs_list_get(li, 1);
26 
27-            xs *actors = list_content(user, lid, NULL, 0);
28+            xs *actors = list_members(user, lid, NULL, 0);
29             const char *md5;
30 
31             xs_list_foreach(actors, md5) {
32@@ -907,7 +913,7 @@ void import_list_csv(snac *user, const char *ifn)
33                     if (valid_status(webfinger_request(acct, &url, &uid))) {
34                         xs *actor_md5 = xs_md5_hex(url, strlen(url));
35 
36-                        list_content(user, list_id, actor_md5, 1);
37+                        list_members(user, list_id, actor_md5, 1);
38                         snac_log(user, xs_fmt("Added %s to list %s", url, lname));
39 
40                         if (!following_check(user, url)) {