commit 114ed37
Merge branch 'master' into master
20 files changed,
+173,
-43
+1,
-0
1@@ -107,6 +107,7 @@ This will:
2 - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html).
3 - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac).
4 - [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.
5+- [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/).
6
7 ## Incredibly awesome CSS themes for snac
8
+16,
-0
1@@ -1,5 +1,21 @@
2 # Release Notes
3
4+## 2.71
5+
6+Fixed memory leak (contributed by inz).
7+
8+Fixed crash.
9+
10+## 2.70
11+
12+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).
13+
14+New command-line option `unmute` to, well, no-longer-mute an actor.
15+
16+The private timeline now includes an approximate mark between new posts and "already seen" ones.
17+
18+Fixed a spurious 404 error in the instance root URL for some configurations.
19+
20 ## 2.69 "Yin/Yang of Love"
21
22 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.
M
TODO.md
+4,
-4
1@@ -14,14 +14,10 @@ Important: deleting a follower should do more that just delete the object, see h
2
3 ## Wishlist
4
5-Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information).
6-
7 The instance timeline should also show boosts from users.
8
9 Mastoapi: implement /v1/conversations.
10
11-Implement following of hashtags (this is not trivial).
12-
13 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).
14
15 Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
16@@ -365,3 +361,7 @@ CSV import/export does not work with OpenBSD security on; document it or fix it
17 Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
18
19 Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
20+
21+Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
22+
23+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).
+1,
-1
1@@ -3081,7 +3081,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
2 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
3
4 /* get the public outbox or the pinned list */
5- xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac);
6+ xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac);
7
8 xs_list_foreach(elems, v) {
9 xs *i = NULL;
M
data.c
+39,
-6
1@@ -1489,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name)
2 }
3
4
5-xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show)
6+xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more)
7 /* returns a timeline (with all entries) */
8 {
9 xs *idx = user_index_fn(user, idx_name);
10
11- return index_list_desc(idx, skip, show);
12+ /* if a more flag is sent, request one more */
13+ xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0));
14+
15+ if (more != NULL) {
16+ if (xs_list_len(lst) > show) {
17+ *more = 1;
18+ lst = xs_list_del(lst, -1);
19+ }
20+ else
21+ *more = 0;
22+ }
23+
24+ return lst;
25 }
26
27
28-xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
29+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more)
30 /* returns a timeline (only top level entries) */
31 {
32 int c_max;
33@@ -1510,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
34 if (show > c_max)
35 show = c_max;
36
37- xs *list = timeline_simple_list(snac, idx_name, skip, show);
38+ xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
39
40 return timeline_top_level(snac, list);
41 }
42
43
44+void timeline_add_mark(snac *user)
45+/* adds an "already seen" mark to the private timeline */
46+{
47+ xs *fn = xs_fmt("%s/private.idx", user->basedir);
48+ char last_entry[MD5_HEX_SIZE] = "";
49+ FILE *f;
50+
51+ /* get the last entry in the index */
52+ if ((f = fopen(fn, "r")) != NULL) {
53+ index_desc_first(f, last_entry, 0);
54+ fclose(f);
55+ }
56+
57+ /* is the last entry *not* a mark? */
58+ if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) {
59+ /* add it */
60+ index_add_md5(fn, MD5_ALREADY_SEEN_MARK);
61+ }
62+}
63+
64+
65 xs_str *instance_index_fn(void)
66 {
67 return xs_fmt("%s/public.idx", srv_basedir);
68@@ -2709,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex,
69 const char *md5s[3] = {0};
70 int c[3] = {0};
71
72- tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */
73+ tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */
74 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
75- tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */
76+ tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */
77
78 /* first positioning */
79 for (int n = 0; n < 3; n++)
+5,
-0
1@@ -234,6 +234,8 @@ Purges old data from the timeline of all users.
2 .It Cm adduser Ar basedir Op uid
3 Adds a new user to the server. This is an interactive command;
4 necessary information will be prompted for.
5+.It Cm deluser Ar basedir Ar uid
6+Deletes a user, unfollowing all accounts first.
7 .It Cm resetpwd Ar basedir Ar uid
8 Resets a user's password to a new, random one.
9 .It Cm queue Ar basedir Ar uid
10@@ -257,6 +259,9 @@ The rest of command line arguments are treated as media files to be
11 attached to the post.
12 .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ...
13 Like the previous one, but creates an "unlisted" (or "quiet public") post.
14+.It Cm note_mention Ar basedir Ar uid Ar text Op file file ...
15+Like the previous one, but creates a post only for accounts mentioned
16+in the post body.
17 .It Cm block Ar basedir Ar instance_url
18 Blocks a full instance, given its URL or domain name. All subsequent
19 incoming activities with identifiers from that instance will be immediately
+1,
-1
1@@ -78,7 +78,7 @@ converted to related emojis:
2 .Ss Accepted HTML
3 All HTML tags in entries are neutered except the following ones:
4 .Bd -literal
5-a p br blockquote ul ol li cite small
6+a p br blockquote ul ol li cite small h2 h3
7 span i b u s pre code em strong hr img del
8 .Ed
9 .Pp
+1,
-0
1@@ -29,6 +29,7 @@ pre { overflow-x: scroll; }
2 .snac-list-of-lists { padding-left: 0; }
3 .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;
4 margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }
5+.snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }
6 @media (prefers-color-scheme: dark) {
7 body, input, textarea { background-color: #000; color: #fff; }
8 a { color: #7799dd }
M
html.c
+36,
-14
1@@ -2658,10 +2658,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2 xs_html_add(body,
3 posts);
4
5+ int mark_shown = 0;
6+
7 while (xs_list_iter(&p, &v)) {
8 xs *msg = NULL;
9 int status;
10
11+ /* "already seen" mark? */
12+ if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
13+ if (skip == 0 && !mark_shown) {
14+ xs *s = xs_fmt("%s/admin", user->actor);
15+
16+ xs_html_add(posts,
17+ xs_html_tag("div",
18+ xs_html_attr("class", "snac-no-more-unseen-posts"),
19+ xs_html_text(L("No more unseen posts")),
20+ xs_html_text(" - "),
21+ xs_html_tag("a",
22+ xs_html_attr("href", s),
23+ xs_html_text(L("Back to top")))));
24+ }
25+
26+ mark_shown = 1;
27+
28+ continue;
29+ }
30+
31 if (utl && user && !is_pinned_by_md5(user, v))
32 status = timeline_get_by_md5(user, v, &msg);
33 else
34@@ -3324,21 +3346,17 @@ int html_get_handler(const xs_dict *req, const char *q_path,
35 }
36 else {
37 xs *list = NULL;
38- xs *next = NULL;
39+ int more = 0;
40
41- if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) {
42- list = timeline_simple_list(&snac, "public", skip, show);
43- next = timeline_simple_list(&snac, "public", skip + show, 1);
44- }
45- else {
46- list = timeline_list(&snac, "public", skip, show);
47- next = timeline_list(&snac, "public", skip + show, 1);
48- }
49+ if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
50+ list = timeline_simple_list(&snac, "public", skip, show, &more);
51+ else
52+ list = timeline_list(&snac, "public", skip, show, &more);
53
54 xs *pins = pinned_list(&snac);
55 pins = xs_list_cat(pins, list);
56
57- *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error);
58+ *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error);
59
60 *b_size = strlen(*body);
61 status = HTTP_STATUS_OK;
62@@ -3487,6 +3505,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
63 }
64 }
65 else {
66+ /** the private timeline **/
67 double t = history_mtime(&snac, "timeline.html_");
68
69 /* if enabled by admin, return a cached page if its timestamp is:
70@@ -3500,19 +3519,22 @@ int html_get_handler(const xs_dict *req, const char *q_path,
71 xs_dict_get(req, "if-none-match"), etag);
72 }
73 else {
74+ int more = 0;
75+
76 snac_debug(&snac, 1, xs_fmt("building timeline"));
77
78- xs *list = timeline_list(&snac, "private", skip, show);
79- xs *next = timeline_list(&snac, "private", skip + show, 1);
80+ xs *list = timeline_list(&snac, "private", skip, show, &more);
81
82 *body = html_timeline(&snac, list, 0, skip, show,
83- xs_list_len(next), NULL, "/admin", 1, error);
84+ more, NULL, "/admin", 1, error);
85
86 *b_size = strlen(*body);
87 status = HTTP_STATUS_OK;
88
89 if (save)
90 history_add(&snac, "timeline.html_", *body, *b_size, etag);
91+
92+ timeline_add_mark(&snac);
93 }
94 }
95 }
96@@ -3712,7 +3734,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
97
98 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
99
100- xs *elems = timeline_simple_list(&snac, "public", 0, cnt);
101+ xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
102 xs *bio = xs_dup(xs_dict_get(snac.config, "bio"));
103
104 xs *rss_title = xs_fmt("%s (@%s@%s)",
M
main.c
+13,
-0
1@@ -49,6 +49,7 @@ int usage(void)
2 printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
3 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
4 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
5+ printf("unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n");
6 printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n");
7 printf("search {basedir} {uid} {regex} Searches posts by content\n");
8 printf("export_csv {basedir} {uid} Exports data as CSV files\n");
9@@ -446,6 +447,18 @@ int main(int argc, char *argv[])
10 return 0;
11 }
12
13+ if (strcmp(cmd, "unmute") == 0) { /** **/
14+ if (is_muted(&snac, url)) {
15+ unmute(&snac, url);
16+
17+ printf("%s unmuted\n", url);
18+ }
19+ else
20+ printf("%s actor is not muted\n", url);
21+
22+ return 0;
23+ }
24+
25 if (strcmp(cmd, "search") == 0) { /** **/
26 int to;
27
+1,
-1
1@@ -1676,7 +1676,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2 else
3 if (strcmp(opt, "statuses") == 0) { /** **/
4 /* the public list of posts of a user */
5- xs *timeline = timeline_simple_list(&snac2, "public", 0, 256);
6+ xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL);
7 xs_list *p = timeline;
8 const xs_str *v;
9
M
snac.h
+7,
-3
1@@ -1,7 +1,7 @@
2 /* snac - A simple, minimalistic ActivityPub instance */
3 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
4
5-#define VERSION "2.70-dev"
6+#define VERSION "2.72-dev"
7
8 #define USER_AGENT "snac/" VERSION
9
10@@ -22,6 +22,8 @@
11
12 #define MD5_HEX_SIZE 33
13
14+#define MD5_ALREADY_SEEN_MARK "00000000000000000000000000000000"
15+
16 extern double disk_layout;
17 extern xs_str *srv_basedir;
18 extern xs_dict *srv_config;
19@@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5);
20 int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
21 int timeline_del(snac *snac, const char *id);
22 xs_str *user_index_fn(snac *user, const char *idx_name);
23-xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show);
24-xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show);
25+xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more);
26+xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more);
27 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
28 int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
29
30 xs_list *timeline_top_level(snac *snac, const xs_list *list);
31+void timeline_add_mark(snac *user);
32+
33 xs_list *local_list(snac *snac, int max);
34 xs_str *instance_index_fn(void);
35 xs_list *timeline_instance_list(int skip, int show);
M
utils.c
+1,
-0
1@@ -73,6 +73,7 @@ static const char *default_css =
2 ".snac-list-of-lists { padding-left: 0; }\n"
3 ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n"
4 " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n"
5+ ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n"
6 "@media (prefers-color-scheme: dark) { \n"
7 " body, input, textarea { background-color: #000; color: #fff; }\n"
8 " a { color: #7799dd }\n"
M
xs.h
+28,
-8
1@@ -12,6 +12,7 @@
2 #include <stdarg.h>
3 #include <signal.h>
4 #include <errno.h>
5+#include <stdint.h>
6
7 typedef enum {
8 XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */
9@@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value);
10 void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size);
11
12 unsigned int xs_hash_func(const char *data, int size);
13+uint64_t xs_hash64_func(const char *data, int size);
14
15 #ifdef XS_ASSERT
16 #include <assert.h>
17@@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end)
18 end = sz + end;
19
20 /* crop from the top */
21- if (end > 0 && end < sz)
22+ if (end >= 0 && end < sz)
23 str[end] = '\0';
24
25 /* crop from the bottom */
26@@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep)
27 xs_list *xs_split_n(const char *str, const char *sep, int times)
28 /* splits a string into a list upto n times */
29 {
30+ xs_list *list = xs_list_new();
31+
32+ if (!xs_is_string(str) || !xs_is_string(sep))
33+ return list;
34+
35 int sz = strlen(sep);
36 char *ss;
37- xs_list *list;
38-
39- list = xs_list_new();
40
41 while (times > 0 && (ss = strstr(str, sep)) != NULL) {
42 /* create a new string with this slice and add it to the list */
43 xs *s = xs_str_new_sz(str, ss - str);
44- list = xs_list_append(list, s);
45+
46+ if (xs_is_string(s))
47+ list = xs_list_append(list, s);
48
49 /* skip past the separator */
50 str = ss + sz;
51@@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times)
52 }
53
54 /* add the rest of the string */
55- list = xs_list_append(list, str);
56+ if (xs_is_string(str))
57+ list = xs_list_append(list, str);
58
59 return list;
60 }
61@@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size)
62 /* a general purpose hashing function */
63 {
64 unsigned int hash = 0x666;
65- int n;
66
67- for (n = 0; n < size; n++) {
68+ for (int n = 0; n < size; n++) {
69 hash ^= (unsigned char)data[n];
70 hash *= 111111111;
71 }
72@@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size)
73 }
74
75
76+uint64_t xs_hash64_func(const char *data, int size)
77+/* a general purpose hashing function (64 bit) */
78+{
79+ uint64_t hash = 0x100;
80+
81+ for (int n = 0; n < size; n++) {
82+ hash ^= (unsigned char)data[n];
83+ hash *= 1111111111111111111;
84+ }
85+
86+ return hash;
87+}
88+
89+
90 #endif /* XS_IMPLEMENTATION */
91
92 #endif /* _XS_H */
M
xs_io.h
+2,
-1
1@@ -27,7 +27,8 @@ xs_str *xs_readline(FILE *f)
2 while ((c = fgetc(f)) != EOF) {
3 unsigned char rc = c;
4
5- s = xs_append_m(s, (char *)&rc, 1);
6+ if (xs_is_string((char *)&rc))
7+ s = xs_append_m(s, (char *)&rc, 1);
8
9 if (c == '\n')
10 break;
+6,
-1
1@@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec)
2 retry:
3
4 for (;;) {
5+ const char *q = spec;
6 char c = *str++;
7 char p = *spec++;
8
9@@ -63,8 +64,12 @@ retry:
10 spec = b_spec;
11 str = ++b_str;
12 }
13- else
14+ else {
15+ if (*q == '|')
16+ spec = q;
17+
18 break;
19+ }
20 }
21 }
22 }
+1,
-1
1@@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size)
2 s = xs_realloc(s, _xs_blk_size(*size + 1));
3 s[*size] = '\0';
4
5- BIO_free_all(mem);
6+ BIO_free_all(b64);
7
8 return s;
9 }
+2,
-0
1@@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv)
2 listen(rs, SOMAXCONN);
3 }
4
5+ freeaddrinfo(res);
6+
7 #else /* WITHOUT_GETADDRINFO */
8 struct sockaddr_in host;
9
M
xs_url.h
+7,
-1
1@@ -17,12 +17,18 @@ xs_str *xs_url_dec(const char *str)
2 xs_str *s = xs_str_new(NULL);
3
4 while (*str) {
5+ if (!xs_is_string(str))
6+ break;
7+
8 if (*str == '%') {
9 unsigned int i;
10
11 if (sscanf(str + 1, "%02x", &i) == 1) {
12 unsigned char uc = i;
13
14+ if (!xs_is_string((char *)&uc))
15+ break;
16+
17 s = xs_append_m(s, (char *)&uc, 1);
18 str += 2;
19 }
20@@ -69,7 +75,7 @@ xs_dict *xs_url_vars(const char *str)
21
22 vars = xs_dict_new();
23
24- if (str != NULL) {
25+ if (xs_is_string(str)) {
26 /* split by arguments */
27 xs *args = xs_split(str, "&");
28
+1,
-1
1@@ -1 +1 @@
2-/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */
3+/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */