snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

commit 114ed37f9c3e57a840155b9b71fa9cdf0d7ec8d6
parent f6044d3aa0241a832b0ad1d2c394c0a1b814dbe3
Author: ltning <ltning@noreply.codeberg.org>
Date:   Tue,  4 Feb 2025 18:38:41 +0000

Merge branch 'master' into master

Diffstat:
MREADME.md | 1+
MRELEASE_NOTES.md | 16++++++++++++++++
MTODO.md | 8++++----
Mactivitypub.c | 2+-
Mdata.c | 45+++++++++++++++++++++++++++++++++++++++------
Mdoc/snac.1 | 5+++++
Mdoc/snac.5 | 2+-
Mdoc/style.css | 1+
Mhtml.c | 50++++++++++++++++++++++++++++++++++++--------------
Mmain.c | 13+++++++++++++
Mmastoapi.c | 2+-
Msnac.h | 10+++++++---
Mutils.c | 1+
Mxs.h | 36++++++++++++++++++++++++++++--------
Mxs_io.h | 3++-
Mxs_match.h | 7++++++-
Mxs_openssl.h | 2+-
Mxs_socket.h | 2++
Mxs_url.h | 8+++++++-
Mxs_version.h | 2+-
20 files changed, 173 insertions(+), 43 deletions(-)

