aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordaltux <1medinnz@duck.com>2025-09-04 15:30:59 -0300
committerdaltux <1medinnz@duck.com>2025-09-04 15:30:59 -0300
commit4f5b49bc2476ab6986c3364aa0b88e0cc97040c4 (patch)
treea793c69df7ef6c00fbb170905719214662461897
parent0f1b67611317ab7163a134573b525eb10b415ea8 (diff)
parent6dae96de25ed8d91767aa4cb4e634b71a4b9ae9a (diff)
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 @@
# 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.
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)
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));
@@ -2921,6 +3083,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));
}
@@ -3391,6 +3559,26 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
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;
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)
}
-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 */
{
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.
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
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)
"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;
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)
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 {
diff --git a/snac.h b/snac.h
index 363855f..a871576 100644
--- a/snac.h
+++ b/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);
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)
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)) {