Selaa lähdekoodia

Merge branch 'master' of https://codeberg.org/daltux/snac2 into docker_tzdata

daltux 10 kuukautta sitten
vanhempi
sitoutus
4f5b49bc24
8 muutettua tiedostoa jossa 262 lisäystä ja 40 poistoa
  1. 5 1
      RELEASE_NOTES.md
  2. 212 24
      activitypub.c
  3. 16 3
      data.c
  4. 2 0
      doc/snac.1
  5. 9 3
      main.c
  6. 4 4
      mastoapi.c
  7. 5 2
      snac.h
  8. 9 3
      utils.c

+ 5 - 1
RELEASE_NOTES.md

@@ -1,6 +1,6 @@
 # Release Notes
 # Release Notes
 
 
-## UNRELEASED
+## 2.82
 
 
 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.
 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.
 
 
@@ -16,6 +16,10 @@ Added nodeinfo 2.1 support.
 
 
 Fixed boosts from the command line not showing in the public timeline (contributed by xvello).
 Fixed boosts from the command line not showing in the public timeline (contributed by xvello).
 
 
+Updated several language files (contributed by zen and daltux).
+
+Retrieving a post's replies is now possible via ActivityPub.
+
 ## 2.81
 ## 2.81
 
 
 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.
 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
activitypub.c

@@ -946,36 +946,33 @@ void collect_replies(snac *user, const char *id)
         return;
         return;
     }
     }
 
 
-    const char *next = xs_dict_get_path(obj, "replies.first.next");
-    if (!xs_is_string(next)) {
-        snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have a replies.first.next URL", id));
+    const xs_dict *replies_first = xs_dict_get_path(obj, "replies.first");
+    if (!xs_is_dict(replies_first)) {
+        snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have replies.first", id));
         return;
         return;
     }
     }
 
 
-    /* pick the first level replies (may be empty) */
-    const xs_list *level0_replies = xs_dict_get_path(obj, "replies.first.items");
+    const xs_list *level0_replies = xs_dict_get(replies_first, "items");
+    const xs_list *level1_replies = NULL;
 
 
+    const char *next = xs_dict_get(replies_first, "next");
     xs *reply_obj = NULL;
     xs *reply_obj = NULL;
 
 
-    if (!valid_status(object_get(next, &reply_obj))) {
-        if (!valid_status(activitypub_request(user, next, &reply_obj))) {
-            snac_debug(user, 1, xs_fmt("collect_replies: cannot get replies object '%s'", next));
-            return;
-        }
-    }
-
-    const xs_list *level1_replies = xs_dict_get(reply_obj, "items");
-    if (!xs_is_list(level1_replies)) {
-        snac_debug(user, 1, xs_fmt("collect_replies: cannot get reply items from object '%s'", next));
+    if (xs_is_string(next) && !valid_status(activitypub_request(user, next, &reply_obj))) {
+        snac_debug(user, 1, xs_fmt("collect_replies: error getting next replies object '%s'", next));
         return;
         return;
     }
     }
 
 
-    xs *items = NULL;
+    if (xs_is_dict(reply_obj))
+        level1_replies = xs_dict_get(reply_obj, "items");
+
+    xs *items = xs_list_new();
 
 
     if (xs_is_list(level0_replies))
     if (xs_is_list(level0_replies))
-        items = xs_list_cat(xs_dup(level0_replies), level1_replies);
-    else
-        items = xs_dup(level1_replies);
+        items = xs_list_cat(items, level0_replies);
+
+    if (xs_is_list(level1_replies))
+        items = xs_list_cat(items, level1_replies);
 
 
     const xs_val *v;
     const xs_val *v;
 
 
@@ -1024,6 +1021,107 @@ void collect_replies(snac *user, const char *id)
 }
 }
 
 
 
 
+void collect_outbox(snac *user, const char *actor_id)
+/* gets an actor's outbox and inserts a bunch of posts in a user's timeline */
+{
+    int status;
+    xs *actor = NULL;
+
+    if (!valid_status(status = actor_request(user, actor_id, &actor))) {
+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor object '%s' %d", actor_id, status));
+        return;
+    }
+
+    xs *outbox = NULL;
+    const char *outbox_url = xs_dict_get(actor, "outbox");
+
+    if (!xs_is_string(outbox_url))
+        return;
+
+    if (!valid_status(status = activitypub_request(user, outbox_url, &outbox))) {
+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get actor outbox '%s' %d", outbox_url, status));
+        return;
+    }
+
+    const xs_list *ordered_items = xs_dict_get(outbox, "orderedItems");
+
+    if (!xs_is_list(ordered_items)) {
+        /* the list is not here; does it have a 'first'? */
+        const char *first = xs_dict_get(outbox, "first");
+
+        if (xs_is_string(first)) {
+            /* download this instead */
+            xs *first2 = xs_dup(first);
+            xs_free(outbox);
+
+            if (!valid_status(status = activitypub_request(user, first2, &outbox))) {
+                snac_debug(user, 1, xs_fmt("collect_outbox: cannot get first page of outbox '%s' %d", first2, status));
+                return;
+            }
+
+            /* last chance */
+            ordered_items = xs_dict_get(outbox, "orderedItems");
+        }
+    }
+
+    if (!xs_is_list(ordered_items)) {
+        snac_debug(user, 1, xs_fmt("collect_outbox: cannot get list of posts for actor '%s' outbox", actor_id));
+        return;
+    }
+
+    /* well, ok, then */
+    int max = 4;
+    const xs_val *v;
+
+    xs_list_foreach(ordered_items, v) {
+        if (max == 0)
+            break;
+
+        xs *post = NULL;
+
+        if (xs_is_string(v)) {
+            /* it's probably the post url */
+            if (!valid_status(activitypub_request(user, v, &post)))
+                continue;
+        }
+        else
+        if (xs_is_dict(v))
+            post = xs_dup(v);
+
+        if (post == NULL)
+            continue;
+
+        const char *type = xs_dict_get(post, "type");
+
+        if (!xs_is_string(type) || strcmp(type, "Create")) {
+            /* not a post */
+            continue;
+        }
+
+        const xs_dict *object = xs_dict_get(post, "object");
+
+        if (!xs_is_dict(object))
+            continue;
+
+        type = xs_dict_get(object, "type");
+        const char *id = xs_dict_get(object, "id");
+        const char *attr_to = get_atto(object);
+
+        if (!xs_is_string(type) || !xs_is_string(id) || !xs_is_string(attr_to))
+            continue;
+
+        if (!timeline_here(user, id)) {
+            timeline_add(user, id, object);
+            snac_log(user, xs_fmt("new '%s' (collect_outbox) %s %s", type, attr_to, id));
+        }
+        else
+            snac_debug(user, 1, xs_fmt("collect_outbox: post '%s' already here", id));
+
+        max--;
+    }
+}
+
+
 void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg)
 void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg)
 /* notifies the user of relevant events */
 /* notifies the user of relevant events */
 {
 {
@@ -1267,6 +1365,45 @@ xs_dict *msg_collection(snac *snac, const char *id, int items)
 }
 }
 
 
 
 
+xs_dict *msg_replies(snac *user, const char *id, int fill)
+/* creates a CollectionPage with replies of id */
+{
+    xs *r_id = xs_replace(id, "/p/", "/r/");
+    xs *r_idp = xs_fmt("%s#page", r_id);
+    xs *r_idh = xs_fmt("%s#hdr", r_id);
+
+    xs_dict *msg = msg_base(user, "CollectionPage", r_idp, NULL, NULL, NULL);
+
+    msg = xs_dict_set(msg, "partOf", r_idh);
+
+    xs *items = xs_list_new();
+    if (fill) {
+        xs *children = object_children(id);
+        const char *md5;
+
+        xs_list_foreach(children, md5) {
+            xs *obj = NULL;
+
+            if (valid_status(object_get_by_md5(md5, &obj)) && is_msg_public(obj)) {
+                const char *c_id = xs_dict_get(obj, "id");
+
+                if (xs_is_string(c_id))
+                    items = xs_list_append(items, c_id);
+            }
+        }
+    }
+    else {
+        msg = xs_dict_del(msg, "@context");
+        msg = xs_dict_del(msg, "id");
+        msg = xs_dict_set(msg, "next", r_idp);
+    }
+
+    msg = xs_dict_set(msg, "items", items);
+
+    return msg;
+}
+
+
 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to)
 /* creates an Accept message (as a response to a Follow) */
 /* creates an Accept message (as a response to a Follow) */
 {
 {
@@ -1871,6 +2008,20 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
         }
         }
     }
     }
 
 
+    if (!priv) {
+        /* create the replies object */
+        xs *replies = xs_dict_new();
+        xs *r_id = xs_replace(id, "/p/", "/r/");
+        xs *h_id = xs_fmt("%s#hdr", r_id);
+        xs *rp = msg_replies(snac, id, 0);
+
+        replies = xs_dict_set(replies, "id", h_id);
+        replies = xs_dict_set(replies, "type", "Collection");
+        replies = xs_dict_set(replies, "first", rp);
+
+        msg = xs_dict_set(msg, "replies", replies);
+    }
+
     return msg;
     return msg;
 }
 }
 
 
@@ -2437,6 +2588,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
             if (following_check(snac, actor)) {
             if (following_check(snac, actor)) {
                 following_add(snac, actor, msg);
                 following_add(snac, actor, msg);
                 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
                 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
+
+                /* request a bit of this fellow's outbox */
+                enqueue_collect_outbox(snac, actor);
             }
             }
             else
             else
                 snac_log(snac, xs_fmt("spurious follow accept from %s", actor));
                 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)
                 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
                 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
             else
             else
             if (object_here(id)) {
             if (object_here(id)) {
-                object_add_ow(id, object);
-                timeline_touch(snac);
+                if (xs_startswith(id, srv_baseurl) && !xs_startswith(id, actor))
+                    snac_log(snac, xs_fmt("ignored incorrect 'Update' %s %s", actor, id));
+                else {
+                    object_add_ow(id, object);
+                    timeline_touch(snac);
 
 
-                snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
+                    snac_log(snac, xs_fmt("updated '%s' %s", utype, id));
+                }
             }
             }
             else
             else
                 snac_log(snac, xs_fmt("dropped update for unknown '%s' %s", utype, id));
                 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)
             snac_log(snac, xs_fmt("malformed message: no 'id' field"));
             snac_log(snac, xs_fmt("malformed message: no 'id' field"));
         else
         else
         if (object_here(object)) {
         if (object_here(object)) {
-            timeline_del(snac, object);
-            snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
+            if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor))
+                snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object));
+            else {
+                timeline_del(snac, object);
+                snac_debug(snac, 1, xs_fmt("new 'Delete' %s %s", actor, object));
+            }
         }
         }
         else
         else
             snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
             snac_debug(snac, 1, xs_fmt("ignored 'Delete' for unknown object %s", object));
@@ -2920,6 +3082,12 @@ void process_user_queue_item(snac *user, xs_dict *q_item)
 
 
         collect_replies(user, post);
         collect_replies(user, post);
     }
     }
+    else
+    if (strcmp(type, "collect_outbox") == 0) {
+        const char *actor_id = xs_dict_get(q_item, "message");
+
+        collect_outbox(user, actor_id);
+    }
     else
     else
         snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
         snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
 }
 }
@@ -3390,6 +3558,26 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
         if (valid_status(status) && !is_msg_public(msg))
         if (valid_status(status) && !is_msg_public(msg))
             status = HTTP_STATUS_NOT_FOUND;
             status = HTTP_STATUS_NOT_FOUND;
     }
     }
+    else
+    if (xs_startswith(p_path, "r/")) {
+        /* replies to a post */
+        xs *s = xs_dup(p_path);
+        s[0] = 'p';
+
+        xs *id = xs_fmt("%s/%s", snac.actor, s);
+
+        xs *obj = NULL;
+        status = object_get(id, &obj);
+
+        /* don't return non-public objects */
+        if (!valid_status(status))
+            status = HTTP_STATUS_NOT_FOUND;
+        else
+        if (!is_msg_public(obj))
+            status = HTTP_STATUS_NOT_FOUND;
+        else
+            msg = msg_replies(&snac, id, 1);
+    }
     else
     else
         status = HTTP_STATUS_NOT_FOUND;
         status = HTTP_STATUS_NOT_FOUND;
 
 

+ 16 - 3
data.c