diff --git a/README.md b/README.md @@ -107,6 +107,7 @@ This will: - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). - [How to run your own social network with snac (by Giacomo Tesio)](https://encrypted.tesio.it/2024/12/18/how-to-run-your-own-social-network.html). Includes information on how to run snac as a CGI. +- [Improving snac Performance with Nginx Proxy Cache (by Stefano Marinelli)](https://it-notes.dragas.net/2025/01/29/improving-snac-performance-with-nginx-proxy-cache/). ## Incredibly awesome CSS themes for snac diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md @@ -1,5 +1,21 @@ # Release Notes +## 2.71 + +Fixed memory leak (contributed by inz). + +Fixed crash. + +## 2.70 + +Notifications are now shown in a more compact way (i.e. all reactions are shown just above your post, instead of repeating the post *ad nauseam* for every reaction). + +New command-line option `unmute` to, well, no-longer-mute an actor. + +The private timeline now includes an approximate mark between new posts and "already seen" ones. + +Fixed a spurious 404 error in the instance root URL for some configurations. + ## 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. diff --git a/TODO.md b/TODO.md @@ -14,14 +14,10 @@ Important: deleting a follower should do more that just delete the object, see h ## Wishlist -Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information). - The instance timeline should also show boosts from users. 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/). 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 @@ -365,3 +361,7 @@ CSV import/export does not work with OpenBSD security on; document it or fix it 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). + +Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100). + +Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100). diff --git a/activitypub.c b/activitypub.c @@ -3081,7 +3081,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); /* get the public outbox or the pinned list */ - xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac); + xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac); xs_list_foreach(elems, v) { xs *i = NULL; diff --git a/data.c b/data.c @@ -1489,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name) } -xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show) +xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more) /* returns a timeline (with all entries) */ { xs *idx = user_index_fn(user, idx_name); - return index_list_desc(idx, skip, show); + /* if a more flag is sent, request one more */ + xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0)); + + if (more != NULL) { + if (xs_list_len(lst) > show) { + *more = 1; + lst = xs_list_del(lst, -1); + } + else + *more = 0; + } + + return lst; } -xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) +xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more) /* returns a timeline (only top level entries) */ { int c_max; @@ -1510,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) if (show > c_max) show = c_max; - xs *list = timeline_simple_list(snac, idx_name, skip, show); + xs *list = timeline_simple_list(snac, idx_name, skip, show, more); return timeline_top_level(snac, list); } +void timeline_add_mark(snac *user) +/* adds an "already seen" mark to the private timeline */ +{ + xs *fn = xs_fmt("%s/private.idx", user->basedir); + char last_entry[MD5_HEX_SIZE] = ""; + FILE *f; + + /* get the last entry in the index */ + if ((f = fopen(fn, "r")) != NULL) { + index_desc_first(f, last_entry, 0); + fclose(f); + } + + /* is the last entry *not* a mark? */ + if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) { + /* add it */ + index_add_md5(fn, MD5_ALREADY_SEEN_MARK); + } +} + + xs_str *instance_index_fn(void) { return xs_fmt("%s/public.idx", srv_basedir); @@ -2709,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex, const char *md5s[3] = {0}; int c[3] = {0}; - tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */ + tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */ tls[1] = timeline_instance_list(0, XS_ALL); /* instance */ - tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */ + tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */ /* first positioning */ for (int n = 0; n < 3; n++) diff --git a/doc/snac.1 b/doc/snac.1 @@ -234,6 +234,8 @@ Purges old data from the timeline of all users. .It Cm adduser Ar basedir Op uid Adds a new user to the server. This is an interactive command; necessary information will be prompted for. +.It Cm deluser Ar basedir Ar uid +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 queue Ar basedir Ar uid @@ -257,6 +259,9 @@ 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") post. +.It Cm note_mention Ar basedir Ar uid Ar text Op file file ... +Like the previous one, but creates a post only for accounts mentioned +in the post body. .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 diff --git a/doc/snac.5 b/doc/snac.5 @@ -78,7 +78,7 @@ converted to related emojis: .Ss Accepted HTML All HTML tags in entries are neutered except the following ones: .Bd -literal -a p br blockquote ul ol li cite small +a p br blockquote ul ol li cite small h2 h3 span i b u s pre code em strong hr img del .Ed .Pp diff --git a/doc/style.css b/doc/style.css @@ -29,6 +29,7 @@ pre { overflow-x: scroll; } .snac-list-of-lists { padding-left: 0; } .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px; margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; } +.snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; } @media (prefers-color-scheme: dark) { body, input, textarea { background-color: #000; color: #fff; } a { color: #7799dd } diff --git a/html.c b/html.c @@ -2658,10 +2658,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html_add(body, posts); + int mark_shown = 0; + while (xs_list_iter(&p, &v)) { xs *msg = NULL; int status; + /* "already seen" mark? */ + if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) { + if (skip == 0 && !mark_shown) { + xs *s = xs_fmt("%s/admin", user->actor); + + xs_html_add(posts, + xs_html_tag("div", + xs_html_attr("class", "snac-no-more-unseen-posts"), + xs_html_text(L("No more unseen posts")), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", s), + xs_html_text(L("Back to top"))))); + } + + mark_shown = 1; + + continue; + } + if (utl && user && !is_pinned_by_md5(user, v)) status = timeline_get_by_md5(user, v, &msg); else @@ -3324,21 +3346,17 @@ int html_get_handler(const xs_dict *req, const char *q_path, } else { xs *list = NULL; - xs *next = NULL; + int more = 0; - if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) { - list = timeline_simple_list(&snac, "public", skip, show); - next = timeline_simple_list(&snac, "public", skip + show, 1); - } - else { - list = timeline_list(&snac, "public", skip, show); - next = timeline_list(&snac, "public", skip + show, 1); - } + if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) + list = timeline_simple_list(&snac, "public", skip, show, &more); + else + list = timeline_list(&snac, "public", skip, show, &more); xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); - *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error); + *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -3487,6 +3505,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else { + /** the private timeline **/ double t = history_mtime(&snac, "timeline.html_"); /* if enabled by admin, return a cached page if its timestamp is: @@ -3500,19 +3519,22 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs_dict_get(req, "if-none-match"), etag); } else { + int more = 0; + snac_debug(&snac, 1, xs_fmt("building timeline")); - xs *list = timeline_list(&snac, "private", skip, show); - xs *next = timeline_list(&snac, "private", skip + show, 1); + xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - xs_list_len(next), NULL, "/admin", 1, error); + more, NULL, "/admin", 1, error); *b_size = strlen(*body); status = HTTP_STATUS_OK; if (save) history_add(&snac, "timeline.html_", *body, *b_size, etag); + + timeline_add_mark(&snac); } } } @@ -3712,7 +3734,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); - xs *elems = timeline_simple_list(&snac, "public", 0, cnt); + xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL); xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); xs *rss_title = xs_fmt("%s (@%s@%s)", diff --git a/main.c b/main.c @@ -49,6 +49,7 @@ int usage(void) printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); + printf("unmute {basedir} {uid} {actor} Unmutes a previously muted 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\n"); @@ -446,6 +447,18 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "unmute") == 0) { /** **/ + if (is_muted(&snac, url)) { + unmute(&snac, url); + + printf("%s unmuted\n", url); + } + else + printf("%s actor is not muted\n", url); + + return 0; + } + if (strcmp(cmd, "search") == 0) { /** **/ int to; diff --git a/mastoapi.c b/mastoapi.c @@ -1676,7 +1676,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, else if (strcmp(opt, "statuses") == 0) { /** **/ /* the public list of posts of a user */ - xs *timeline = timeline_simple_list(&snac2, "public", 0, 256); + xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL); xs_list *p = timeline; const xs_str *v; diff --git 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.70-dev" +#define VERSION "2.72-dev" #define USER_AGENT "snac/" VERSION @@ -22,6 +22,8 @@ #define MD5_HEX_SIZE 33 +#define MD5_ALREADY_SEEN_MARK "00000000000000000000000000000000" + extern double disk_layout; extern xs_str *srv_basedir; extern xs_dict *srv_config; @@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5); int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); int timeline_del(snac *snac, const char *id); xs_str *user_index_fn(snac *user, const char *idx_name); -xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show); -xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); +xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more); +xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more); int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); int timeline_admire(snac *snac, const char *id, const char *admirer, int like); xs_list *timeline_top_level(snac *snac, const xs_list *list); +void timeline_add_mark(snac *user); + xs_list *local_list(snac *snac, int max); xs_str *instance_index_fn(void); xs_list *timeline_instance_list(int skip, int show); diff --git a/utils.c b/utils.c @@ -73,6 +73,7 @@ static const char *default_css = ".snac-list-of-lists { padding-left: 0; }\n" ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n" " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n" + ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n" "@media (prefers-color-scheme: dark) { \n" " body, input, textarea { background-color: #000; color: #fff; }\n" " a { color: #7799dd }\n" diff --git a/xs.h b/xs.h @@ -12,6 +12,7 @@ #include <stdarg.h> #include <signal.h> #include <errno.h> +#include <stdint.h> typedef enum { XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ @@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value); void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); unsigned int xs_hash_func(const char *data, int size); +uint64_t xs_hash64_func(const char *data, int size); #ifdef XS_ASSERT #include <assert.h> @@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end) end = sz + end; /* crop from the top */ - if (end > 0 && end < sz) + if (end >= 0 && end < sz) str[end] = '\0'; /* crop from the bottom */ @@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep) xs_list *xs_split_n(const char *str, const char *sep, int times) /* splits a string into a list upto n times */ { + xs_list *list = xs_list_new(); + + if (!xs_is_string(str) || !xs_is_string(sep)) + return list; + int sz = strlen(sep); char *ss; - xs_list *list; - - list = xs_list_new(); while (times > 0 && (ss = strstr(str, sep)) != NULL) { /* create a new string with this slice and add it to the list */ xs *s = xs_str_new_sz(str, ss - str); - list = xs_list_append(list, s); + + if (xs_is_string(s)) + list = xs_list_append(list, s); /* skip past the separator */ str = ss + sz; @@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times) } /* add the rest of the string */ - list = xs_list_append(list, str); + if (xs_is_string(str)) + list = xs_list_append(list, str); return list; } @@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size) /* a general purpose hashing function */ { unsigned int hash = 0x666; - int n; - for (n = 0; n < size; n++) { + for (int n = 0; n < size; n++) { hash ^= (unsigned char)data[n]; hash *= 111111111; } @@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size) } +uint64_t xs_hash64_func(const char *data, int size) +/* a general purpose hashing function (64 bit) */ +{ + uint64_t hash = 0x100; + + for (int n = 0; n < size; n++) { + hash ^= (unsigned char)data[n]; + hash *= 1111111111111111111; + } + + return hash; +} + + #endif /* XS_IMPLEMENTATION */ #endif /* _XS_H */ diff --git a/xs_io.h b/xs_io.h @@ -27,7 +27,8 @@ xs_str *xs_readline(FILE *f) while ((c = fgetc(f)) != EOF) { unsigned char rc = c; - s = xs_append_m(s, (char *)&rc, 1); + if (xs_is_string((char *)&rc)) + s = xs_append_m(s, (char *)&rc, 1); if (c == '\n') break; diff --git a/xs_match.h b/xs_match.h @@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec) retry: for (;;) { + const char *q = spec; char c = *str++; char p = *spec++; @@ -63,8 +64,12 @@ retry: spec = b_spec; str = ++b_str; } - else + else { + if (*q == '|') + spec = q; + break; + } } } } diff --git a/xs_openssl.h b/xs_openssl.h @@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size) s = xs_realloc(s, _xs_blk_size(*size + 1)); s[*size] = '\0'; - BIO_free_all(mem); + BIO_free_all(b64); return s; } diff --git a/xs_socket.h b/xs_socket.h @@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv) listen(rs, SOMAXCONN); } + freeaddrinfo(res); + #else /* WITHOUT_GETADDRINFO */ struct sockaddr_in host; diff --git a/xs_url.h b/xs_url.h @@ -17,12 +17,18 @@ xs_str *xs_url_dec(const char *str) xs_str *s = xs_str_new(NULL); while (*str) { + if (!xs_is_string(str)) + break; + if (*str == '%') { unsigned int i; if (sscanf(str + 1, "%02x", &i) == 1) { unsigned char uc = i; + if (!xs_is_string((char *)&uc)) + break; + s = xs_append_m(s, (char *)&uc, 1); str += 2; } @@ -69,7 +75,7 @@ xs_dict *xs_url_vars(const char *str) vars = xs_dict_new(); - if (str != NULL) { + if (xs_is_string(str)) { /* split by arguments */ xs *args = xs_split(str, "&"); diff --git a/xs_version.h b/xs_version.h @@ -1 +1 @@ -/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */ +/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */