commit 4f5b49b
Merge branch 'master' of https://codeberg.org/daltux/snac2 into docker_tzdata
8 files changed,
+262,
-40
+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);
M
utils.c
+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)) {