@@ -2429,8 +2429,8 @@ xs_list *list_timeline(snac *user, const char *list, int skip, int show)
 }
 }
 
 
 
 
-xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op)
-/* list content management */
+xs_val *list_members(snac *user, const char *list, const char *actor_md5, int op)
+/* list member management */
 {
 {
     xs_val *l = NULL;
     xs_val *l = NULL;
 
 
@@ -2443,7 +2443,7 @@ xs_val *list_content(snac *user, const char *list, const char *actor_md5, int op
     xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
     xs *fn = xs_fmt("%s/list/%s.lst", user->basedir, list);
 
 
     switch (op) {
     switch (op) {
-    case 0: /** list content **/
+    case 0: /** list members **/
         l = index_list(fn, XS_ALL);
         l = index_list(fn, XS_ALL);
 
 
         break;
         break;
@@ -3589,6 +3589,19 @@ void enqueue_collect_replies(snac *user, const char *post)
 }
 }
 
 
 
 
+void enqueue_collect_outbox(snac *user, const char *actor_id)
+/* enqueues a collect outbox request */
+{
+    xs *qmsg = _new_qmsg("collect_outbox", actor_id, 0);
+    const char *ntid = xs_dict_get(qmsg, "ntid");
+    xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid);
+
+    qmsg = _enqueue_put(fn, qmsg);
+
+    snac_debug(user, 1, xs_fmt("enqueue_collect_outbox %s", actor_id));
+}
+
+
 int was_question_voted(snac *user, const char *id)
 int was_question_voted(snac *user, const char *id)
 /* returns true if the user voted in this poll */
 /* returns true if the user voted in this poll */
 {
 {

+ 2 - 0
doc/snac.1

@@ -282,6 +282,8 @@ necessary information will be prompted for.
 Deletes a user, unfollowing all accounts first.
 Deletes a user, unfollowing all accounts first.
 .It Cm resetpwd Ar basedir Ar uid
 .It Cm resetpwd Ar basedir Ar uid
 Resets a user's password to a new, random one.
 Resets a user's password to a new, random one.
+.It Cm update Ar basedir Ar uid
+Sends a user's updated profile to following instances.
 .It Cm queue Ar basedir Ar uid
 .It Cm queue Ar basedir Ar uid
 Processes the output queue of the specified user, sending all
 Processes the output queue of the specified user, sending all
 enqueued messages and re-enqueing the failing ones. This command
 enqueued messages and re-enqueing the failing ones. This command

+ 9 - 3
main.c

@@ -30,6 +30,7 @@ int usage(const char *cmd)
         "upgrade {basedir}                    Upgrade to a new version\n"
         "upgrade {basedir}                    Upgrade to a new version\n"
         "adduser {basedir} [{uid}]            Adds a new user\n"
         "adduser {basedir} [{uid}]            Adds a new user\n"
         "deluser {basedir} {uid}              Deletes a user\n"
         "deluser {basedir} {uid}              Deletes a user\n"
+        "update {basedir} {uid}               Sends a user's updated profile\n"
         "httpd {basedir}                      Starts the HTTPD daemon\n"
         "httpd {basedir}                      Starts the HTTPD daemon\n"
         "purge {basedir}                      Purges old data\n"
         "purge {basedir}                      Purges old data\n"
         "state {basedir}                      Prints server state\n"
         "state {basedir}                      Prints server state\n"
@@ -354,7 +355,7 @@ int main(int argc, char *argv[])
         xs *lid = list_maint(&snac, url, 4);
         xs *lid = list_maint(&snac, url, 4);
 
 
         if (lid != NULL) {
         if (lid != NULL) {
-            xs *lcont = list_content(&snac, lid, NULL, 0);
+            xs *lcont = list_members(&snac, lid, NULL, 0);
             const char *md5;
             const char *md5;
 
 
             xs_list_foreach(lcont, md5) {
             xs_list_foreach(lcont, md5) {
@@ -410,7 +411,7 @@ int main(int argc, char *argv[])
                 if (valid_status(webfinger_request(account, &actor, &uid))) {
                 if (valid_status(webfinger_request(account, &actor, &uid))) {
                     xs *md5 = xs_md5_hex(actor, strlen(actor));
                     xs *md5 = xs_md5_hex(actor, strlen(actor));
 
 
-                    list_content(&snac, lid, md5, 1);
+                    list_members(&snac, lid, md5, 1);
                     printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid);
                     printf("Actor %s (%s) added to list '%s' (%s)\n", actor, uid, url, lid);
                 }
                 }
                 else
                 else
@@ -433,7 +434,7 @@ int main(int argc, char *argv[])
             if (lid != NULL) {
             if (lid != NULL) {
                 xs *md5 = xs_md5_hex(account, strlen(account));
                 xs *md5 = xs_md5_hex(account, strlen(account));
 
 
-                list_content(&snac, lid, md5, 2);
+                list_members(&snac, lid, md5, 2);
                 printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid);
                 printf("Actor %s deleted from list '%s' (%s)\n", account, url, lid);
             }
             }
             else
             else
@@ -738,6 +739,11 @@ int main(int argc, char *argv[])
         return 0;
         return 0;
     }
     }
 
 
+    if (strcmp(cmd, "collect_outbox") == 0) { /** **/
+        enqueue_collect_outbox(&snac, url);
+        return 0;
+    }
+
     if (strcmp(cmd, "insert") == 0) { /** **/
     if (strcmp(cmd, "insert") == 0) { /** **/
         int status;
         int status;
         xs *data = NULL;
         xs *data = NULL;

+ 4 - 4
mastoapi.c

@@ -1560,7 +1560,7 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid)
         const char *list_id    = xs_list_get(li, 0);
         const char *list_id    = xs_list_get(li, 0);
         const char *list_title = xs_list_get(li, 1);
         const char *list_title = xs_list_get(li, 1);
         if (uid) {
         if (uid) {
-            xs *users = list_content(user, list_id, NULL, 0);
+            xs *users = list_members(user, list_id, NULL, 0);
             if (xs_list_in(users, actor_md5) == -1)
             if (xs_list_in(users, actor_md5) == -1)
                 continue;
                 continue;
         }
         }
@@ -2087,7 +2087,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                     p = xs_list_get(l, -2);
                     p = xs_list_get(l, -2);
 
 
                     if (p && xs_is_hex(p)) {
                     if (p && xs_is_hex(p)) {
-                        xs *actors = list_content(&snac1, p, NULL, 0);
+                        xs *actors = list_members(&snac1, p, NULL, 0);
                         xs *out = xs_list_new();
                         xs *out = xs_list_new();
                         int c = 0;
                         int c = 0;
                         const char *v;
                         const char *v;
@@ -3297,7 +3297,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                     const char *v;
                     const char *v;
 
 
                     while (xs_list_next(accts, &v, &c)) {
                     while (xs_list_next(accts, &v, &c)) {
-                        list_content(&snac, id, v, 1);
+                        list_members(&snac, id, v, 1);
                     }
                     }
 
 
                     xs *out = xs_dict_new();
                     xs *out = xs_dict_new();
@@ -3507,7 +3507,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
                     const char *v;
                     const char *v;
 
 
                     while (xs_list_next(accts, &v, &c)) {
                     while (xs_list_next(accts, &v, &c)) {
-                        list_content(&snac, p, v, 2);
+                        list_members(&snac, p, v, 2);
                     }
                     }
                 }
                 }
                 else {
                 else {

+ 5 - 2
snac.h

@@ -1,7 +1,7 @@
 /* snac - A simple, minimalistic ActivityPub instance */
 /* snac - A simple, minimalistic ActivityPub instance */
 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 
 
-#define VERSION "2.82-dev"
+#define VERSION "2.82"
 
 
 #define USER_AGENT "snac/" VERSION
 #define USER_AGENT "snac/" VERSION
 
 
@@ -233,7 +233,7 @@ xs_list *tag_search(const char *tag, int skip, int show);
 xs_val *list_maint(snac *user, const char *list, int op);
 xs_val *list_maint(snac *user, const char *list, int op);
 xs_str *list_timeline_fn(snac *user, const char *list);
 xs_str *list_timeline_fn(snac *user, const char *list);
 xs_list *list_timeline(snac *user, const char *list, int skip, int show);
 xs_list *list_timeline(snac *user, const char *list, int skip, int show);
-xs_val *list_content(snac *user, const char *list_id, const char *actor_md5, int op);
+xs_val *list_members(snac *user, const char *list_id, const char *actor_md5, int op);
 void list_distribute(snac *user, const char *who, const xs_dict *post);
 void list_distribute(snac *user, const char *who, const xs_dict *post);
 
 
 int actor_add(const char *actor, const xs_dict *msg);
 int 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);
 void enqueue_webmention(const xs_dict *msg);
 void enqueue_webmention(const xs_dict *msg);
 void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries);
 void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries);
 void enqueue_collect_replies(snac *user, const char *post);
 void enqueue_collect_replies(snac *user, const char *post);
+void enqueue_collect_outbox(snac *user, const char *actor_id);
 
 
 int was_question_voted(snac *user, const char *id);
 int was_question_voted(snac *user, const char *id);
 
 
@@ -336,6 +337,7 @@ const char *default_avatar_base64(void);
 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
 
 
 void collect_replies(snac *user, const char *id);
 void collect_replies(snac *user, const char *id);
+void collect_outbox(snac *user, const char *actor_id);
 
 
 const char *get_atto(const xs_dict *msg);
 const char *get_atto(const xs_dict *msg);
 const char *get_in_reply_to(const xs_dict *msg);
 const char *get_in_reply_to(const xs_dict *msg);
@@ -360,6 +362,7 @@ xs_dict *msg_move(snac *user, const char *new_account);
 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
 xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
                       const xs_list *opts, int multiple, int end_secs);
                       const xs_list *opts, int multiple, int end_secs);
+xs_dict *msg_replies(snac *user, const char *id, int fill);
 
 
 int activitypub_request(snac *snac, const char *url, xs_dict **data);
 int activitypub_request(snac *snac, const char *url, xs_dict **data);
 int actor_request(snac *user, const char *actor, xs_dict **data);
 int actor_request(snac *user, const char *actor, xs_dict **data);

+ 9 - 3
utils.c

@@ -229,6 +229,9 @@ int snac_init(const char *basedir)
     xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
     xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
     mkdirx(ibdir);
     mkdirx(ibdir);
 
 
+    xs *langdir = xs_fmt("%s/lang", srv_basedir);
+    mkdirx(langdir);
+
     xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
     xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
     if ((f = fopen(gfn, "w")) == NULL) {
     if ((f = fopen(gfn, "w")) == NULL) {
         printf("ERROR: cannot create '%s'\n", gfn);
         printf("ERROR: cannot create '%s'\n", gfn);
@@ -253,7 +256,10 @@ int snac_init(const char *basedir)
     xs_json_dump(srv_config, 4, f);
     xs_json_dump(srv_config, 4, f);
     fclose(f);
     fclose(f);
 
 
-    printf("Done.\n");
+    printf("Done.\n\n");
+
+    printf("Wanted web UI language files (.po) must be copied manually to %s\n", langdir);
+
     return 0;
     return 0;
 }
 }
 
 
@@ -681,7 +687,7 @@ void export_csv(snac *user)
             const char *lid = xs_list_get(li, 0);
             const char *lid = xs_list_get(li, 0);
             const char *ltitle = xs_list_get(li, 1);
             const char *ltitle = xs_list_get(li, 1);
 
 
-            xs *actors = list_content(user, lid, NULL, 0);
+            xs *actors = list_members(user, lid, NULL, 0);
             const char *md5;
             const char *md5;
 
 
             xs_list_foreach(actors, md5) {
             xs_list_foreach(actors, md5) {
@@ -907,7 +913,7 @@ void import_list_csv(snac *user, const char *ifn)
                     if (valid_status(webfinger_request(acct, &url, &uid))) {
                     if (valid_status(webfinger_request(acct, &url, &uid))) {
                         xs *actor_md5 = xs_md5_hex(url, strlen(url));
                         xs *actor_md5 = xs_md5_hex(url, strlen(url));
 
 
-                        list_content(user, list_id, actor_md5, 1);
+                        list_members(user, list_id, actor_md5, 1);
                         snac_log(user, xs_fmt("Added %s to list %s", url, lname));
                         snac_log(user, xs_fmt("Added %s to list %s", url, lname));
 
 
                         if (!following_check(user, url)) {
                         if (!following_check(user, url)) {