commit 114ed37

ltning  ·  2025-02-04 18:38:41 +0000 UTC
parents f6044d3, a2dde5d
Merge branch 'master' into master
20 files changed,  +173, -43
M data.c
M html.c
M main.c
M snac.h
M xs.h
+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.
+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);
+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 */
+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 
+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 */