Quellcode durchsuchen

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

daltux vor 10 Monaten
Ursprung
Commit
4f5b49bc24
8 geänderte Dateien mit 262 neuen und 40 gelöschten Zeilen
  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
 
-## 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.
 
@@ -16,6 +16,10 @@ Added nodeinfo 2.1 support.
 
 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
 
 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;
     }
 
-    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;
     }
 
-    /* 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;
 
-    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;
     }
 
-    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))
-        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;
 
@@ -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)
 /* 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)
 /* 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;
 }
 
@@ -2437,6 +2588,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
             if (following_check(snac, actor)) {
                 following_add(snac, actor, msg);
                 snac_log(snac, xs_fmt("confirmed follow from %s", actor));
+
+                /* request a bit of this fellow's outbox */
+                enqueue_collect_outbox(snac, actor);
             }
             else
                 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"));
             else
             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
                 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"));
         else
         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
             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);
     }
+    else
+    if (strcmp(type, "collect_outbox") == 0) {
+        const char *actor_id = xs_dict_get(q_item, "message");
+
+        collect_outbox(user, actor_id);
+    }
     else
         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))
             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
         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;
 
@@ -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);
 
     switch (op) {
-    case 0: /** list content **/
+    case 0: /** list members **/
         l = index_list(fn, XS_ALL);
 
         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)
 /* 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.
 .It Cm resetpwd Ar basedir Ar uid
 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
 Processes the output queue of the specified user, sending all
 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"
         "adduser {basedir} [{uid}]            Adds a new 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"
         "purge {basedir}                      Purges old data\n"
         "state {basedir}                      Prints server state\n"
@@ -354,7 +355,7 @@ int main(int argc, char *argv[])
         xs *lid = list_maint(&snac, url, 4);
 
         if (lid != NULL) {
-            xs *lcont = list_content(&snac, lid, NULL, 0);
+            xs *lcont = list_members(&snac, lid, NULL, 0);
             const char *md5;
 
             xs_list_foreach(lcont, md5) {
@@ -410,7 +411,7 @@ int main(int argc, char *argv[])
                 if (valid_status(webfinger_request(account, &actor, &uid))) {
                     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);
                 }
                 else
@@ -433,7 +434,7 @@ int main(int argc, char *argv[])
             if (lid != NULL) {
                 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);
             }
             else
@@ -738,6 +739,11 @@ int main(int argc, char *argv[])
         return 0;
     }
 
+    if (strcmp(cmd, "collect_outbox") == 0) { /** **/
+        enqueue_collect_outbox(&snac, url);
+        return 0;
+    }
+
     if (strcmp(cmd, "insert") == 0) { /** **/
         int status;
         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_title = xs_list_get(li, 1);
         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)
                 continue;
         }
@@ -2087,7 +2087,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                     p = xs_list_get(l, -2);
 
                     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();
                         int c = 0;
                         const char *v;
@@ -3297,7 +3297,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                     const char *v;
 
                     while (xs_list_next(accts, &v, &c)) {
-                        list_content(&snac, id, v, 1);
+                        list_members(&snac, id, v, 1);
                     }
 
                     xs *out = xs_dict_new();
@@ -3507,7 +3507,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path,
                     const char *v;
 
                     while (xs_list_next(accts, &v, &c)) {
-                        list_content(&snac, p, v, 2);
+                        list_members(&snac, p, v, 2);
                     }
                 }
                 else {

+ 5 - 2
snac.h

@@ -1,7 +1,7 @@
 /* snac - A simple, minimalistic ActivityPub instance */
 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
 
-#define VERSION "2.82-dev"
+#define VERSION "2.82"
 
 #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_str *list_timeline_fn(snac *user, const char *list);
 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);
 
 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_notify_webhook(snac *user, const xs_dict *noti, int retries);
 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);
 
@@ -336,6 +337,7 @@ const char *default_avatar_base64(void);
 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
 
 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_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_question(snac *user, const char *content, xs_list *attach,
                       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 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);
     mkdirx(ibdir);
 
+    xs *langdir = xs_fmt("%s/lang", srv_basedir);
+    mkdirx(langdir);
+
     xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
     if ((f = fopen(gfn, "w")) == NULL) {
         printf("ERROR: cannot create '%s'\n", gfn);
@@ -253,7 +256,10 @@ int snac_init(const char *basedir)
     xs_json_dump(srv_config, 4, 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;
 }
 
@@ -681,7 +687,7 @@ void export_csv(snac *user)
             const char *lid = xs_list_get(li, 0);
             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;
 
             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))) {
                         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));
 
                         if (!following_check(user, url)) {