commit f6044d3aa0241a832b0ad1d2c394c0a1b814dbe3
parent 4528029dabb7d79cddefaa5677ae314e16ab51f4
Author: ltning <ltning@noreply.codeberg.org>
Date: Mon, 27 Jan 2025 18:07:00 +0000
Merge branch 'master' into master
Diffstat:
43 files changed, 1020 insertions(+), 198 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2022 - 2024 grunfink et al. (Fediverse: @grunfink@comam.es)
+Copyright (c) 2022 - 2025 grunfink et al. (Fediverse: @grunfink@comam.es)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/Makefile b/Makefile
@@ -42,7 +42,8 @@ data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
xs_time.h xs_match.h snac.h http_codes.h
html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
- xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h snac.h http_codes.h
+ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \
+ http_codes.h
http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
snac.h http_codes.h
httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
diff --git a/Makefile.NetBSD b/Makefile.NetBSD
@@ -5,7 +5,7 @@ LDFLAGS=-lrt
all: snac
-snac: snac.o main.o data.o http.o httpd.o webfinger.o \
+snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \
activitypub.o html.o utils.o format.o upgrade.o mastoapi.o
$(CC) $(CFLAGS) -L/usr/pkg/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -Wl,-rpath,/usr/lib -Wl,-rpath,/usr/pkg/lib -o $@
@@ -44,7 +44,8 @@ data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
xs_time.h xs_match.h snac.h http_codes.h
html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
- xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h snac.h http_codes.h
+ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \
+ http_codes.h
http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
snac.h http_codes.h
httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
@@ -1,6 +1,34 @@
# Release Notes
-## UNRELEASED
+## 2.69 "Yin/Yang of Love"
+
+Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature.
+
+Added support for following hashtags. This is only useful if your instance is subscribed to relays (see above).
+
+Added support for a Mastodon-like `/authorize_interaction` webpoint entry, that allows following, liking and boosting from another account's Mastodon public web interface. To be able to use it, you must reconfigure your https proxy to redirect `/authorize_interaction` to snac (see `snac(8)`).
+
+Some fixes to accept `Event` objects properly (like those coming from implementations like https://gancio.org/ or https://mobilizon.fr).
+
+Added some caching for local `Actor` objects.
+
+Hashtags that are not explicitly linked in a post's content are shown below it.
+
+Fixed broken NetBSD build (missing dependency in Makefile.NetBSD).
+
+The user profile can now include longitude and latitude data for your current location.
+
+Mastodon API: implemented limit= on notification fetches (contributed by nowster), implemented faster min_id handling (contributed by nowster), obey the quiet public visibility set for posts, other timeline improvements (contributed by nowster).
+
+Reduced RSA key size for new users from 4096 to 2048. This will be friendlier to smaller machines, and everybody else out there is using 2048.
+
+If the `SNAC_BASEDIR` environment variable is defined and set to the base directory of your installation, you don't have to include the base directory in the command line.
+
+Fixed a bug in the generation of the top page (contributed by an-im-dugud).
+
+Added support for Markdown headers and underlining (contributed by an-im-dugud).
+
+## 2.68
Fixed regression in link verification code (contributed by nowster).
diff --git a/TODO.md b/TODO.md
@@ -22,7 +22,7 @@ Mastoapi: implement /v1/conversations.
Implement following of hashtags (this is not trivial).
-Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/)
+Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id).
Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
@@ -32,8 +32,6 @@ Integrate "Added handling for International Domain Names" PR https://codeberg.or
Do something about Akkoma and Misskey's quoted replies (they use the `quoteUrl` field instead of `inReplyTo`).
-Add support for /authorize_interaction (whatever it is).
-
Add a list of hashtags to drop.
Take a look at crashes in the brittle Mastodon official app (crashes when hitting the reply button, crashes or 'ownVotes is null' errors when trying to show polls).
@@ -365,3 +363,5 @@ Unfollowing lemmy groups gets rejected with an http status of 400 (it seems to w
CSV import/export does not work with OpenBSD security on; document it or fix it (2025-01-04T19:35:09+0100).
Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
+
+Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
diff --git a/activitypub.c b/activitypub.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_json.h"
@@ -587,6 +587,70 @@ int is_msg_from_private_user(const xs_dict *msg)
}
+int followed_hashtag_check(snac *user, const xs_dict *msg)
+/* returns true if this message contains a hashtag followed by me */
+{
+ const xs_list *fw_tags = xs_dict_get(user->config, "followed_hashtags");
+
+ if (xs_is_list(fw_tags)) {
+ const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
+
+ if (xs_is_list(tags_in_msg)) {
+ const xs_dict *te;
+
+ /* iterate the tags in the message */
+ xs_list_foreach(tags_in_msg, te) {
+ if (xs_is_dict(te)) {
+ const char *type = xs_dict_get(te, "type");
+ const char *name = xs_dict_get(te, "name");
+
+ if (xs_is_string(type) && xs_is_string(name)) {
+ if (strcmp(type, "Hashtag") == 0) {
+ xs *lc_name = xs_utf8_to_lower(name);
+
+ if (xs_list_in(fw_tags, lc_name) != -1)
+ return 1;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+
+void followed_hashtag_distribute(const xs_dict *msg)
+/* distribute this post to all users following the included hashtags */
+{
+ const char *id = xs_dict_get(msg, "id");
+ const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
+
+ if (!xs_is_string(id) || !xs_is_list(tags_in_msg) || xs_list_len(tags_in_msg) == 0)
+ return;
+
+ srv_debug(1, xs_fmt("followed_hashtag_distribute check for %s", id));
+
+ xs *users = user_list();
+ const char *uid;
+
+ xs_list_foreach(users, uid) {
+ snac user;
+
+ if (user_open(&user, uid)) {
+ if (followed_hashtag_check(&user, msg)) {
+ timeline_add(&user, id, msg);
+
+ snac_log(&user, xs_fmt("followed hashtag in %s", id));
+ }
+
+ user_free(&user);
+ }
+ }
+}
+
+
int is_msg_for_me(snac *snac, const xs_dict *c_msg)
/* checks if this message is for me */
{
@@ -602,19 +666,32 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
if (xs_match(type, "Like|Announce|EmojiReact")) {
const char *object = xs_dict_get(c_msg, "object");
- if (xs_type(object) == XSTYPE_DICT)
+ if (xs_is_dict(object))
object = xs_dict_get(object, "id");
/* bad object id? reject */
- if (xs_type(object) != XSTYPE_STRING)
+ if (!xs_is_string(object))
return 0;
/* if it's about one of our posts, accept it */
if (xs_startswith(object, snac->actor))
return 2;
- /* if it's by someone we don't follow, reject */
- return following_check(snac, actor);
+ /* if it's by someone we follow, accept it */
+ if (following_check(snac, actor))
+ return 1;
+
+ /* do we follow any hashtag? */
+ if (xs_is_list(xs_dict_get(snac->config, "followed_hashtags"))) {
+ xs *obj = NULL;
+
+ /* if the admired object contains any followed hashtag, accept it */
+ if (valid_status(object_get(object, &obj)) &&
+ followed_hashtag_check(snac, obj))
+ return 7;
+ }
+
+ return 0;
}
/* if it's an Undo, it must be from someone related to us */
@@ -675,7 +752,7 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
if (pub_msg) {
/* a public message for someone we follow? (probably cc'ed) accept */
- if (following_check(snac, v))
+ if (strcmp(v, public_address) != 0 && following_check(snac, v))
return 5;
}
else
@@ -708,30 +785,8 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
}
/* does this message contain a tag we are following? */
- const xs_list *fw_tags = xs_dict_get(snac->config, "followed_hashtags");
- if (pub_msg && xs_type(fw_tags) == XSTYPE_LIST) {
- const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
- if (xs_type(tags_in_msg) == XSTYPE_LIST) {
- const xs_dict *te;
-
- /* iterate the tags in the message */
- xs_list_foreach(tags_in_msg, te) {
- if (xs_type(te) == XSTYPE_DICT) {
- const char *type = xs_dict_get(te, "type");
- const char *name = xs_dict_get(te, "name");
-
- if (xs_type(type) == XSTYPE_STRING && xs_type(name) == XSTYPE_STRING) {
- if (strcmp(type, "Hashtag") == 0) {
- xs *lc_name = xs_utf8_to_lower(name);
-
- if (xs_list_in(fw_tags, lc_name) != -1)
- return 7;
- }
- }
- }
- }
- }
- }
+ if (pub_msg && followed_hashtag_check(snac, msg))
+ return 7;
return 0;
}
@@ -889,6 +944,11 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
/* if it's not an admiration about something by us, done */
if (xs_is_null(objid) || !xs_startswith(objid, snac->actor))
return;
+
+ /* if it's an announce by our own relay, done */
+ xs *relay_id = xs_fmt("%s/relay", srv_baseurl);
+ if (xs_startswith(id, relay_id))
+ return;
}
/* updated poll? */
@@ -1184,6 +1244,28 @@ xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
}
+xs_dict *msg_actor_place(snac *user, const char *label)
+/* creates a Place object, if the user has a location defined */
+{
+ xs_dict *place = NULL;
+ const char *latitude = xs_dict_get_def(user->config, "latitude", "");
+ const char *longitude = xs_dict_get_def(user->config, "longitude", "");
+
+ if (*latitude && *longitude) {
+ xs *d_la = xs_number_new(atof(latitude));
+ xs *d_lo = xs_number_new(atof(longitude));
+
+ place = msg_base(user, "Place", NULL, user->actor, NULL, NULL);
+
+ place = xs_dict_set(place, "name", label);
+ place = xs_dict_set(place, "latitude", d_la);
+ place = xs_dict_set(place, "longitude", d_lo);
+ }
+
+ return place;
+}
+
+
xs_dict *msg_actor(snac *snac)
/* create a Person message for this actor */
{
@@ -1194,10 +1276,20 @@ xs_dict *msg_actor(snac *snac)
xs *avtr = NULL;
xs *kid = NULL;
xs *f_bio = NULL;
- xs_dict *msg = msg_base(snac, "Person", snac->actor, NULL, NULL, NULL);
+ xs_dict *msg = NULL;
const char *p;
int n;
+ /* everybody loves some caching */
+ if (time(NULL) - object_mtime(snac->actor) < 3 * 3600 &&
+ valid_status(object_get(snac->actor, &msg))) {
+ snac_debug(snac, 2, xs_fmt("Returning cached actor %s", snac->actor));
+
+ return msg;
+ }
+
+ msg = msg_base(snac, "Person", snac->actor, NULL, NULL, NULL);
+
/* change the @context (is this really necessary?) */
ctxt = xs_list_append(ctxt, "https:/" "/www.w3.org/ns/activitystreams");
ctxt = xs_list_append(ctxt, "https:/" "/w3id.org/security/v1");
@@ -1242,6 +1334,10 @@ xs_dict *msg_actor(snac *snac)
if (xs_type(xs_dict_get(snac->config, "bot")) == XSTYPE_TRUE)
msg = xs_dict_set(msg, "type", "Service");
+ /* if it's named "relay", then identify as an "Application" */
+ if (strcmp(snac->uid, "relay") == 0)
+ msg = xs_dict_set(msg, "type", "Application");
+
/* add the header image, if there is one defined */
const char *header = xs_dict_get(snac->config, "header");
if (!xs_is_null(header)) {
@@ -1307,7 +1403,7 @@ xs_dict *msg_actor(snac *snac)
}
/* use shared inboxes? */
- if (xs_type(xs_dict_get(srv_config, "shared_inboxes")) == XSTYPE_TRUE) {
+ if (xs_is_true(xs_dict_get(srv_config, "shared_inboxes")) || strcmp(snac->uid, "relay") == 0) {
xs *d = xs_dict_new();
xs *si = xs_fmt("%s/shared-inbox", srv_baseurl);
d = xs_dict_append(d, "sharedInbox", si);
@@ -1326,6 +1422,15 @@ xs_dict *msg_actor(snac *snac)
msg = xs_dict_set(msg, "manuallyApprovesFollowers",
xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE));
+ /* if there are location coords, create a Place object */
+ xs *location = msg_actor_place(snac, "Home");
+ if (xs_type(location) == XSTYPE_DICT)
+ msg = xs_dict_set(msg, "location", location);
+
+ /* cache it */
+ snac_debug(snac, 1, xs_fmt("Caching actor %s", snac->actor));
+ object_add_ow(snac->actor, msg);
+
return msg;
}
@@ -1423,8 +1528,9 @@ xs_dict *msg_follow(snac *snac, const char *q)
xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
const xs_str *in_reply_to, const xs_list *attach,
- int priv, const char *lang_str)
+ int scope, const char *lang_str)
/* creates a 'Note' message */
+/* scope: 0, public; 1, private (mentioned only); 2, "quiet public"; 3, followers only */
{
xs *ntid = tid(0);
xs *id = xs_fmt("%s/p/%s", snac->actor, ntid);
@@ -1440,6 +1546,9 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
xs_list *p;
const xs_val *v;
+ /* FIXME: implement scope 3 */
+ int priv = scope == 1;
+
if (rcpts == NULL)
to = xs_list_new();
else {
@@ -1557,6 +1666,12 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
}
}
+ if (scope == 2) {
+ /* Mastodon's "quiet public": add public address to cc */
+ if (xs_list_in(cc, public_address) == -1)
+ cc = xs_list_append(cc, public_address);
+ }
+ else
/* no recipients? must be for everybody */
if (!priv && xs_list_len(to) == 0)
to = xs_list_append(to, public_address);
@@ -1845,6 +1960,17 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
/* reject uninteresting messages right now */
if (xs_match(type, "Add|View|Reject|Read|Remove")) {
srv_debug(0, xs_fmt("Ignored message of type '%s'", type));
+
+ /* archive the ignored activity */
+ xs *ntid = tid(0);
+ xs *fn = xs_fmt("%s/ignored/%s.json", srv_basedir, ntid);
+ FILE *f;
+
+ if ((f = fopen(fn, "w")) != NULL) {
+ xs_json_dump(msg, 4, f);
+ fclose(f);
+ }
+
return -1;
}
@@ -2118,14 +2244,14 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
snac_log(snac, xs_fmt("new 'Question' %s %s", actor, id));
}
else
- if (strcmp(utype, "Video") == 0) { /** **/
+ if (xs_match(utype, "Audio|Video|Event")) { /** **/
const char *id = xs_dict_get(object, "id");
if (xs_is_null(id))
snac_log(snac, xs_fmt("malformed message: no 'id' field"));
else
if (timeline_add(snac, id, object))
- snac_log(snac, xs_fmt("new 'Video' %s %s", actor, id));
+ snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id));
}
else
snac_debug(snac, 1, xs_fmt("ignored 'Create' for object type '%s'", utype));
@@ -2203,15 +2329,23 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
xs *who_o = NULL;
if (valid_status(actor_request(snac, who, &who_o))) {
- if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
- snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
- else
- snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
- actor, object));
+ /* don't account as such announces by our own relay */
+ xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
+
+ if (strcmp(actor, this_relay) != 0) {
+ if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
+ snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
+ else
+ snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
+ actor, object));
+ }
/* distribute the post with the actor as 'proxy' */
list_distribute(snac, actor, a_msg);
+ /* distribute the post to users following these hashtags */
+ followed_hashtag_distribute(a_msg);
+
do_notify = 1;
}
else
@@ -2226,14 +2360,14 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
}
else
if (strcmp(type, "Update") == 0) { /** **/
- if (xs_match(utype, "Person|Service")) { /** **/
+ if (xs_match(utype, "Person|Service|Application")) { /** **/
actor_add(actor, xs_dict_get(msg, "object"));
timeline_touch(snac);
snac_log(snac, xs_fmt("updated actor %s", actor));
}
else
- if (xs_match(utype, "Note|Page|Article|Video")) { /** **/
+ if (xs_match(utype, "Note|Page|Article|Video|Audio|Event")) { /** **/
const char *id = xs_dict_get(object, "id");
if (xs_is_null(id))
@@ -2419,7 +2553,7 @@ int send_email(const char *msg)
}
-void process_user_queue_item(snac *snac, xs_dict *q_item)
+void process_user_queue_item(snac *user, xs_dict *q_item)
/* processes an item from the user queue */
{
const char *type;
@@ -2430,7 +2564,7 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
if (strcmp(type, "message") == 0) {
const xs_dict *msg = xs_dict_get(q_item, "message");
- xs *rcpts = recipient_list(snac, msg, 1);
+ xs *rcpts = recipient_list(user, msg, 1);
xs_set inboxes;
const xs_str *actor;
@@ -2439,7 +2573,7 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
/* add this shared inbox first */
xs *this_shared_inbox = xs_fmt("%s/shared-inbox", srv_baseurl);
xs_set_add(&inboxes, this_shared_inbox);
- enqueue_output(snac, msg, this_shared_inbox, 0, 0);
+ enqueue_output(user, msg, this_shared_inbox, 0, 0);
/* iterate the recipients */
xs_list_foreach(rcpts, actor) {
@@ -2450,10 +2584,10 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
if (inbox != NULL) {
/* add to the set and, if it's not there, send message */
if (xs_set_add(&inboxes, inbox) == 1)
- enqueue_output(snac, msg, inbox, 0, 0);
+ enqueue_output(user, msg, inbox, 0, 0);
}
else
- snac_log(snac, xs_fmt("cannot find inbox for %s", actor));
+ snac_log(user, xs_fmt("cannot find inbox for %s", actor));
}
}
@@ -2465,12 +2599,36 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
xs_list_foreach(shibx, inbox) {
if (xs_set_add(&inboxes, inbox) == 1)
- enqueue_output(snac, msg, inbox, 0, 0);
+ enqueue_output(user, msg, inbox, 0, 0);
}
}
}
xs_set_free(&inboxes);
+
+ /* relay this note */
+ if (is_msg_public(msg) && strcmp(user->uid, "relay") != 0) { /* avoid loops */
+ snac relay;
+ if (user_open(&relay, "relay")) {
+ /* a 'relay' user exists */
+ const char *type = xs_dict_get(msg, "type");
+
+ if (xs_is_string(type) && strcmp(type, "Create") == 0) {
+ const xs_val *object = xs_dict_get(msg, "object");
+
+ if (xs_is_dict(object)) {
+ object = xs_dict_get(object, "id");
+
+ snac_debug(&relay, 1, xs_fmt("relaying message %s", object));
+
+ xs *boost = msg_admiration(&relay, object, "Announce");
+ enqueue_message(&relay, boost);
+ }
+ }
+
+ user_free(&relay);
+ }
+ }
}
else
if (strcmp(type, "input") == 0) {
@@ -2482,13 +2640,13 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
if (xs_is_null(msg))
return;
- if (!process_input_message(snac, msg, req)) {
+ if (!process_input_message(user, msg, req)) {
if (retries > queue_retry_max)
- snac_log(snac, xs_fmt("input giving up"));
+ snac_log(user, xs_fmt("input giving up"));
else {
/* reenqueue */
- enqueue_input(snac, msg, req, retries + 1);
- snac_log(snac, xs_fmt("input requeue #%d", retries + 1));
+ enqueue_input(user, msg, req, retries + 1);
+ snac_log(user, xs_fmt("input requeue #%d", retries + 1));
}
}
}
@@ -2498,7 +2656,7 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
const char *id = xs_dict_get(q_item, "message");
if (!xs_is_null(id))
- update_question(snac, id);
+ update_question(user, id);
}
else
if (strcmp(type, "object_request") == 0) {
@@ -2508,17 +2666,17 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
int status;
xs *data = NULL;
- status = activitypub_request(snac, id, &data);
+ status = activitypub_request(user, id, &data);
if (valid_status(status))
object_add_ow(id, data);
- snac_debug(snac, 1, xs_fmt("object_request %s %d", id, status));
+ snac_debug(user, 1, xs_fmt("object_request %s %d", id, status));
}
}
else
if (strcmp(type, "verify_links") == 0) {
- verify_links(snac);
+ verify_links(user);
}
else
if (strcmp(type, "actor_refresh") == 0) {
@@ -2530,16 +2688,16 @@ void process_user_queue_item(snac *snac, xs_dict *q_item)
xs *actor_o = NULL;
int status;
- if (valid_status((status = activitypub_request(snac, actor, &actor_o))))
+ if (valid_status((status = activitypub_request(user, actor, &actor_o))))
actor_add(actor, actor_o);
else
object_touch(actor);
- snac_log(snac, xs_fmt("actor_refresh %s %d", actor, status));
+ snac_log(user, xs_fmt("actor_refresh %s %d", actor, status));
}
}
else
- snac_log(snac, xs_fmt("unexpected user q_item type '%s'", type));
+ snac_log(user, xs_fmt("unexpected user q_item type '%s'", type));
}
@@ -2640,7 +2798,7 @@ void process_queue_item(xs_dict *q_item)
|| status == HTTP_STATUS_UNPROCESSABLE_CONTENT
|| status < 0)
/* explicit error: discard */
- srv_log(xs_fmt("output message: fatal error %s %d", inbox, status));
+ srv_log(xs_fmt("output message: error %s %d", inbox, status));
else
if (retries > queue_retry_max)
srv_log(xs_fmt("output message: giving up %s %d", inbox, status));
@@ -2769,11 +2927,12 @@ void process_queue_item(xs_dict *q_item)
snac user;
if (user_open(&user, v)) {
- if (is_msg_for_me(&user, msg)) {
+ int rsn = is_msg_for_me(&user, msg);
+ if (rsn) {
xs *fn = xs_fmt("%s/queue/%s.json", user.basedir, ntid);
snac_debug(&user, 1,
- xs_fmt("enqueue_input (from shared inbox) %s", xs_dict_get(msg, "id")));
+ xs_fmt("enqueue_input (from shared inbox) %s [%d]", xs_dict_get(msg, "id"), rsn));
if (link(tmpfn, fn) < 0)
srv_log(xs_fmt("link(%s, %s) error", tmpfn, fn));
diff --git a/data.c b/data.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_hex.h"
@@ -319,7 +319,8 @@ int user_persist(snac *snac, int publish)
if (old != NULL) {
int nw = 0;
- const char *fields[] = { "header", "avatar", "name", "bio", "metadata", NULL };
+ const char *fields[] = { "header", "avatar", "name", "bio",
+ "metadata", "latitude", "longitude", NULL };
for (int n = 0; fields[n]; n++) {
const char *of = xs_dict_get(old, fields[n]);
@@ -336,6 +337,10 @@ int user_persist(snac *snac, int publish)
if (!nw)
publish = 0;
+ else {
+ /* uncache the actor object */
+ object_del(snac->actor);
+ }
}
}
}
@@ -674,6 +679,37 @@ int index_desc_first(FILE *f, char md5[MD5_HEX_SIZE], int skip)
return 1;
}
+int index_asc_first(FILE *f,char md5[MD5_HEX_SIZE], const char *seek_md5)
+/* reads the first entry of an ascending index, starting from a given md5 */
+{
+ fseek(f, SEEK_SET, 0);
+ while (fread(md5, MD5_HEX_SIZE, 1, f)) {
+ md5[MD5_HEX_SIZE - 1] = '\0';
+ if (strcmp(md5,seek_md5) == 0) {
+ return index_asc_next(f, md5);
+ }
+ }
+ return 0;
+}
+
+int index_asc_next(FILE *f, char md5[MD5_HEX_SIZE])
+/* reads the next entry of an ascending index */
+{
+ for (;;) {
+ /* read an md5 */
+ if (!fread(md5, MD5_HEX_SIZE, 1, f))
+ return 0;
+
+ /* deleted, skip */
+ if (md5[0] != '-')
+ break;
+ }
+
+ md5[MD5_HEX_SIZE - 1] = '\0';
+
+ return 1;
+}
+
xs_list *index_list_desc(const char *fn, int skip, int show)
/* returns an index as a list, in reverse order */
@@ -1363,11 +1399,13 @@ void timeline_update_indexes(snac *snac, const char *id)
if (valid_status(object_get(id, &msg))) {
/* if its ours and is public, also store in public */
if (is_msg_public(msg)) {
- object_user_cache_add(snac, id, "public");
-
- /* also add it to the instance public timeline */
- xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
- index_add(ipt, id);
+ if (object_user_cache_add(snac, id, "public") >= 0) {
+ /* also add it to the instance public timeline */
+ xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
+ index_add(ipt, id);
+ }
+ else
+ srv_debug(1, xs_fmt("Not added to public instance index %s", id));
}
}
}
@@ -1488,8 +1526,17 @@ xs_list *timeline_instance_list(int skip, int show)
/* returns the timeline for the full instance */
{
xs *idx = instance_index_fn();
+ xs *lst = index_list_desc(idx, skip, show);
- return index_list_desc(idx, skip, show);
+ /* make the list unique */
+ xs_set rep;
+ xs_set_init(&rep);
+ const char *md5;
+
+ xs_list_foreach(lst, md5)
+ xs_set_add(&rep, md5);
+
+ return xs_set_result(&rep);
}
diff --git a/doc/snac.1 b/doc/snac.1
@@ -256,7 +256,7 @@ it's - (a lonely hyphen), the post content will be read from stdin.
The rest of command line arguments are treated as media files to be
attached to the post.
.It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ...
-Like the previous one, but creates an "unlisted" (or "quiet public") one.
+Like the previous one, but creates an "unlisted" (or "quiet public") post.
.It Cm block Ar basedir Ar instance_url
Blocks a full instance, given its URL or domain name. All subsequent
incoming activities with identifiers from that instance will be immediately
@@ -377,6 +377,13 @@ https://$SNAC_HOST/oauth/x-snac-get-token
.Pp
.Sh ENVIRONMENT
.Bl -tag -width Ds
+.It SNAC_BASEDIR
+This optional environment variable can be set to the base directory of
+your installation; if set, you don't have to add the base directory as an
+argument to command-line operations. This may prove useful if you only
+have one
+.Nm
+instance in you system (which is probably your case).
.It Ev DEBUG
Overrides the debugging level from the server 'dbglevel' configuration
variable. Set it to an integer value. The higher, the deeper in meaningless
diff --git a/doc/snac.5 b/doc/snac.5
@@ -24,9 +24,11 @@ A special subset of Markdown is allowed, including:
.It bold
**text between two pairs of asterisks**
.It italic
-*text between a pair of asterisks*
+*text between a pair of asterisks* or _between a pair of underscores_
.It strikethrough text
~~text between a pair of tildes~~
+.It underlined text
+__text between two pairs of underscores__
.It code
Text `between backticks` is formatted as code.
.Bd -literal
@@ -53,6 +55,9 @@ Horizonal rules can be inserted by typing three minus symbols
alone in a line.
.It quoted text
Lines starting with >.
+.It headers
+One, two or three # at the beginning of a line plus a space plus
+some text are converted to HTML headers.
.It user mentions
Strings in the format @user@host are requested using the Webfinger
protocol and converted to links and mentions if something reasonable
diff --git a/doc/snac.8 b/doc/snac.8
@@ -585,6 +585,31 @@ to pass the remote connection address in the
.Ic X-Forwarded-For
HTTP header (unless you use the FastCGI interface; if that's the case, you don't have
to do anything).
+.Pp
+.Ss Subscribing to Fediverse Relays
+Since version 2.69, a
+.Nm
+instance can subscribe to LitePub (Pleroma-style) Fediverse Relays. Doing this improves
+visibility and allows following hashtags. To do this, you must create a special user named
+relay and, from it, follow the relay actor(s) like you do with regular actor URLs. This
+special user will start receiving boosts from the relay server of posts from other instances
+also following it. If any other user of the same
+.Nm
+instance follows any of the hashtags included in these boosted posts coming from the relay,
+they will be received as if they were for them.
+.Pp
+Example:
+.Bd -literal -offset indent
+snac adduser $SNAC_BASEDIR relay # only needed once
+snac follow $SNAC_BASEDIR relay https://relay.example.com/actor
+.Ed
+.Pp
+Users on your instance do NOT need to follow the local relay user to benefit from following
+hashtags.
+.Pp
+Please take note that subscribing to relays can increase the traffic towards your instance
+significantly. In any case, lowering the "Maximum days to keep posts" value for the relay
+special user is recommended (e.g. setting to just 1 day).
.Sh ENVIRONMENT
.Bl -tag -width Ds
.It Ev DEBUG
@@ -685,6 +710,12 @@ location /share {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
}
+# optional (Mastodon-like "authorize interaction" entrypoint)
+location /authorize_interaction {
+ proxy_pass http://localhost:8001;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Forwarded-For $remote_addr;
+}
.Ed
.Pp
Restart the nginx daemon and connect to
@@ -738,6 +769,11 @@ ProxyPreserveHost On
<Location /share>
ProxyPass http://127.0.0.1:8001/share
</Location>
+
+# optional (Mastodon-like "authorize interaction" entrypoint)
+<Location /authorize_interaction>
+ ProxyPass http://127.0.0.1:8001/share
+</Location>
.Ed
.Pp
Since version 2.43,
@@ -797,6 +833,10 @@ location "/.well-known/host-meta" {
location "/share" {
fastcgi socket tcp "127.0.0.1" 8001
}
+
+location "/authorize_interaction" {
+ fastcgi socket tcp "127.0.0.1" 8001
+}
.Ed
.Sh SEE ALSO
.Xr snac 1 ,
diff --git a/format.c b/format.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_regex.h"
@@ -92,6 +92,8 @@ static xs_str *format_line(const char *line, xs_list **attach)
"`[^`]+`" "|"
"~~[^~]+~~" "|"
"\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|"
+ "_[^_]+_" "|" //anzu
+ "__[^_]+__" "|" //anzu
"!\\[[^]]+\\]\\([^\\)]+\\)" "|"
"\\[[^]]+\\]\\([^\\)]+\\)" "|"
"[a-z]+:/" "/[^[:space:]]+" "|"
@@ -127,6 +129,20 @@ static xs_str *format_line(const char *line, xs_list **attach)
xs *s2 = xs_fmt("<i>%s</i>", s1);
s = xs_str_cat(s, s2);
}
+ //anzu - begin
+ else
+ if (xs_startswith(v, "__")) {
+ xs *s1 = xs_strip_chars_i(xs_dup(v), "_");
+ xs *s2 = xs_fmt("<u>%s</u>", s1);
+ s = xs_str_cat(s, s2);
+ }
+ else
+ if (xs_startswith(v, "_")) {
+ xs *s1 = xs_strip_chars_i(xs_dup(v), "_");
+ xs *s2 = xs_fmt("<i>%s</i>", s1);
+ s = xs_str_cat(s, s2);
+ }
+ //anzu - end
else
if (xs_startswith(v, "~~")) {
xs *s1 = xs_strip_chars_i(xs_dup(v), "~");
@@ -303,6 +319,31 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
continue;
}
+ //anzu - begin
+ // h1 reserved for snac?
+ if (xs_startswith(ss, "# ")) {
+ ss = xs_strip_i(xs_crop_i(ss, 2, 0));
+ s = xs_str_cat(s, "<h2>");
+ s = xs_str_cat(s, ss);
+ s = xs_str_cat(s, "</h2>");
+ continue;
+ }
+ if (xs_startswith(ss, "## ")) {
+ ss = xs_strip_i(xs_crop_i(ss, 3, 0));
+ s = xs_str_cat(s, "<h2>");
+ s = xs_str_cat(s, ss);
+ s = xs_str_cat(s, "</h2>");
+ continue;
+ }
+ if (xs_startswith(ss, "### ")) {
+ ss = xs_strip_i(xs_crop_i(ss, 4, 0));
+ s = xs_str_cat(s, "<h3>");
+ s = xs_str_cat(s, ss);
+ s = xs_str_cat(s, "</h3>");
+ continue;
+ }
+ //anzu - end
+
if (xs_startswith(ss, ">")) {
/* delete the > and subsequent spaces */
ss = xs_strip_i(xs_crop_i(ss, 1, 0));
@@ -336,6 +377,8 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
s = xs_replace_i(s, "<br><br><blockquote>", "<br><blockquote>");
s = xs_replace_i(s, "</blockquote><br>", "</blockquote>");
s = xs_replace_i(s, "</pre><br>", "</pre>");
+ s = xs_replace_i(s, "</h2><br>", "</h2>"); //anzu ???
+ s = xs_replace_i(s, "</h3><br>", "</h3>"); //anzu ???
{
/* traditional emoticons */
@@ -378,7 +421,9 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
const char *valid_tags[] = {
"a", "p", "br", "br/", "blockquote", "ul", "ol", "li", "cite", "small",
- "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi", NULL
+ "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi",
+ "h2","h3", //anzu
+ NULL
};
xs_str *sanitize(const char *content)
diff --git a/html.c b/html.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
@@ -12,6 +12,7 @@
#include "xs_match.h"
#include "xs_html.h"
#include "xs_curl.h"
+#include "xs_unicode.h"
#include "snac.h"
@@ -113,7 +114,8 @@ xs_str *actor_name(xs_dict *actor, const char *proxy)
xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
- const char *udate, const char *url, int priv, int in_people, const char *proxy)
+ const char *udate, const char *url, int priv,
+ int in_people, const char *proxy, const char *lang)
{
xs_html *actor_icon = xs_html_tag("p", NULL);
@@ -219,6 +221,9 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
date_title = xs_str_cat(date_title, " / ", udate);
}
+ if (xs_is_string(lang))
+ date_title = xs_str_cat(date_title, " (", lang, ")");
+
xs_html_add(actor_icon,
xs_html_text(" "),
xs_html_tag("time",
@@ -265,6 +270,7 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con
const char *date = NULL;
const char *udate = NULL;
const char *url = NULL;
+ const char *lang = NULL;
int priv = 0;
const char *type = xs_dict_get(msg, "type");
@@ -276,7 +282,17 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con
date = xs_dict_get(msg, "published");
udate = xs_dict_get(msg, "updated");
- actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy);
+ lang = xs_dict_get(msg, "contentMap");
+ if (xs_is_dict(lang)) {
+ const char *v;
+ int c = 0;
+
+ xs_dict_next(lang, &lang, &v, &c);
+ }
+ else
+ lang = NULL;
+
+ actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang);
}
return actor_icon;
@@ -933,6 +949,7 @@ static xs_html *html_user_body(snac *user, int read_only)
xs_html_raw("✔ "),
xs_html_tag("a",
xs_html_attr("rel", "me"),
+ xs_html_attr("target", "_blank"),
xs_html_attr("href", v),
xs_html_text(v)));
}
@@ -969,6 +986,23 @@ static xs_html *html_user_body(snac *user, int read_only)
snac_metadata);
}
+ const char *latitude = xs_dict_get_def(user->config, "latitude", "");
+ const char *longitude = xs_dict_get_def(user->config, "longitude", "");
+
+ if (*latitude && *longitude) {
+ xs *label = xs_fmt(L("%s,%s"), latitude, longitude);
+ xs *url = xs_fmt(L("https://openstreetmap.org/search?query=%s,%s"),
+ latitude, longitude);
+
+ xs_html_add(top_user,
+ xs_html_tag("p",
+ xs_html_text(L("Location: ")),
+ xs_html_tag("a",
+ xs_html_attr("href", url),
+ xs_html_attr("target", "_blank"),
+ xs_html_text(label))));
+ }
+
if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
xs *fwers = follower_list(user);
xs *fwing = following_list(user);
@@ -1110,6 +1144,8 @@ xs_html *html_top_controls(snac *snac)
const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
const xs_val *pending = xs_dict_get(snac->config, "approve_followers");
const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics");
+ const char *latitude = xs_dict_get_def(snac->config, "latitude", "");
+ const char *longitude = xs_dict_get_def(snac->config, "longitude", "");
xs *metadata = NULL;
const xs_dict *md = xs_dict_get(snac->config, "metadata");
@@ -1300,6 +1336,20 @@ xs_html *html_top_controls(snac *snac)
xs_html_attr("for", "show_contact_metrics"),
xs_html_text(L("Publish follower and following metrics")))),
xs_html_tag("p",
+ xs_html_text(L("Current location:")),
+ xs_html_sctag("br", NULL),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "latitude"),
+ xs_html_attr("value", latitude),
+ xs_html_attr("placeholder", "latitude")),
+ xs_html_text(" "),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "longitude"),
+ xs_html_attr("value", longitude),
+ xs_html_attr("placeholder", "longitude"))),
+ xs_html_tag("p",
xs_html_text(L("Profile metadata (key=value pairs in each line):")),
xs_html_sctag("br", NULL),
xs_html_tag("textarea",
@@ -1328,7 +1378,41 @@ xs_html *html_top_controls(snac *snac)
xs_html_sctag("input",
xs_html_attr("type", "submit"),
xs_html_attr("class", "button"),
- xs_html_attr("value", L("Update user info")))))));
+ xs_html_attr("value", L("Update user info"))),
+
+ xs_html_tag("p", NULL)))));
+
+ xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", snac->actor);
+ xs *followed_hashtags = xs_join(xs_dict_get_def(snac->config,
+ "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
+
+ xs_html_add(top_controls,
+ xs_html_tag("details",
+ xs_html_tag("summary",
+ xs_html_text(L("Followed hashtags..."))),
+ xs_html_tag("p",
+ xs_html_text(L("One hashtag per line"))),
+ xs_html_tag("div",
+ xs_html_attr("class", "snac-followed-hashtags"),
+ xs_html_tag("form",
+ xs_html_attr("autocomplete", "off"),
+ xs_html_attr("method", "post"),
+ xs_html_attr("action", followed_hashtags_action),
+ xs_html_attr("enctype", "multipart/form-data"),
+
+ xs_html_tag("textarea",
+ xs_html_attr("name", "followed_hashtags"),
+ xs_html_attr("cols", "40"),
+ xs_html_attr("rows", "4"),
+ xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"),
+ xs_html_text(followed_hashtags)),
+
+ xs_html_tag("br", NULL),
+
+ xs_html_sctag("input",
+ xs_html_attr("type", "submit"),
+ xs_html_attr("class", "button"),
+ xs_html_attr("value", L("Update hashtags")))))));
return top_controls;
}
@@ -1781,13 +1865,15 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
}
}
}
- else
- if (strcmp(type, "Note") == 0) {
- if (level == 0) {
- /* is the parent not here? */
- const char *parent = get_in_reply_to(msg);
- if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
+ if (user && strcmp(type, "Note") == 0) {
+ /* is the parent not here? */
+ const char *parent = get_in_reply_to(msg);
+
+ if (!xs_is_null(parent) && *parent) {
+ xs *md5 = xs_md5_hex(parent, strlen(parent));
+
+ if (!timeline_here(user, md5)) {
xs_html_add(post_header,
xs_html_tag("div",
xs_html_attr("class", "snac-origin"),
@@ -2199,6 +2285,135 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
au_tag);
}
+ /* does it have a location? */
+ const xs_dict *location = xs_dict_get(msg, "location");
+ if (xs_type(location) == XSTYPE_DICT) {
+ const xs_number *latitude = xs_dict_get(location, "latitude");
+ const xs_number *longitude = xs_dict_get(location, "longitude");
+ const char *name = xs_dict_get(location, "name");
+ const char *address = xs_dict_get(location, "address");
+ xs *label_list = xs_list_new();
+
+ if (xs_type(name) == XSTYPE_STRING)
+ label_list = xs_list_append(label_list, name);
+ if (xs_type(address) == XSTYPE_STRING)
+ label_list = xs_list_append(label_list, address);
+
+ if (xs_list_len(label_list)) {
+ const char *url = xs_dict_get(location, "url");
+ xs *label = xs_join(label_list, ", ");
+
+ if (xs_type(url) == XSTYPE_STRING) {
+ xs_html_add(snac_content_wrap,
+ xs_html_tag("p",
+ xs_html_text(L("Location: ")),
+ xs_html_tag("a",
+ xs_html_attr("href", url),
+ xs_html_attr("target", "_blank"),
+ xs_html_text(label))));
+ }
+ else
+ if (!xs_is_null(latitude) && !xs_is_null(longitude)) {
+ xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s",
+ xs_number_str(latitude), xs_number_str(longitude));
+
+ xs_html_add(snac_content_wrap,
+ xs_html_tag("p",
+ xs_html_text(L("Location: ")),
+ xs_html_tag("a",
+ xs_html_attr("href", url),
+ xs_html_attr("target", "_blank"),
+ xs_html_text(label))));
+ }
+ else
+ xs_html_add(snac_content_wrap,
+ xs_html_tag("p",
+ xs_html_text(L("Location: ")),
+ xs_html_text(label)));
+ }
+ }
+
+ if (strcmp(type, "Event") == 0) { /** Event start and end times **/
+ const char *s_time = xs_dict_get(msg, "startTime");
+
+ if (xs_is_string(s_time) && strlen(s_time) > 20) {
+ const char *e_time = xs_dict_get(msg, "endTime");
+ const char *tz = xs_dict_get(msg, "timezone");
+
+ xs *s = xs_replace_i(xs_dup(s_time), "T", " ");
+ xs *e = NULL;
+
+ if (xs_is_string(e_time) && strlen(e_time) > 20)
+ e = xs_replace_i(xs_dup(e_time), "T", " ");
+
+ /* if the event has a timezone, crop the offsets */
+ if (xs_is_string(tz)) {
+ s = xs_crop_i(s, 0, 19);
+
+ if (e)
+ e = xs_crop_i(e, 0, 19);
+ }
+ else
+ tz = "";
+
+ /* if start and end share the same day, crop it from the end */
+ if (e && memcmp(s, e, 11) == 0)
+ e = xs_crop_i(e, 11, 0);
+
+ if (e)
+ s = xs_str_cat(s, " / ", e);
+
+ if (*tz)
+ s = xs_str_cat(s, " (", tz, ")");
+
+ /* replace ugly decimals */
+ s = xs_replace_i(s, ".000", "");
+
+ xs_html_add(snac_content_wrap,
+ xs_html_tag("p",
+ xs_html_text(L("Time: ")),
+ xs_html_text(s)));
+ }
+ }
+
+ /* show all hashtags that has not been shown previously in the content */
+ const xs_list *tags = xs_dict_get(msg, "tag");
+ const char *o_content = xs_dict_get_def(msg, "content", "");
+
+ if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) {
+ xs *content = xs_utf8_to_lower(o_content);
+ const xs_dict *tag;
+
+ xs_html *add_hashtags = xs_html_tag("ul",
+ xs_html_attr("class", "snac-more-hashtags"));
+
+ xs_list_foreach(tags, tag) {
+ const char *type = xs_dict_get(tag, "type");
+
+ if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) {
+ const char *o_href = xs_dict_get(tag, "href");
+ const char *name = xs_dict_get(tag, "name");
+
+ if (xs_is_string(o_href) && xs_is_string(name)) {
+ xs *href = xs_utf8_to_lower(o_href);
+
+ if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) {
+ /* not in the content: add here */
+ xs_html_add(add_hashtags,
+ xs_html_tag("li",
+ xs_html_tag("a",
+ xs_html_attr("href", href),
+ xs_html_text(name),
+ xs_html_text(" "))));
+ }
+ }
+ }
+ }
+
+ xs_html_add(snac_content_wrap,
+ add_hashtags);
+ }
+
/** controls **/
if (!read_only && user) {
@@ -2583,7 +2798,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
xs_html_tag("div",
xs_html_attr("class", "snac-post-header"),
html_actor_icon(snac, actor, xs_dict_get(actor, "published"),
- NULL, NULL, 0, 1, proxy)));
+ NULL, NULL, 0, 1, proxy, NULL)));
/* content (user bio) */
const char *c = xs_dict_get(actor, "summary");
@@ -2762,9 +2977,15 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs_html_attr("class", "snac-posts"));
xs_html_add(body, posts);
- xs_list *p = n_list;
+ xs_set rep;
+ xs_set_init(&rep);
+
+ /* dict to store previous notification labels */
+ xs *admiration_labels = xs_dict_new();
+
const xs_str *v;
- while (xs_list_iter(&p, &v)) {
+
+ xs_list_foreach(n_list, v) {
xs *noti = notify_get(user, v);
if (noti == NULL)
@@ -2775,6 +2996,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
const char *utype = xs_dict_get(noti, "utype");
const char *id = xs_dict_get(noti, "objid");
const char *date = xs_dict_get(noti, "date");
+ const char *id2 = xs_dict_get_path(noti, "msg.id");
xs *wrk = NULL;
if (xs_is_null(id))
@@ -2783,8 +3005,16 @@ xs_str *html_notifications(snac *user, int skip, int show)
if (is_hidden(user, id))
continue;
+ if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
+ continue;
+
object_get(id, &obj);
+ const char *msg_id = NULL;
+
+ if (xs_is_dict(obj))
+ msg_id = xs_dict_get(obj, "id");
+
const char *actor_id = xs_dict_get(noti, "actor");
xs *actor = NULL;
@@ -2817,9 +3047,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
- xs_html *entry = xs_html_tag("div",
- xs_html_attr("class", "snac-post-with-desc"),
- xs_html_tag("p",
+ xs_html *this_html_label = xs_html_container(
xs_html_tag("b",
xs_html_text(label),
xs_html_text(" by "),
@@ -2830,13 +3058,45 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs_html_tag("time",
xs_html_attr("class", "dt-published snac-pubdate"),
xs_html_attr("title", date),
- xs_html_text(s_date))));
+ xs_html_text(s_date)));
+
+ xs_html *html_label = NULL;
+
+ if (xs_is_string(msg_id)) {
+ const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
+
+ if (xs_type(prev_label) == XSTYPE_DATA) {
+ /* there is a previous list of admiration labels! */
+ xs_data_get(&html_label, prev_label);
+
+ xs_html_add(html_label,
+ xs_html_sctag("br", NULL),
+ this_html_label);
+
+ continue;
+ }
+ }
+
+ xs_html *entry = NULL;
+
+ html_label = xs_html_tag("p",
+ this_html_label);
+
+ /* store in the admiration labels dict */
+ xs *pl = xs_data_new(&html_label, sizeof(html_label));
+
+ if (xs_is_string(msg_id))
+ admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
+
+ entry = xs_html_tag("div",
+ xs_html_attr("class", "snac-post-with-desc"),
+ html_label);
if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) {
xs_html_add(entry,
xs_html_tag("div",
xs_html_attr("class", "snac-post"),
- html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy)));
+ html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL)));
}
else
if (strcmp(type, "Move") == 0) {
@@ -2850,7 +3110,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs_html_add(entry,
xs_html_tag("div",
xs_html_attr("class", "snac-post"),
- html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy)));
+ html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL)));
}
}
}
@@ -2917,6 +3177,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
xs_html_text(L("More...")))));
}
+ xs_set_free(&rep);
+
xs_html_add(body,
html_footer());
@@ -2970,6 +3232,21 @@ int html_get_handler(const xs_dict *req, const char *q_path,
else
return HTTP_STATUS_NOT_FOUND;
}
+ else
+ if (strcmp(v, "auth-int-bridge") == 0) {
+ const char *login = xs_dict_get(q_vars, "login");
+ const char *id = xs_dict_get(q_vars, "id");
+ const char *action = xs_dict_get(q_vars, "action");
+
+ if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) {
+ *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s",
+ srv_baseurl, login, action, id);
+
+ return HTTP_STATUS_SEE_OTHER;
+ }
+ else
+ return HTTP_STATUS_NOT_FOUND;
+ }
uid = xs_dup(v);
@@ -3542,6 +3819,52 @@ int html_get_handler(const xs_dict *req, const char *q_path,
}
}
else
+ if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/
+ if (!login(&snac, req)) {
+ *body = xs_dup(uid);
+ status = HTTP_STATUS_UNAUTHORIZED;
+ }
+ else {
+ status = HTTP_STATUS_NOT_FOUND;
+
+ const char *id = xs_dict_get(q_vars, "id");
+ const char *action = xs_dict_get(q_vars, "action");
+
+ if (xs_is_string(id) && xs_is_string(action)) {
+ if (strcmp(action, "Follow") == 0) {
+ xs *msg = msg_follow(&snac, id);
+
+ if (msg != NULL) {
+ const char *actor = xs_dict_get(msg, "object");
+
+ following_add(&snac, actor, msg);
+
+ enqueue_output_by_actor(&snac, msg, actor, 0);
+
+ status = HTTP_STATUS_SEE_OTHER;
+ }
+ }
+ else
+ if (xs_match(action, "Like|Boost|Announce")) {
+ /* bring the post */
+ xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
+
+ if (msg != NULL) {
+ enqueue_message(&snac, msg);
+ timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0);
+
+ status = HTTP_STATUS_SEE_OTHER;
+ }
+ }
+ }
+
+ if (status == HTTP_STATUS_SEE_OTHER) {
+ *body = xs_fmt("%s/admin", snac.actor);
+ *b_size = strlen(*body);
+ }
+ }
+ }
+ else
status = HTTP_STATUS_NOT_FOUND;
user_free(&snac);
@@ -4024,6 +4347,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
else
snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
+ snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
+ snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
+
if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
snac.config = xs_dict_set(snac.config, "metadata", v);
@@ -4140,6 +4466,35 @@ int html_post_handler(const xs_dict *req, const char *q_path,
status = HTTP_STATUS_SEE_OTHER;
}
+ else
+ if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/
+ const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags");
+
+ if (xs_is_string(followed_hashtags)) {
+ xs *new_hashtags = xs_list_new();
+ xs *l = xs_split(followed_hashtags, "\n");
+ const char *v;
+
+ xs_list_foreach(l, v) {
+ xs *s1 = xs_strip_i(xs_dup(v));
+ s1 = xs_replace_i(s1, " ", "");
+
+ if (*s1 == '\0')
+ continue;
+
+ xs *s2 = xs_utf8_to_lower(s1);
+ if (*s2 != '#')
+ s2 = xs_str_prepend_i(s2, "#");
+
+ new_hashtags = xs_list_append(new_hashtags, s2);
+ }
+
+ snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags);
+ user_persist(&snac, 0);
+ }
+
+ status = HTTP_STATUS_SEE_OTHER;
+ }
if (status == HTTP_STATUS_SEE_OTHER) {
const char *redir = xs_dict_get(p_vars, "redir");
diff --git a/http.c b/http.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
diff --git a/httpd.c b/httpd.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
@@ -138,7 +138,7 @@ static xs_str *greeting_html(void)
while (xs_list_iter(&p, &uid)) {
snac user;
- if (user_open(&user, uid)) {
+ if (strcmp(uid, "relay") && user_open(&user, uid)) {
xs_html_add(ul,
xs_html_tag("li",
xs_html_tag("a",
@@ -182,6 +182,29 @@ const char *share_page = ""
"";
+const char *authorize_interaction_page = ""
+"<!DOCTYPE html>\n"
+"<html>\n"
+"<head>\n"
+"<title>%s - snac</title>\n"
+"<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">\n"
+"<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/style.css\"/>\n"
+"<style>:root {color-scheme: light dark}</style>\n"
+"</head>\n"
+"<body><h1>%s authorize interaction</h1>\n"
+"<form method=\"get\" action=\"%s/auth-int-bridge\">\n"
+"<select name=\"action\">\n"
+"<option value=\"Follow\">Follow</option>\n"
+"<option value=\"Boost\">Boost</option>\n"
+"<option value=\"Like\">Like</option>\n"
+"</select> %s\n"
+"<input type=\"hidden\" name=\"id\" value=\"%s\">\n"
+"<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\" required=\"required\"></p>\n"
+"<input type=\"submit\" value=\"OK\">\n"
+"</form><p>%s</p></body></html>\n"
+"";
+
+
int server_get_handler(xs_dict *req, const char *q_path,
char **body, int *b_size, char **ctype)
/* basic server services */
@@ -189,7 +212,7 @@ int server_get_handler(xs_dict *req, const char *q_path,
int status = 0;
/* is it the server root? */
- if (*q_path == '\0') {
+ if (*q_path == '\0' || strcmp(q_path, "/") == 0) {
const xs_dict *q_vars = xs_dict_get(req, "q_vars");
const char *t = NULL;
@@ -318,6 +341,25 @@ int server_get_handler(xs_dict *req, const char *q_path,
USER_AGENT
);
}
+ else
+ if (strcmp(q_path, "/authorize_interaction") == 0) {
+ const xs_dict *q_vars = xs_dict_get(req, "q_vars");
+ const char *uri = xs_dict_get(q_vars, "uri");
+
+ if (xs_is_string(uri)) {
+ status = HTTP_STATUS_OK;
+ *ctype = "text/html; charset=utf-8";
+ *body = xs_fmt(authorize_interaction_page,
+ xs_dict_get(srv_config, "host"),
+ srv_baseurl,
+ xs_dict_get(srv_config, "host"),
+ srv_baseurl,
+ uri,
+ uri,
+ USER_AGENT
+ );
+ }
+ }
if (status != 0)
srv_debug(1, xs_fmt("server_get_handler serving '%s' %d", q_path, status));
@@ -459,13 +501,13 @@ void httpd_connection(FILE *f)
}
if (status == HTTP_STATUS_FORBIDDEN)
- body = xs_str_new("<h1>403 Forbidden</h1>");
+ body = xs_str_new("<h1>403 Forbidden (" USER_AGENT ")</h1>");
if (status == HTTP_STATUS_NOT_FOUND)
- body = xs_str_new("<h1>404 Not Found</h1>");
+ body = xs_str_new("<h1>404 Not Found (" USER_AGENT ")</h1>");
if (status == HTTP_STATUS_BAD_REQUEST && body != NULL)
- body = xs_str_new("<h1>400 Bad Request</h1>");
+ body = xs_str_new("<h1>400 Bad Request (" USER_AGENT ")</h1>");
if (status == HTTP_STATUS_SEE_OTHER)
headers = xs_dict_append(headers, "location", body);
diff --git a/main.c b/main.c
@@ -1,11 +1,12 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
#include "xs_json.h"
#include "xs_time.h"
#include "xs_openssl.h"
+#include "xs_match.h"
#include "snac.h"
@@ -14,7 +15,7 @@
int usage(void)
{
printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n");
- printf("Copyright (c) 2022 - 2024 grunfink et al. / MIT license\n");
+ printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n");
printf("\n");
printf("Commands:\n");
printf("\n");
@@ -34,6 +35,7 @@ int usage(void)
printf("actor {basedir} [{uid}] {url} Requests an actor\n");
printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n");
printf("note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n");
+ printf("note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n");
printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n");
printf("unboost {basedir} {uid} {url} Unboosts a post\n");
printf("resetpwd {basedir} {uid} Resets the password of a user\n");
@@ -49,10 +51,10 @@ int usage(void)
printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
printf("search {basedir} {uid} {regex} Searches posts by content\n");
- printf("export_csv {basedir} {uid} Exports data as CSV files into current directory\n");
+ printf("export_csv {basedir} {uid} Exports data as CSV files\n");
printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n");
printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n");
- printf("import_csv {basedir} {uid} Imports data from CSV files in the current directory\n");
+ printf("import_csv {basedir} {uid} Imports data from CSV files\n");
printf("import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n");
printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n");
@@ -94,19 +96,6 @@ int main(int argc, char *argv[])
return snac_init(basedir);
}
- if (strcmp(cmd, "upgrade") == 0) { /** **/
- int ret;
-
- /* upgrade */
- if ((basedir = GET_ARGV()) == NULL)
- return usage();
-
- if ((ret = srv_open(basedir, 1)) == 1)
- srv_log(xs_dup("OK"));
-
- return ret;
- }
-
if (strcmp(cmd, "markdown") == 0) { /** **/
/* undocumented, for testing only */
xs *c = xs_readall(stdin);
@@ -116,8 +105,20 @@ int main(int argc, char *argv[])
return 0;
}
- if ((basedir = GET_ARGV()) == NULL)
- return usage();
+ if ((basedir = getenv("SNAC_BASEDIR")) == NULL) {
+ if ((basedir = GET_ARGV()) == NULL)
+ return usage();
+ }
+
+ if (strcmp(cmd, "upgrade") == 0) { /** **/
+ int ret;
+
+ /* upgrade */
+ if ((ret = srv_open(basedir, 1)) == 1)
+ srv_log(xs_dup("OK"));
+
+ return ret;
+ }
if (!srv_open(basedir, 0)) {
srv_log(xs_fmt("error opening data storage at %s", basedir));
@@ -351,6 +352,22 @@ int main(int argc, char *argv[])
return 0;
}
+
+ if (strcmp(cmd, "assist") == 0) { /** **/
+ /* undocumented: experimental (do not use) */
+ xs *msg = msg_admiration(&snac, url, "Accept");
+
+ if (msg != NULL) {
+ enqueue_message(&snac, msg);
+
+ if (dbglevel) {
+ xs_json_dump(msg, 4, stdout);
+ }
+ }
+
+ return 0;
+ }
+
if (strcmp(cmd, "unboost") == 0) { /** **/
xs *msg = msg_repulsion(&snac, url, "Announce");
@@ -604,7 +621,9 @@ int main(int argc, char *argv[])
return 0;
}
- if (strcmp(cmd, "note") == 0 || strcmp(cmd, "note_unlisted") == 0) { /** **/
+ if (strcmp(cmd, "note") == 0 || /** **/
+ strcmp(cmd, "note_unlisted") == 0 || /** **/
+ strcmp(cmd, "note_mention") == 0) { /** **/
xs *content = NULL;
xs *msg = NULL;
xs *c_msg = NULL;
@@ -668,15 +687,14 @@ int main(int argc, char *argv[])
else
content = xs_dup(url);
- msg = msg_note(&snac, content, NULL, NULL, attl, 0, getenv("LANG"));
+ int scope = 0;
+ if (strcmp(cmd, "note_mention") == 0)
+ scope = 1;
+ else
+ if (strcmp(cmd, "note_unlisted") == 0)
+ scope = 2;
- if (strcmp(cmd, "note_unlisted") == 0) {
- /* according to Mastodon, "unlisted" posts (now called "quiet public")
- has the public address as a cc instead of to, so toggle it */
- xs *to = xs_dup(xs_dict_get(msg, "to"));
- msg = xs_dict_set(msg, "cc", to);
- msg = xs_dict_set(msg, "to", xs_stock(XSTYPE_LIST));
- }
+ msg = msg_note(&snac, content, NULL, NULL, attl, scope, getenv("LANG"));
c_msg = msg_create(&snac, msg);
diff --git a/mastoapi.c b/mastoapi.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef NO_MASTODON_API
@@ -1339,6 +1339,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
const char *since_id = xs_dict_get(args, "since_id");
const char *min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */
const char *limit_s = xs_dict_get(args, "limit");
+ int (*iterator)(FILE *, char *);
+ int initial_status = 0;
+ int ascending = 0;
int limit = 0;
int cnt = 0;
@@ -1348,27 +1351,40 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
if (limit == 0)
limit = 20;
- if (index_desc_first(f, md5, 0)) {
+ if (min_id) {
+ iterator = &index_asc_next;
+ initial_status = index_asc_first(f, md5, MID_TO_MD5(min_id));
+ ascending = 1;
+ }
+ else {
+ iterator = &index_desc_next;
+ initial_status = index_desc_first(f, md5, 0);
+ }
+
+ if (initial_status) {
do {
xs *msg = NULL;
/* only return entries older that max_id */
if (max_id) {
- if (strcmp(md5, MID_TO_MD5(max_id)) == 0)
+ if (strcmp(md5, MID_TO_MD5(max_id)) == 0) {
max_id = NULL;
-
- continue;
+ if (ascending)
+ break;
+ }
+ if (!ascending)
+ continue;
}
/* only returns entries newer than since_id */
if (since_id) {
- if (strcmp(md5, MID_TO_MD5(since_id)) == 0)
- break;
- }
-
- if (min_id) {
- if (strcmp(md5, MID_TO_MD5(min_id)) == 0)
- break;
+ if (strcmp(md5, MID_TO_MD5(since_id)) == 0) {
+ if (!ascending)
+ break;
+ since_id = NULL;
+ }
+ if (ascending)
+ continue;
}
/* get the entry */
@@ -1428,26 +1444,23 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
continue;
}
- /* if it has a name and it's not a Page or a Video,
+ /* if it has a name and it's not an object that may have one,
it's a poll vote, so discard it */
- if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video"))
+ if (!xs_is_null(xs_dict_get(msg, "name")) && !xs_match(type, "Page|Video|Audio|Event"))
continue;
/* convert the Note into a Mastodon status */
xs *st = mastoapi_status(user, msg);
if (st != NULL) {
- out = xs_list_append(out, st);
+ if (ascending)
+ out = xs_list_insert(out, 0, st);
+ else
+ out = xs_list_append(out, st);
cnt++;
}
- if (min_id) {
- while (cnt > limit) {
- out = xs_list_del(out, 0);
- cnt--;
- }
- }
- } while ((min_id || (cnt < limit)) && index_desc_next(f, md5));
+ } while ((cnt < limit) && (*iterator)(f, md5));
}
int more = index_desc_next(f, md5);
@@ -1816,6 +1829,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
const xs_list *excl = xs_dict_get(args, "exclude_types[]");
const char *min_id = xs_dict_get(args, "min_id");
const char *max_id = xs_dict_get(args, "max_id");
+ const char *limit = xs_dict_get(args, "limit");
+ int limit_count = 0;
+ if (!xs_is_null(limit)) {
+ limit_count = atoi(limit);
+ }
if (dbglevel) {
xs *js = xs_json_dumps(args, 0);
@@ -1903,6 +1921,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
}
out = xs_list_append(out, mn);
+ if (!xs_is_null(limit)) {
+ if (--limit_count <= 0)
+ break;
+ }
}
srv_debug(1, xs_fmt("mastoapi_notifications count %d", xs_list_len(out)));
@@ -2650,8 +2672,14 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
/* prepare the message */
- xs *msg = msg_note(&snac, content, NULL, irt, attach_list,
- strcmp(visibility, "public") == 0 ? 0 : 1, language);
+ int scope = 1;
+ if (strcmp(visibility, "unlisted") == 0)
+ scope = 2;
+ else
+ if (strcmp(visibility, "public") == 0)
+ scope = 0;
+
+ xs *msg = msg_note(&snac, content, NULL, irt, attach_list, scope, language);
if (!xs_is_null(summary) && *summary) {
msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE));
diff --git a/sandbox.c b/sandbox.c
@@ -71,15 +71,22 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smail)
LANDLOCK_ACCESS_FS_REFER_COMPAT,
s = LANDLOCK_ACCESS_FS_MAKE_SOCK,
x = LANDLOCK_ACCESS_FS_EXECUTE;
+ char *resolved_path = NULL;
LL_PATH(basedir, rf|rd|w|c);
LL_PATH("/tmp", rf|rd|w|c);
#ifndef WITHOUT_SHM
LL_PATH("/dev/shm", rf|w|c );
#endif
+ LL_PATH("/dev/urandom", rf );
LL_PATH("/etc/resolv.conf", rf );
LL_PATH("/etc/hosts", rf );
- LL_PATH("/etc/ssl", rf );
+ LL_PATH("/etc/ssl", rf|rd );
+ if ((resolved_path = realpath("/etc/ssl/cert.pem", NULL))) {
+ /* some distros like cert.pem to be a symlink */
+ LL_PATH(resolved_path, rf );
+ free(resolved_path);
+ }
LL_PATH("/usr/share/zoneinfo", rf );
if (mtime("/etc/pki") > 0)
diff --git a/snac.c b/snac.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#define XS_IMPLEMENTATION
diff --git a/snac.h b/snac.h
@@ -1,7 +1,7 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
-#define VERSION "2.68-dev"
+#define VERSION "2.70-dev"
#define USER_AGENT "snac/" VERSION
@@ -108,6 +108,8 @@ int index_len(const char *fn);
xs_list *index_list(const char *fn, int max);
int index_desc_next(FILE *f, char md5[MD5_HEX_SIZE]);
int index_desc_first(FILE *f, char md5[MD5_HEX_SIZE], int skip);
+int index_asc_next(FILE *f, char md5[MD5_HEX_SIZE]);
+int index_asc_first(FILE *f, char md5[MD5_HEX_SIZE], const char *seek_md5);
xs_list *index_list_desc(const char *fn, int skip, int show);
int object_add(const char *id, const xs_dict *obj);
@@ -317,7 +319,7 @@ xs_dict *msg_follow(snac *snac, const char *actor);
xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts,
const xs_str *in_reply_to, const xs_list *attach,
- int priv, const char *lang);
+ int scope, const char *lang);
xs_dict *msg_undo(snac *snac, const xs_val *object);
xs_dict *msg_delete(snac *snac, const char *id);
diff --git a/upgrade.c b/upgrade.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
diff --git a/utils.c b/utils.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
@@ -98,7 +98,7 @@ static const char *greeting_html =
"<html><head>\n"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n"
"<link rel=\"icon\" type=\"image/x-icon\" href=\"https://%host%/favicon.ico\"/>\n"
- "<title>Welcome to %host%</title>\n"
+ "<title>Welcome to %host%</title>\n</head>\n"
"<body style=\"margin: auto; max-width: 50em\">\n"
"%blurb%"
"<p>The following users are part of this community:</p>\n"
@@ -319,6 +319,10 @@ int adduser(const char *uid)
mkdirx(d);
}
+ /* add a specially short data retention time for the relay */
+ if (strcmp(uid, "relay") == 0)
+ config = xs_dict_set(config, "purge_days", xs_stock(1));
+
xs *cfn = xs_fmt("%s/user.json", basedir);
if ((f = fopen(cfn, "w")) == NULL) {
@@ -331,7 +335,7 @@ int adduser(const char *uid)
}
printf("\nCreating RSA key...\n");
- key = xs_evp_genkey(4096);
+ key = xs_evp_genkey(2048);
printf("Done.\n");
xs *kfn = xs_fmt("%s/key.json", basedir);
diff --git a/webfinger.c b/webfinger.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_json.h"
diff --git a/xs.h b/xs.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_H
@@ -157,6 +157,9 @@ unsigned int xs_hash_func(const char *data, int size);
#define xs_is_true(v) (xs_type((v)) == XSTYPE_TRUE)
#define xs_is_false(v) (xs_type((v)) == XSTYPE_FALSE)
#define xs_not(v) xs_stock(xs_is_true((v)) ? XSTYPE_FALSE : XSTYPE_TRUE)
+#define xs_is_string(v) (xs_type((v)) == XSTYPE_STRING)
+#define xs_is_list(v) (xs_type((v)) == XSTYPE_LIST)
+#define xs_is_dict(v) (xs_type((v)) == XSTYPE_DICT)
#define xs_list_foreach(l, v) for (int ct_##__LINE__ = 0; xs_list_next(l, &v, &ct_##__LINE__); )
#define xs_dict_foreach(l, k, v) for (int ct_##__LINE__ = 0; xs_dict_next(l, &k, &v, &ct_##__LINE__); )
@@ -623,15 +626,14 @@ int xs_between(const char *prefix, const char *str, const char *suffix)
xs_str *xs_crop_i(xs_str *str, int start, int end)
/* crops the string to be only from start to end */
{
- XS_ASSERT_TYPE(str, XSTYPE_STRING);
-
int sz = strlen(str);
if (end <= 0)
end = sz + end;
/* crop from the top */
- str[end] = '\0';
+ if (end > 0 && end < sz)
+ str[end] = '\0';
/* crop from the bottom */
str = xs_collapse(str, 0, start);
@@ -1061,14 +1063,15 @@ xs_keyval *xs_keyval_make(xs_keyval *keyval, const xs_str *key, const xs_val *va
typedef struct {
int value_offset; /* offset to value (from dict start) */
- int next; /* next node in sequential search */
+ int next; /* next node in sequential scanning */
int child[4]; /* child nodes in hashed search */
char key[]; /* C string key */
} ditem_hdr;
typedef struct {
int size; /* size of full dict (_XS_TYPE_SIZE) */
- int first; /* first node for sequential search */
+ int first; /* first node for sequential scanning */
+ int last; /* last node for sequential scanning */
int root; /* root node for hashed search */
/* a bunch of ditem_hdr and value follows */
} dict_hdr;
@@ -1153,8 +1156,15 @@ xs_dict *xs_dict_set(xs_dict *dict, const xs_str *key, const xs_val *value)
memcpy(dict + di->value_offset, value, vsz);
/* chain to the sequential list */
- di->next = dh->first;
- dh->first = end;
+ if (dh->first == 0)
+ dh->first = end;
+ else {
+ /* chain this new element to the last one */
+ ditem_hdr *dil = (ditem_hdr *)(dict + dh->last);
+ dil->next = end;
+ }
+
+ dh->last = end;
}
else {
/* ditem already exists */
diff --git a/xs_curl.h b/xs_curl.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_CURL_H
diff --git a/xs_fcgi.h b/xs_fcgi.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
/*
This is an intentionally-dead-simple FastCGI implementation;
diff --git a/xs_glob.h b/xs_glob.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_GLOB_H
diff --git a/xs_hex.h b/xs_hex.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_HEX_H
diff --git a/xs_html.h b/xs_html.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_HTML_H
diff --git a/xs_httpd.h b/xs_httpd.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_HTTPD_H
diff --git a/xs_io.h b/xs_io.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_IO_H
diff --git a/xs_json.h b/xs_json.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_JSON_H
diff --git a/xs_match.h b/xs_match.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_MATCH_H
diff --git a/xs_mime.h b/xs_mime.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_MIME_H
diff --git a/xs_openssl.h b/xs_openssl.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_OPENSSL_H
diff --git a/xs_random.h b/xs_random.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_RANDOM_H
diff --git a/xs_regex.h b/xs_regex.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_REGEX_H
diff --git a/xs_set.h b/xs_set.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_SET_H
diff --git a/xs_socket.h b/xs_socket.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_SOCKET_H
diff --git a/xs_time.h b/xs_time.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_TIME_H
diff --git a/xs_unicode.h b/xs_unicode.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_UNICODE_H
diff --git a/xs_unix_socket.h b/xs_unix_socket.h
@@ -1,4 +1,4 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_UNIX_SOCKET_H
diff --git a/xs_url.h b/xs_url.h
@@ -1,10 +1,11 @@
-/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
#ifndef _XS_URL_H
#define _XS_URL_H
xs_str *xs_url_dec(const char *str);
+xs_str *xs_url_enc(const char *str);
xs_dict *xs_url_vars(const char *str);
xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
@@ -39,6 +40,28 @@ xs_str *xs_url_dec(const char *str)
}
+xs_str *xs_url_enc(const char *str)
+/* URL-encodes a string (RFC 3986) */
+{
+ xs_str *s = xs_str_new(NULL);
+
+ while (*str) {
+ if (isalnum(*str) || strchr("-._~", *str)) {
+ s = xs_append_m(s, str, 1);
+ }
+ else {
+ char tmp[8];
+ snprintf(tmp, sizeof(tmp), "%%%02X", (unsigned char)*str);
+ s = xs_append_m(s, tmp, 3);
+ }
+
+ str++;
+ }
+
+ return s;
+}
+
+
xs_dict *xs_url_vars(const char *str)
/* parse url variables */
{
diff --git a/xs_version.h b/xs_version.h
@@ -1 +1 @@
-/* 297f71e198be7819213e9122e1e78c3b963111bc 2024-11-24T18:48:42+01:00 */
+/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */