Browse Source

Merge branch 'master' into master

ltning 2 months ago
parent
commit
114ed37f9c
20 changed files with 173 additions and 43 deletions
  1. 1 0
      README.md
  2. 16 0
      RELEASE_NOTES.md
  3. 4 4
      TODO.md
  4. 1 1
      activitypub.c
  5. 39 6
      data.c
  6. 5 0
      doc/snac.1
  7. 1 1
      doc/snac.5
  8. 1 0
      doc/style.css
  9. 36 14
      html.c
  10. 13 0
      main.c
  11. 1 1
      mastoapi.c
  12. 7 3
      snac.h
  13. 1 0
      utils.c
  14. 28 8
      xs.h
  15. 2 1
      xs_io.h
  16. 6 1
      xs_match.h
  17. 1 1
      xs_openssl.h
  18. 2 0
      xs_socket.h
  19. 7 1
      xs_url.h
  20. 1 1
      xs_version.h

+ 1 - 0
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
 

+ 16 - 0
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.

+ 4 - 4
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).

+ 1 - 1
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;

+ 39 - 6
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++)

+ 5 - 0
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

+ 1 - 1
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

+ 1 - 0
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 }

+ 36 - 14
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)",

+ 13 - 0
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;
 

+ 1 - 1
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;
 

+ 7 - 3
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);

+ 1 - 0
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"

+ 28 - 8
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 */

+ 2 - 1
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;

+ 6 - 1
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;
+                }
             }
         }
     }

+ 1 - 1
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;
 }

+ 2 - 0
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;
 

+ 7 - 1
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, "&");
 

+ 1 - 1
xs_version.h

@@ -1 +1 @@
-/* b865e89769aedfdbc61251e94451e9d37579f52e 2025-01-12T16:17:47+01:00 */
+/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */