summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar daltux2025-09-04 15:30:59 -0300
committerGravatar daltux2025-09-04 15:30:59 -0300
commit4f5b49bc2476ab6986c3364aa0b88e0cc97040c4 (patch)
treea793c69df7ef6c00fbb170905719214662461897
parentAdd tzdata to container image (diff)
parentMerge branch 'master' of grunfink-codeberg:grunfink/snac2 (diff)
downloadsnac2-4f5b49bc2476ab6986c3364aa0b88e0cc97040c4.tar.gz
snac2-4f5b49bc2476ab6986c3364aa0b88e0cc97040c4.tar.xz
snac2-4f5b49bc2476ab6986c3364aa0b88e0cc97040c4.zip
Merge branch 'master' of https://codeberg.org/daltux/snac2 into docker_tzdata
-rw-r--r--RELEASE_NOTES.md6
-rw-r--r--activitypub.c236
-rw-r--r--data.c19
-rw-r--r--doc/snac.12
-rw-r--r--main.c12
-rw-r--r--mastoapi.c8
-rw-r--r--snac.h7
-rw-r--r--utils.c12
8 files changed, 262 insertions, 40 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 0a3d07f..8dfc18c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,6 @@
1# Release Notes 1# Release Notes
2 2
3## UNRELEASED 3## 2.82
4 4
5The 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. 5The 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.
6 6
@@ -16,6 +16,10 @@ Added nodeinfo 2.1 support.
16 16
17Fixed boosts from the command line not showing in the public timeline (contributed by xvello). 17Fixed boosts from the command line not showing in the public timeline (contributed by xvello).
18 18
19Updated several language files (contributed by zen and daltux).
20
21Retrieving a post's replies is now possible via ActivityPub.
22
19## 2.81 23## 2.81
20 24
21If 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. 25If 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.
diff --git a/activitypub.c b/activitypub.c
index 065fbcd..2c0aa98 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -946,36 +946,33 @@ void collect_replies(snac *user, const char *id)
946 return; 946 return;
947 } 947 }
948 948
949 const char *next = xs_dict_get_path(obj, "replies.first.next"); 949 const xs_dict *replies_first = xs_dict_get_path(obj, "replies.first");
950 if (!xs_is_string(next)) { 950 if (!xs_is_dict(replies_first)) {
951 snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have a replies.first.next URL", id)); 951 snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have replies.first", id));
952 return; 952 return;
953 } 953 }
954 954
955 /* pick the first level replies (may be empty) */ 955 const xs_list *level0_replies = xs_dict_get(replies_first, "items");
956 const xs_list *level0_replies = xs_dict_get_path(obj, "replies.first.items"); 956 const xs_list *level1_replies = NULL;
957 957
958 const char *next = xs_dict_get(replies_first, "next");
958 xs *reply_obj = NULL; 959 xs *reply_obj = NULL;
959 960
960 if (!valid_status(object_get(next, &reply_obj))) { 961 if (xs_is_string(next) && !valid_status(activitypub_request(user, next, &reply_obj))) {
961 if (!valid_status(activitypub_request(user, next, &reply_obj))) { 962 snac_debug(user, 1, xs_fmt("collect_replies: error getting next replies object '%s'", next));
962 snac_debug(user, 1, xs_fmt("collect_replies: cannot get replies object '%s'", next));
963 return;
964 }
965 }
966
967 const xs_list *level1_replies = xs_dict_get(reply_obj, "items");
968 if (!xs_is_list(level1_replies)) {
969 snac_debug(user, 1, xs_fmt("collect_replies: cannot get reply items from object '%s'", next));
970 return; 963 return;
971 } 964 }
972 965
973 xs *items = NULL; 966 if (xs_is_dict(reply_obj))
967 level1_replies = xs_dict_get(reply_obj, "items");
968
969 xs *items = xs_list_new();
974 970
975 if (xs_is_list(level0_replies)) 971 if (xs_is_list(level0_replies))
976 items = xs_list_cat(xs_dup(level0_replies), level1_replies); 972 items = xs_list_cat(items, level0_replies);
977 else 973
978 items = xs_dup(level1_replies); 974 if (xs_is_list(level1_replies))
975 items = xs_list_cat(items, level1_replies);
979 976
980 const xs_val *v; 977 const xs_val *v;
981 978
@@ -1024,6 +1021,107 @@ void collect_replies(snac *user, const char *id)
1024} 1021}
1025 1022
1026 1023
1024void collect_outbox(snac *user, const char *actor_id)
1025/* gets an actor's outbox and inserts a bunch of posts in a user's timeline */
1026{
1027 int status;
1028 xs *actor = NULL;
1029
1030 if (!valid_status(status = actor_request(user, actor_id, &actor))) {
1031 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor object '%s' %d", actor_id, status));
1032 return;
1033 }
1034
1035 xs *outbox = NULL;
1036 const char *outbox_url = xs_dict_get(actor, "outbox");
1037
1038 if (!xs_is_string(outbox_url))
1039 return;
1040
1041 if (!valid_status(status = activitypub_request(user, outbox_url, &outbox))) {
1042 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor outbox '%s' %d", outbox_url, status));
1043 return;
1044 }
1045
1046 const xs_list *ordered_items = xs_dict_get(outbox, "orderedItems");
1047
1048 if (!xs_is_list(ordered_items)) {
1049 /* the list is not here; does it have a 'first'? */
1050 const char *first = xs_dict_get(outbox, "first");
1051
1052 if (xs_is_string(first)) {
1053 /* download this instead */
1054 xs *first2 = xs_dup(first);
1055 xs_free(outbox);
1056
1057 if (!valid_status(status = activitypub_request(user, first2, &outbox))) {
1058 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get first page of outbox '%s' %d", first2, status));
1059 return;
1060 }
1061
1062 /* last chance */
1063 ordered_items = xs_dict_get(outbox, "orderedItems");
1064 }
1065 }
1066
1067 if (!xs_is_list(ordered_items)) {
1068 snac_debug(user, 1, xs_fmt("collect_outbox: cannot get list of posts for actor '%s' outbox", actor_id));
1069 return;
1070 }
1071
1072 /* well, ok, then */
1073 int max = 4;
1074 const xs_val *v;
1075
1076 xs_list_foreach(ordered_items, v) {
1077 if (max == 0)
1078 break;
1079
1080 xs *post = NULL;
1081
1082 if (xs_is_string(v)) {
1083 /* it's probably the post url */
1084 if (!valid_status(activitypub_request(user, v, &post)))
1085 continue;
1086 }
1087 else
1088 if (xs_is_dict(v))
1089 post = xs_dup(v);
1090
1091 if (post == NULL)
1092 continue;
1093
1094 const char *type = xs_dict_get(post, "type");
1095
1096 if (!xs_is_string(type) || strcmp(type, "Create")) {
1097 /* not a post */
1098 continue;
1099 }
1100
1101 const xs_dict *object = xs_dict_get(post, "object");
1102
1103 if (!xs_is_dict(object))
1104 continue;
1105
1106 type = xs_dict_get(object, "type");
1107 const char *id = xs_dict_get(object, "id");
1108 const char *attr_to = get_atto(object);
1109
1110 if (!xs_is_string(type) || !xs_is_string(id) || !xs_is_string(attr_to))
1111 continue;
1112
1113 if (!timeline_here(user, id)) {
1114 timeline_add(user, id, object);
1115 snac_log(user, xs_fmt("new '%s' (collect_outbox) %s %s", type, attr_to, id));
1116 }
1117 else
1118 snac_debug(user, 1, xs_fmt("collect_outbox: post '%s' already here", id));
1119
1120 max--;
1121 }
1122}
1123
1124
1027void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg) 1125void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg)
1028/* notifies the user of relevant events */ 1126/* notifies the user of relevant events */
1029{ 1127{
@@ -1267,6 +1365,45 @@ xs_dict *msg_collection(snac *snac, const char *id, int items)
1267} 1365}
1268 1366
1269 1367
1368xs_dict *msg_replies(snac *user, const char *id, int fill)
1369/* creates a CollectionPage with replies of id */
1370{
1371 xs *r_id = xs_replace(id, "/p/", "/r/");
1372 xs *r_idp = xs_fmt("%s#page", r_id);
1373 xs *r_idh = xs_fmt("%s#hdr", r_id);
1374
1375 xs_dict *msg = msg_base(user, "CollectionPage", r_idp, NULL, NULL, NULL);
1376
1377 msg = xs_dict_set(msg, "partOf", r_idh);
1378
1379 xs *items = xs_list_new();
1380 if (fill) {
1381 xs *children = object_children(id);
1382 const char *md5;
1383
1384 xs_list_foreach(children, md5) {
1385 xs *obj = NULL;
1386
1387 if (valid_status(object_get_by_md5(md5, &obj)) && is_msg_public(obj)) {
1388 const char *c_id = xs_dict_get(obj, "id");
1389
1390 if (xs_is_string(c_id))
1391 items = xs_list_append(items, c_id);
1392 }
1393 }
1394 }
1395 else {
1396 msg = xs_dict_del(msg, "@context");
1397 msg = xs_dict_del(msg, "id");
1398 msg = xs_dict_set(msg, "next", r_idp);
1399 }
1400
1401 msg = xs_dict_set(msg, "items", items);
1402
1403 return msg;
1404}
1405
1406
1270xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to) 1407xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
1271/* creates an Accept message (as a response to a Follow) */ 1408/* creates an Accept message (as a response to a Follow) */
1272{ 1409{
@@ -1871,6 +2008,20 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
1871 } 2008 }
1872 } 2009 }
1873 2010
2011 if (!priv) {
2012 /* create the replies object */
2013 xs *replies = xs_dict_new();
2014 xs *r_id = xs_replace(id, "/p/", "/r/");
2015 xs *h_id = xs_fmt("%s#hdr", r_id);
2016 xs *rp = msg_replies(snac, id, 0);
2017
2018 replies = xs_dict_set(replies, "id", h_id);
2019 replies = xs_dict_set(replies, "type", "Collection");
2020 replies = xs_dict_set(replies, "first", rp);
2021
2022 msg = xs_dict_set(msg, "replies", replies);
2023 }
2024
1874 return msg; 2025 return msg;
1875} 2026}
1876 2027
@@ -2437,6 +2588,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2437 if (following_check(snac, actor)) { 2588 if (following_check(snac, actor)) {
2438 following_add(snac, actor, msg); 2589 following_add(snac, actor, msg);
2439 snac_log(snac, xs_fmt("confirmed follow from %s", actor)); 2590 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
2591
2592 /* request a bit of this fellow's outbox */
2593 enqueue_collect_outbox(snac, actor);
2440 } 2594 }
2441 else 2595 else
2442 snac_log(snac, xs_fmt("spurious follow accept from %s", actor)); 2596 snac_log(snac, xs_fmt("spurious follow accept from %s", actor));
@@ -2538,10 +2692,14 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2538 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2692 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2539 else 2693 else
2540 if (object_here(id)) { 2694 if (object_here(id)) {
2541 object_add_ow(id, object); 2695 if (xs_startswith(id, srv_baseurl) && !xs_startswith(id, actor))
2542 timeline_touch(snac); 2696 snac_log(snac, xs_fmt("ignored incorrect 'Update' %s %s", actor, id));
2697 else {
2698 object_add_ow(id, object);
2699 timeline_touch(snac);
2543 2700
2544 snac_log(snac, xs_fmt("updated '%s' %s", utype, id)); 2701 snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
2702 }
2545 } 2703 }
2546 else 2704 else
2547 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id)); 2705 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id));
@@ -2578,8 +2736,12 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2578 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2736 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2579 else 2737 else
2580 if (object_here(object)) { 2738 if (object_here(object)) {
2581 timeline_del(snac, object); 2739 if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor))
2582 snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object)); 2740 snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object));
2741 else {
2742 timeline_del(snac, object);
2743 snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
2744 }
2583 } 2745 }
2584 else 2746 else
2585 snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object)); 2747 snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
@@ -2921,6 +3083,12 @@ void process_user_queue_item(snac *user, xs_dict *q_item)
2921 collect_replies(user, post); 3083 collect_replies(user, post);
2922 } 3084 }
2923 else 3085 else
3086 if (strcmp(type, "collect_outbox") == 0) {
3087 const char *actor_id = xs_dict_get(q_item, "message");
3088
3089 collect_outbox(user, actor_id);
3090 }
3091 else
2924 snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); 3092 snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
2925} 3093}
2926 3094
@@ -3391,6 +3559,26 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
3391 status = HTTP_STATUS_NOT_FOUND; 3559 status = HTTP_STATUS_NOT_FOUND;
3392 } 3560 }
3393 else 3561 else
3562 if (xs_startswith(p_path, "r/")) {
3563 /* replies to a post */
3564 xs *s = xs_dup(p_path);
3565 s[0] = 'p';
3566
3567 xs *id = xs_fmt("%s/%s", snac.actor, s);
3568
3569 xs *obj = NULL;
3570 status = object_get(id, &obj);
3571
3572 /* don't return non-public objects */
3573 if (!valid_status(status))
3574 status = HTTP_STATUS_NOT_FOUND;
3575 else
3576 if (!is_msg_public(obj))
3577 status = HTTP_STATUS_NOT_FOUND;
3578 else
3579 msg = msg_replies(&snac, id, 1);
3580 }
3581 else
3394 status = HTTP_STATUS_NOT_FOUND; 3582 status = HTTP_STATUS_NOT_FOUND;
3395 3583
3396 if (status == HTTP_STATUS_OK && msg != NULL) { 3584 if (status == HTTP_STATUS_OK && msg != NULL) {
diff --git a/data.c b/data.c
index ca5e3ed..7e90a06 100644
--- a/data.c
+++ b/data.c
@@ -2429,8 +2429,8 @@ xs_list *list_timeline(snac *user, const char *list, int skip, int show)
2429} 2429}
2430 2430
2431 2431
2432xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op) 2432xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
2433/* list content management */ 2433/* list member management */
2434{ 2434{
2435 xs_val *l = NULL; 2435 xs_val *l = NULL;
2436 2436
@@ -2443,7 +2443,7 @@ xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op
2443 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list); 2443 xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
2444 2444
2445 switch (op) { 2445 switch (op) {
2446 case 0: /** list content **/ 2446 case 0: /** list members **/
2447 l = index_list(fn, XS_ALL); 2447 l = index_list(fn, XS_ALL);
2448 2448
2449 break; 2449 break;
@@ -3589,6 +3589,19 @@ void enqueue_collect_replies(snac *user, const char *post)
3589} 3589}
3590 3590
3591 3591
3592void enqueue_collect_outbox(snac *user, const char *actor_id)
3593/* enqueues a collect outbox request */
3594{
3595 xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
3596 const char *ntid = xs_dict_get(qmsg, "ntid");
3597 xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
3598
3599 qmsg = _enqueue_put(fn, qmsg);
3600
3601 snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
3602}
3603
3604
3592int was_question_voted(snac *user, const char *id) 3605int was_question_voted(snac *user, const char *id)
3593/* returns true if the user voted in this poll */ 3606/* returns true if the user voted in this poll */
3594{ 3607{
diff --git a/doc/snac.1 b/doc/snac.1
index 134ee6e..6c9b3d3 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -282,6 +282,8 @@ necessary information will be prompted for.
282Deletes a user, unfollowing all accounts first. 282Deletes a user, unfollowing all accounts first.
283.It Cm resetpwd Ar basedir Ar uid 283.It Cm resetpwd Ar basedir Ar uid
284Resets a user's password to a new, random one. 284Resets a user's password to a new, random one.
285.It Cm update Ar basedir Ar uid
286Sends a user's updated profile to following instances.
285.It Cm queue Ar basedir Ar uid 287.It Cm queue Ar basedir Ar uid
286Processes the output queue of the specified user, sending all 288Processes the output queue of the specified user, sending all
287enqueued messages and re-enqueing the failing ones. This command 289enqueued messages and re-enqueing the failing ones. This command
diff --git a/main.c b/main.c
index e01b6a2..f767355 100644
--- a/main.c
+++ b/main.c
@@ -30,6 +30,7 @@ int usage(const char *cmd)
30 "upgrade {basedir} Upgrade to a new version\n" 30 "upgrade {basedir} Upgrade to a new version\n"
31 "adduser {basedir} [{uid}] Adds a new user\n" 31 "adduser {basedir} [{uid}] Adds a new user\n"
32 "deluser {basedir} {uid} Deletes a user\n" 32 "deluser {basedir} {uid} Deletes a user\n"
33 "update {basedir} {uid} Sends a user's updated profile\n"
33 "httpd {basedir} Starts the HTTPD daemon\n" 34 "httpd {basedir} Starts the HTTPD daemon\n"
34 "purge {basedir} Purges old data\n" 35 "purge {basedir} Purges old data\n"
35 "state {basedir} Prints server state\n" 36 "state {basedir} Prints server state\n"
@@ -354,7 +355,7 @@ int main(int argc, char *argv[])
354 xs *lid = list_maint(&snac, url, 4); 355 xs *lid = list_maint(&snac, url, 4);
355 356
356 if (lid != NULL) { 357 if (lid != NULL) {
357 xs *lcont = list_content(&snac, lid, NULL, 0); 358 xs *lcont = list_members(&snac, lid, NULL, 0);
358 const char *md5; 359 const char *md5;
359 360
360 xs_list_foreach(lcont, md5) { 361 xs_list_foreach(lcont, md5) {
@@ -410,7 +411,7 @@ int main(int argc, char *argv[])
410 if (valid_status(webfinger_request(account, &actor, &uid))) { 411 if (valid_status(webfinger_request(account, &actor, &uid))) {
411 xs *md5 = xs_md5_hex(actor, strlen(actor)); 412 xs *md5 = xs_md5_hex(actor, strlen(actor));
412 413
413 list_content(&snac, lid, md5, 1); 414 list_members(&snac, lid, md5, 1);
414 printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid); 415 printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid);
415 } 416 }
416 else 417 else
@@ -433,7 +434,7 @@ int main(int argc, char *argv[])
433 if (lid != NULL) { 434 if (lid != NULL) {
434 xs *md5 = xs_md5_hex(account, strlen(account)); 435 xs *md5 = xs_md5_hex(account, strlen(account));
435 436
436 list_content(&snac, lid, md5, 2); 437 list_members(&snac, lid, md5, 2);
437 printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid); 438 printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid);
438 } 439 }
439 else 440 else
@@ -738,6 +739,11 @@ int main(int argc, char *argv[])
738 return 0; 739 return 0;
739 } 740 }
740 741
742 if (strcmp(cmd, "collect_outbox") == 0) { /** **/
743 enqueue_collect_outbox(&snac, url);
744 return 0;
745 }
746
741 if (strcmp(cmd, "insert") == 0) { /** **/ 747 if (strcmp(cmd, "insert") == 0) { /** **/
742 int status; 748 int status;
743 xs *data = NULL; 749 xs *data = NULL;
diff --git a/mastoapi.c b/mastoapi.c
index 42f3a47..568426b 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -1560,7 +1560,7 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid)
1560 const char *list_id = xs_list_get(li, 0); 1560 const char *list_id = xs_list_get(li, 0);
1561 const char *list_title = xs_list_get(li, 1); 1561 const char *list_title = xs_list_get(li, 1);
1562 if (uid) { 1562 if (uid) {
1563 xs *users = list_content(user, list_id, NULL, 0); 1563 xs *users = list_members(user, list_id, NULL, 0);
1564 if (xs_list_in(users, actor_md5) == -1) 1564 if (xs_list_in(users, actor_md5) == -1)
1565 continue; 1565 continue;
1566 } 1566 }
@@ -2087,7 +2087,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2087 p = xs_list_get(l, -2); 2087 p = xs_list_get(l, -2);
2088 2088
2089 if (p && xs_is_hex(p)) { 2089 if (p && xs_is_hex(p)) {
2090 xs *actors = list_content(&snac1, p, NULL, 0); 2090 xs *actors = list_members(&snac1, p, NULL, 0);
2091 xs *out = xs_list_new(); 2091 xs *out = xs_list_new();
2092 int c = 0; 2092 int c = 0;
2093 const char *v; 2093 const char *v;
@@ -3297,7 +3297,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
3297 const char *v; 3297 const char *v;
3298 3298
3299 while (xs_list_next(accts, &v, &c)) { 3299 while (xs_list_next(accts, &v, &c)) {
3300 list_content(&snac, id, v, 1); 3300 list_members(&snac, id, v, 1);
3301 } 3301 }
3302 3302
3303 xs *out = xs_dict_new(); 3303 xs *out = xs_dict_new();
@@ -3507,7 +3507,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
3507 const char *v; 3507 const char *v;
3508 3508
3509 while (xs_list_next(accts, &v, &c)) { 3509 while (xs_list_next(accts, &v, &c)) {
3510 list_content(&snac, p, v, 2); 3510 list_members(&snac, p, v, 2);
3511 } 3511 }
3512 } 3512 }
3513 else { 3513 else {
diff --git a/snac.h b/snac.h
index 363855f..a871576 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
3 3
4#define VERSION "2.82-dev" 4#define VERSION "2.82"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -233,7 +233,7 @@ xs_list *tag_search(const char *tag, int skip, int show);
233xs_val *list_maint(snac *user, const char *list, int op); 233xs_val *list_maint(snac *user, const char *list, int op);
234xs_str *list_timeline_fn(snac *user, const char *list); 234xs_str *list_timeline_fn(snac *user, const char *list);
235xs_list *list_timeline(snac *user, const char *list, int skip, int show); 235xs_list *list_timeline(snac *user, const char *list, int skip, int show);
236xs_val *list_content(snac *user, const char *list_id, const char *actor_md5, int op); 236xs_val *list_members(snac *user, const char *list_id, const char *actor_md5, int op);
237void list_distribute(snac *user, const char *who, const xs_dict *post); 237void list_distribute(snac *user, const char *who, const xs_dict *post);
238 238
239int actor_add(const char *actor, const xs_dict *msg); 239int actor_add(const char *actor, const xs_dict *msg);
@@ -298,6 +298,7 @@ void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs);
298void enqueue_webmention(const xs_dict *msg); 298void enqueue_webmention(const xs_dict *msg);
299void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries); 299void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries);
300void enqueue_collect_replies(snac *user, const char *post); 300void enqueue_collect_replies(snac *user, const char *post);
301void enqueue_collect_outbox(snac *user, const char *actor_id);
301 302
302int was_question_voted(snac *user, const char *id); 303int was_question_voted(snac *user, const char *id);
303 304
@@ -336,6 +337,7 @@ const char *default_avatar_base64(void);
336xs_str *process_tags(snac *snac, const char *content, xs_list **tag); 337xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
337 338
338void collect_replies(snac *user, const char *id); 339void collect_replies(snac *user, const char *id);
340void collect_outbox(snac *user, const char *actor_id);
339 341
340const char *get_atto(const xs_dict *msg); 342const char *get_atto(const xs_dict *msg);
341const char *get_in_reply_to(const xs_dict *msg); 343const char *get_in_reply_to(const xs_dict *msg);
@@ -360,6 +362,7 @@ xs_dict *msg_move(snac *user, const char *new_account);
360xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to); 362xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
361xs_dict *msg_question(snac *user, const char *content, xs_list *attach, 363xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
362 const xs_list *opts, int multiple, int end_secs); 364 const xs_list *opts, int multiple, int end_secs);
365xs_dict *msg_replies(snac *user, const char *id, int fill);
363 366
364int activitypub_request(snac *snac, const char *url, xs_dict **data); 367int activitypub_request(snac *snac, const char *url, xs_dict **data);
365int actor_request(snac *user, const char *actor, xs_dict **data); 368int actor_request(snac *user, const char *actor, xs_dict **data);
diff --git a/utils.c b/utils.c
index 8db20bd..bdee09d 100644
--- a/utils.c
+++ b/utils.c
@@ -229,6 +229,9 @@ int snac_init(const char *basedir)
229 xs *ibdir = xs_fmt("%s/inbox", srv_basedir); 229 xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
230 mkdirx(ibdir); 230 mkdirx(ibdir);
231 231
232 xs *langdir = xs_fmt("%s/lang", srv_basedir);
233 mkdirx(langdir);
234
232 xs *gfn = xs_fmt("%s/greeting.html", srv_basedir); 235 xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
233 if ((f = fopen(gfn, "w")) == NULL) { 236 if ((f = fopen(gfn, "w")) == NULL) {
234 printf("ERROR: cannot create '%s'\n", gfn); 237 printf("ERROR: cannot create '%s'\n", gfn);
@@ -253,7 +256,10 @@ int snac_init(const char *basedir)
253 xs_json_dump(srv_config, 4, f); 256 xs_json_dump(srv_config, 4, f);
254 fclose(f); 257 fclose(f);
255 258
256 printf("Done.\n"); 259 printf("Done.\n\n");
260
261 printf("Wanted web UI language files (.po) must be copied manually to %s\n", langdir);
262
257 return 0; 263 return 0;
258} 264}
259 265
@@ -681,7 +687,7 @@ void export_csv(snac *user)
681 const char *lid = xs_list_get(li, 0); 687 const char *lid = xs_list_get(li, 0);
682 const char *ltitle = xs_list_get(li, 1); 688 const char *ltitle = xs_list_get(li, 1);
683 689
684 xs *actors = list_content(user, lid, NULL, 0); 690 xs *actors = list_members(user, lid, NULL, 0);
685 const char *md5; 691 const char *md5;
686 692
687 xs_list_foreach(actors, md5) { 693 xs_list_foreach(actors, md5) {
@@ -907,7 +913,7 @@ void import_list_csv(snac *user, const char *ifn)
907 if (valid_status(webfinger_request(acct, &url, &uid))) { 913 if (valid_status(webfinger_request(acct, &url, &uid))) {
908 xs *actor_md5 = xs_md5_hex(url, strlen(url)); 914 xs *actor_md5 = xs_md5_hex(url, strlen(url));
909 915
910 list_content(user, list_id, actor_md5, 1); 916 list_members(user, list_id, actor_md5, 1);
911 snac_log(user, xs_fmt("Added %s to list %s", url, lname)); 917 snac_log(user, xs_fmt("Added %s to list %s", url, lname));
912 918
913 if (!following_check(user, url)) { 919 if (!following_check(user, url)) {