diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 16 | ||||
| -rw-r--r-- | TODO.md | 8 | ||||
| -rw-r--r-- | activitypub.c | 2 | ||||
| -rw-r--r-- | data.c | 45 | ||||
| -rw-r--r-- | doc/snac.1 | 5 | ||||
| -rw-r--r-- | doc/snac.5 | 2 | ||||
| -rw-r--r-- | doc/style.css | 1 | ||||
| -rw-r--r-- | html.c | 50 | ||||
| -rw-r--r-- | main.c | 13 | ||||
| -rw-r--r-- | mastoapi.c | 2 | ||||
| -rw-r--r-- | snac.h | 10 | ||||
| -rw-r--r-- | utils.c | 1 | ||||
| -rw-r--r-- | xs.h | 36 | ||||
| -rw-r--r-- | xs_io.h | 3 | ||||
| -rw-r--r-- | xs_match.h | 7 | ||||
| -rw-r--r-- | xs_openssl.h | 2 | ||||
| -rw-r--r-- | xs_socket.h | 2 | ||||
| -rw-r--r-- | xs_url.h | 8 | ||||
| -rw-r--r-- | xs_version.h | 2 |
20 files changed, 173 insertions, 43 deletions
| @@ -107,6 +107,7 @@ This will: | |||
| 107 | - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). | 107 | - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). |
| 108 | - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). | 108 | - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). |
| 109 | - [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. | 109 | - [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. |
| 110 | - [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/). | ||
| 110 | 111 | ||
| 111 | ## Incredibly awesome CSS themes for snac | 112 | ## Incredibly awesome CSS themes for snac |
| 112 | 113 | ||
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 23a2a17..f39a912 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md | |||
| @@ -1,5 +1,21 @@ | |||
| 1 | # Release Notes | 1 | # Release Notes |
| 2 | 2 | ||
| 3 | ## 2.71 | ||
| 4 | |||
| 5 | Fixed memory leak (contributed by inz). | ||
| 6 | |||
| 7 | Fixed crash. | ||
| 8 | |||
| 9 | ## 2.70 | ||
| 10 | |||
| 11 | 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). | ||
| 12 | |||
| 13 | New command-line option `unmute` to, well, no-longer-mute an actor. | ||
| 14 | |||
| 15 | The private timeline now includes an approximate mark between new posts and "already seen" ones. | ||
| 16 | |||
| 17 | Fixed a spurious 404 error in the instance root URL for some configurations. | ||
| 18 | |||
| 3 | ## 2.69 "Yin/Yang of Love" | 19 | ## 2.69 "Yin/Yang of Love" |
| 4 | 20 | ||
| 5 | 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. | 21 | 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. |
| @@ -14,14 +14,10 @@ Important: deleting a follower should do more that just delete the object, see h | |||
| 14 | 14 | ||
| 15 | ## Wishlist | 15 | ## Wishlist |
| 16 | 16 | ||
| 17 | Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information). | ||
| 18 | |||
| 19 | The instance timeline should also show boosts from users. | 17 | The instance timeline should also show boosts from users. |
| 20 | 18 | ||
| 21 | Mastoapi: implement /v1/conversations. | 19 | Mastoapi: implement /v1/conversations. |
| 22 | 20 | ||
| 23 | Implement following of hashtags (this is not trivial). | ||
| 24 | |||
| 25 | 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). | 21 | 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). |
| 26 | 22 | ||
| 27 | Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md | 23 | 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 | |||
| 365 | Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). | 361 | Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). |
| 366 | 362 | ||
| 367 | Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). | 363 | Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). |
| 364 | |||
| 365 | Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100). | ||
| 366 | |||
| 367 | 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 index cade0d9..bcb733a 100644 --- a/activitypub.c +++ b/activitypub.c | |||
| @@ -3081,7 +3081,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, | |||
| 3081 | int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); | 3081 | int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); |
| 3082 | 3082 | ||
| 3083 | /* get the public outbox or the pinned list */ | 3083 | /* get the public outbox or the pinned list */ |
| 3084 | xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac); | 3084 | xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac); |
| 3085 | 3085 | ||
| 3086 | xs_list_foreach(elems, v) { | 3086 | xs_list_foreach(elems, v) { |
| 3087 | xs *i = NULL; | 3087 | xs *i = NULL; |
| @@ -1489,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name) | |||
| 1489 | } | 1489 | } |
| 1490 | 1490 | ||
| 1491 | 1491 | ||
| 1492 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show) | 1492 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more) |
| 1493 | /* returns a timeline (with all entries) */ | 1493 | /* returns a timeline (with all entries) */ |
| 1494 | { | 1494 | { |
| 1495 | xs *idx = user_index_fn(user, idx_name); | 1495 | xs *idx = user_index_fn(user, idx_name); |
| 1496 | 1496 | ||
| 1497 | return index_list_desc(idx, skip, show); | 1497 | /* if a more flag is sent, request one more */ |
| 1498 | xs_list *lst = index_list_desc(idx, skip, show + (more != NULL ? 1 : 0)); | ||
| 1499 | |||
| 1500 | if (more != NULL) { | ||
| 1501 | if (xs_list_len(lst) > show) { | ||
| 1502 | *more = 1; | ||
| 1503 | lst = xs_list_del(lst, -1); | ||
| 1504 | } | ||
| 1505 | else | ||
| 1506 | *more = 0; | ||
| 1507 | } | ||
| 1508 | |||
| 1509 | return lst; | ||
| 1498 | } | 1510 | } |
| 1499 | 1511 | ||
| 1500 | 1512 | ||
| 1501 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) | 1513 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more) |
| 1502 | /* returns a timeline (only top level entries) */ | 1514 | /* returns a timeline (only top level entries) */ |
| 1503 | { | 1515 | { |
| 1504 | int c_max; | 1516 | int c_max; |
| @@ -1510,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) | |||
| 1510 | if (show > c_max) | 1522 | if (show > c_max) |
| 1511 | show = c_max; | 1523 | show = c_max; |
| 1512 | 1524 | ||
| 1513 | xs *list = timeline_simple_list(snac, idx_name, skip, show); | 1525 | xs *list = timeline_simple_list(snac, idx_name, skip, show, more); |
| 1514 | 1526 | ||
| 1515 | return timeline_top_level(snac, list); | 1527 | return timeline_top_level(snac, list); |
| 1516 | } | 1528 | } |
| 1517 | 1529 | ||
| 1518 | 1530 | ||
| 1531 | void timeline_add_mark(snac *user) | ||
| 1532 | /* adds an "already seen" mark to the private timeline */ | ||
| 1533 | { | ||
| 1534 | xs *fn = xs_fmt("%s/private.idx", user->basedir); | ||
| 1535 | char last_entry[MD5_HEX_SIZE] = ""; | ||
| 1536 | FILE *f; | ||
| 1537 | |||
| 1538 | /* get the last entry in the index */ | ||
| 1539 | if ((f = fopen(fn, "r")) != NULL) { | ||
| 1540 | index_desc_first(f, last_entry, 0); | ||
| 1541 | fclose(f); | ||
| 1542 | } | ||
| 1543 | |||
| 1544 | /* is the last entry *not* a mark? */ | ||
| 1545 | if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) { | ||
| 1546 | /* add it */ | ||
| 1547 | index_add_md5(fn, MD5_ALREADY_SEEN_MARK); | ||
| 1548 | } | ||
| 1549 | } | ||
| 1550 | |||
| 1551 | |||
| 1519 | xs_str *instance_index_fn(void) | 1552 | xs_str *instance_index_fn(void) |
| 1520 | { | 1553 | { |
| 1521 | return xs_fmt("%s/public.idx", srv_basedir); | 1554 | return xs_fmt("%s/public.idx", srv_basedir); |
| @@ -2709,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex, | |||
| 2709 | const char *md5s[3] = {0}; | 2742 | const char *md5s[3] = {0}; |
| 2710 | int c[3] = {0}; | 2743 | int c[3] = {0}; |
| 2711 | 2744 | ||
| 2712 | tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */ | 2745 | tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */ |
| 2713 | tls[1] = timeline_instance_list(0, XS_ALL); /* instance */ | 2746 | tls[1] = timeline_instance_list(0, XS_ALL); /* instance */ |
| 2714 | tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL) : xs_list_new(); /* private or none */ | 2747 | tls[2] = priv ? timeline_simple_list(user, "private", 0, XS_ALL, NULL) : xs_list_new(); /* private or none */ |
| 2715 | 2748 | ||
| 2716 | /* first positioning */ | 2749 | /* first positioning */ |
| 2717 | for (int n = 0; n < 3; n++) | 2750 | for (int n = 0; n < 3; n++) |
| @@ -234,6 +234,8 @@ Purges old data from the timeline of all users. | |||
| 234 | .It Cm adduser Ar basedir Op uid | 234 | .It Cm adduser Ar basedir Op uid |
| 235 | Adds a new user to the server. This is an interactive command; | 235 | Adds a new user to the server. This is an interactive command; |
| 236 | necessary information will be prompted for. | 236 | necessary information will be prompted for. |
| 237 | .It Cm deluser Ar basedir Ar uid | ||
| 238 | Deletes a user, unfollowing all accounts first. | ||
| 237 | .It Cm resetpwd Ar basedir Ar uid | 239 | .It Cm resetpwd Ar basedir Ar uid |
| 238 | Resets a user's password to a new, random one. | 240 | Resets a user's password to a new, random one. |
| 239 | .It Cm queue Ar basedir Ar uid | 241 | .It Cm queue Ar basedir Ar uid |
| @@ -257,6 +259,9 @@ The rest of command line arguments are treated as media files to be | |||
| 257 | attached to the post. | 259 | attached to the post. |
| 258 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... | 260 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... |
| 259 | Like the previous one, but creates an "unlisted" (or "quiet public") post. | 261 | Like the previous one, but creates an "unlisted" (or "quiet public") post. |
| 262 | .It Cm note_mention Ar basedir Ar uid Ar text Op file file ... | ||
| 263 | Like the previous one, but creates a post only for accounts mentioned | ||
| 264 | in the post body. | ||
| 260 | .It Cm block Ar basedir Ar instance_url | 265 | .It Cm block Ar basedir Ar instance_url |
| 261 | Blocks a full instance, given its URL or domain name. All subsequent | 266 | Blocks a full instance, given its URL or domain name. All subsequent |
| 262 | incoming activities with identifiers from that instance will be immediately | 267 | incoming activities with identifiers from that instance will be immediately |
| @@ -78,7 +78,7 @@ converted to related emojis: | |||
| 78 | .Ss Accepted HTML | 78 | .Ss Accepted HTML |
| 79 | All HTML tags in entries are neutered except the following ones: | 79 | All HTML tags in entries are neutered except the following ones: |
| 80 | .Bd -literal | 80 | .Bd -literal |
| 81 | a p br blockquote ul ol li cite small | 81 | a p br blockquote ul ol li cite small h2 h3 |
| 82 | span i b u s pre code em strong hr img del | 82 | span i b u s pre code em strong hr img del |
| 83 | .Ed | 83 | .Ed |
| 84 | .Pp | 84 | .Pp |
diff --git a/doc/style.css b/doc/style.css index 13cef97..87c5598 100644 --- a/doc/style.css +++ b/doc/style.css | |||
| @@ -29,6 +29,7 @@ pre { overflow-x: scroll; } | |||
| 29 | .snac-list-of-lists { padding-left: 0; } | 29 | .snac-list-of-lists { padding-left: 0; } |
| 30 | .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px; | 30 | .snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px; |
| 31 | margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; } | 31 | margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; } |
| 32 | .snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; } | ||
| 32 | @media (prefers-color-scheme: dark) { | 33 | @media (prefers-color-scheme: dark) { |
| 33 | body, input, textarea { background-color: #000; color: #fff; } | 34 | body, input, textarea { background-color: #000; color: #fff; } |
| 34 | a { color: #7799dd } | 35 | a { color: #7799dd } |
| @@ -2658,10 +2658,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, | |||
| 2658 | xs_html_add(body, | 2658 | xs_html_add(body, |
| 2659 | posts); | 2659 | posts); |
| 2660 | 2660 | ||
| 2661 | int mark_shown = 0; | ||
| 2662 | |||
| 2661 | while (xs_list_iter(&p, &v)) { | 2663 | while (xs_list_iter(&p, &v)) { |
| 2662 | xs *msg = NULL; | 2664 | xs *msg = NULL; |
| 2663 | int status; | 2665 | int status; |
| 2664 | 2666 | ||
| 2667 | /* "already seen" mark? */ | ||
| 2668 | if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) { | ||
| 2669 | if (skip == 0 && !mark_shown) { | ||
| 2670 | xs *s = xs_fmt("%s/admin", user->actor); | ||
| 2671 | |||
| 2672 | xs_html_add(posts, | ||
| 2673 | xs_html_tag("div", | ||
| 2674 | xs_html_attr("class", "snac-no-more-unseen-posts"), | ||
| 2675 | xs_html_text(L("No more unseen posts")), | ||
| 2676 | xs_html_text(" - "), | ||
| 2677 | xs_html_tag("a", | ||
| 2678 | xs_html_attr("href", s), | ||
| 2679 | xs_html_text(L("Back to top"))))); | ||
| 2680 | } | ||
| 2681 | |||
| 2682 | mark_shown = 1; | ||
| 2683 | |||
| 2684 | continue; | ||
| 2685 | } | ||
| 2686 | |||
| 2665 | if (utl && user && !is_pinned_by_md5(user, v)) | 2687 | if (utl && user && !is_pinned_by_md5(user, v)) |
| 2666 | status = timeline_get_by_md5(user, v, &msg); | 2688 | status = timeline_get_by_md5(user, v, &msg); |
| 2667 | else | 2689 | else |
| @@ -3324,21 +3346,17 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3324 | } | 3346 | } |
| 3325 | else { | 3347 | else { |
| 3326 | xs *list = NULL; | 3348 | xs *list = NULL; |
| 3327 | xs *next = NULL; | 3349 | int more = 0; |
| 3328 | 3350 | ||
| 3329 | if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) { | 3351 | if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) |
| 3330 | list = timeline_simple_list(&snac, "public", skip, show); | 3352 | list = timeline_simple_list(&snac, "public", skip, show, &more); |
| 3331 | next = timeline_simple_list(&snac, "public", skip + show, 1); | 3353 | else |
| 3332 | } | 3354 | list = timeline_list(&snac, "public", skip, show, &more); |
| 3333 | else { | ||
| 3334 | list = timeline_list(&snac, "public", skip, show); | ||
| 3335 | next = timeline_list(&snac, "public", skip + show, 1); | ||
| 3336 | } | ||
| 3337 | 3355 | ||
| 3338 | xs *pins = pinned_list(&snac); | 3356 | xs *pins = pinned_list(&snac); |
| 3339 | pins = xs_list_cat(pins, list); | 3357 | pins = xs_list_cat(pins, list); |
| 3340 | 3358 | ||
| 3341 | *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error); | 3359 | *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error); |
| 3342 | 3360 | ||
| 3343 | *b_size = strlen(*body); | 3361 | *b_size = strlen(*body); |
| 3344 | status = HTTP_STATUS_OK; | 3362 | status = HTTP_STATUS_OK; |
| @@ -3487,6 +3505,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3487 | } | 3505 | } |
| 3488 | } | 3506 | } |
| 3489 | else { | 3507 | else { |
| 3508 | /** the private timeline **/ | ||
| 3490 | double t = history_mtime(&snac, "timeline.html_"); | 3509 | double t = history_mtime(&snac, "timeline.html_"); |
| 3491 | 3510 | ||
| 3492 | /* if enabled by admin, return a cached page if its timestamp is: | 3511 | /* 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, | |||
| 3500 | xs_dict_get(req, "if-none-match"), etag); | 3519 | xs_dict_get(req, "if-none-match"), etag); |
| 3501 | } | 3520 | } |
| 3502 | else { | 3521 | else { |
| 3522 | int more = 0; | ||
| 3523 | |||
| 3503 | snac_debug(&snac, 1, xs_fmt("building timeline")); | 3524 | snac_debug(&snac, 1, xs_fmt("building timeline")); |
| 3504 | 3525 | ||
| 3505 | xs *list = timeline_list(&snac, "private", skip, show); | 3526 | xs *list = timeline_list(&snac, "private", skip, show, &more); |
| 3506 | xs *next = timeline_list(&snac, "private", skip + show, 1); | ||
| 3507 | 3527 | ||
| 3508 | *body = html_timeline(&snac, list, 0, skip, show, | 3528 | *body = html_timeline(&snac, list, 0, skip, show, |
| 3509 | xs_list_len(next), NULL, "/admin", 1, error); | 3529 | more, NULL, "/admin", 1, error); |
| 3510 | 3530 | ||
| 3511 | *b_size = strlen(*body); | 3531 | *b_size = strlen(*body); |
| 3512 | status = HTTP_STATUS_OK; | 3532 | status = HTTP_STATUS_OK; |
| 3513 | 3533 | ||
| 3514 | if (save) | 3534 | if (save) |
| 3515 | history_add(&snac, "timeline.html_", *body, *b_size, etag); | 3535 | history_add(&snac, "timeline.html_", *body, *b_size, etag); |
| 3536 | |||
| 3537 | timeline_add_mark(&snac); | ||
| 3516 | } | 3538 | } |
| 3517 | } | 3539 | } |
| 3518 | } | 3540 | } |
| @@ -3712,7 +3734,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3712 | 3734 | ||
| 3713 | int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); | 3735 | int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); |
| 3714 | 3736 | ||
| 3715 | xs *elems = timeline_simple_list(&snac, "public", 0, cnt); | 3737 | xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL); |
| 3716 | xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); | 3738 | xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); |
| 3717 | 3739 | ||
| 3718 | xs *rss_title = xs_fmt("%s (@%s@%s)", | 3740 | xs *rss_title = xs_fmt("%s (@%s@%s)", |
| @@ -49,6 +49,7 @@ int usage(void) | |||
| 49 | printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); | 49 | printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); |
| 50 | printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); | 50 | printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); |
| 51 | printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); | 51 | printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); |
| 52 | printf("unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n"); | ||
| 52 | printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); | 53 | printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); |
| 53 | printf("search {basedir} {uid} {regex} Searches posts by content\n"); | 54 | printf("search {basedir} {uid} {regex} Searches posts by content\n"); |
| 54 | printf("export_csv {basedir} {uid} Exports data as CSV files\n"); | 55 | printf("export_csv {basedir} {uid} Exports data as CSV files\n"); |
| @@ -446,6 +447,18 @@ int main(int argc, char *argv[]) | |||
| 446 | return 0; | 447 | return 0; |
| 447 | } | 448 | } |
| 448 | 449 | ||
| 450 | if (strcmp(cmd, "unmute") == 0) { /** **/ | ||
| 451 | if (is_muted(&snac, url)) { | ||
| 452 | unmute(&snac, url); | ||
| 453 | |||
| 454 | printf("%s unmuted\n", url); | ||
| 455 | } | ||
| 456 | else | ||
| 457 | printf("%s actor is not muted\n", url); | ||
| 458 | |||
| 459 | return 0; | ||
| 460 | } | ||
| 461 | |||
| 449 | if (strcmp(cmd, "search") == 0) { /** **/ | 462 | if (strcmp(cmd, "search") == 0) { /** **/ |
| 450 | int to; | 463 | int to; |
| 451 | 464 | ||
| @@ -1676,7 +1676,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1676 | else | 1676 | else |
| 1677 | if (strcmp(opt, "statuses") == 0) { /** **/ | 1677 | if (strcmp(opt, "statuses") == 0) { /** **/ |
| 1678 | /* the public list of posts of a user */ | 1678 | /* the public list of posts of a user */ |
| 1679 | xs *timeline = timeline_simple_list(&snac2, "public", 0, 256); | 1679 | xs *timeline = timeline_simple_list(&snac2, "public", 0, 256, NULL); |
| 1680 | xs_list *p = timeline; | 1680 | xs_list *p = timeline; |
| 1681 | const xs_str *v; | 1681 | const xs_str *v; |
| 1682 | 1682 | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | 1 | /* snac - A simple, minimalistic ActivityPub instance */ |
| 2 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 2 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #define VERSION "2.70-dev" | 4 | #define VERSION "2.72-dev" |
| 5 | 5 | ||
| 6 | #define USER_AGENT "snac/" VERSION | 6 | #define USER_AGENT "snac/" VERSION |
| 7 | 7 | ||
| @@ -22,6 +22,8 @@ | |||
| 22 | 22 | ||
| 23 | #define MD5_HEX_SIZE 33 | 23 | #define MD5_HEX_SIZE 33 |
| 24 | 24 | ||
| 25 | #define MD5_ALREADY_SEEN_MARK "00000000000000000000000000000000" | ||
| 26 | |||
| 25 | extern double disk_layout; | 27 | extern double disk_layout; |
| 26 | extern xs_str *srv_basedir; | 28 | extern xs_str *srv_basedir; |
| 27 | extern xs_dict *srv_config; | 29 | extern xs_dict *srv_config; |
| @@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5); | |||
| 157 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); | 159 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); |
| 158 | int timeline_del(snac *snac, const char *id); | 160 | int timeline_del(snac *snac, const char *id); |
| 159 | xs_str *user_index_fn(snac *user, const char *idx_name); | 161 | xs_str *user_index_fn(snac *user, const char *idx_name); |
| 160 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show); | 162 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more); |
| 161 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); | 163 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more); |
| 162 | int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); | 164 | int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); |
| 163 | int timeline_admire(snac *snac, const char *id, const char *admirer, int like); | 165 | int timeline_admire(snac *snac, const char *id, const char *admirer, int like); |
| 164 | 166 | ||
| 165 | xs_list *timeline_top_level(snac *snac, const xs_list *list); | 167 | xs_list *timeline_top_level(snac *snac, const xs_list *list); |
| 168 | void timeline_add_mark(snac *user); | ||
| 169 | |||
| 166 | xs_list *local_list(snac *snac, int max); | 170 | xs_list *local_list(snac *snac, int max); |
| 167 | xs_str *instance_index_fn(void); | 171 | xs_str *instance_index_fn(void); |
| 168 | xs_list *timeline_instance_list(int skip, int show); | 172 | xs_list *timeline_instance_list(int skip, int show); |
| @@ -73,6 +73,7 @@ static const char *default_css = | |||
| 73 | ".snac-list-of-lists { padding-left: 0; }\n" | 73 | ".snac-list-of-lists { padding-left: 0; }\n" |
| 74 | ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n" | 74 | ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n" |
| 75 | " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n" | 75 | " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n" |
| 76 | ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n" | ||
| 76 | "@media (prefers-color-scheme: dark) { \n" | 77 | "@media (prefers-color-scheme: dark) { \n" |
| 77 | " body, input, textarea { background-color: #000; color: #fff; }\n" | 78 | " body, input, textarea { background-color: #000; color: #fff; }\n" |
| 78 | " a { color: #7799dd }\n" | 79 | " a { color: #7799dd }\n" |
| @@ -12,6 +12,7 @@ | |||
| 12 | #include <stdarg.h> | 12 | #include <stdarg.h> |
| 13 | #include <signal.h> | 13 | #include <signal.h> |
| 14 | #include <errno.h> | 14 | #include <errno.h> |
| 15 | #include <stdint.h> | ||
| 15 | 16 | ||
| 16 | typedef enum { | 17 | typedef enum { |
| 17 | XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ | 18 | XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ |
| @@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value); | |||
| 142 | void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); | 143 | void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); |
| 143 | 144 | ||
| 144 | unsigned int xs_hash_func(const char *data, int size); | 145 | unsigned int xs_hash_func(const char *data, int size); |
| 146 | uint64_t xs_hash64_func(const char *data, int size); | ||
| 145 | 147 | ||
| 146 | #ifdef XS_ASSERT | 148 | #ifdef XS_ASSERT |
| 147 | #include <assert.h> | 149 | #include <assert.h> |
| @@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end) | |||
| 632 | end = sz + end; | 634 | end = sz + end; |
| 633 | 635 | ||
| 634 | /* crop from the top */ | 636 | /* crop from the top */ |
| 635 | if (end > 0 && end < sz) | 637 | if (end >= 0 && end < sz) |
| 636 | str[end] = '\0'; | 638 | str[end] = '\0'; |
| 637 | 639 | ||
| 638 | /* crop from the bottom */ | 640 | /* crop from the bottom */ |
| @@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep) | |||
| 989 | xs_list *xs_split_n(const char *str, const char *sep, int times) | 991 | xs_list *xs_split_n(const char *str, const char *sep, int times) |
| 990 | /* splits a string into a list upto n times */ | 992 | /* splits a string into a list upto n times */ |
| 991 | { | 993 | { |
| 994 | xs_list *list = xs_list_new(); | ||
| 995 | |||
| 996 | if (!xs_is_string(str) || !xs_is_string(sep)) | ||
| 997 | return list; | ||
| 998 | |||
| 992 | int sz = strlen(sep); | 999 | int sz = strlen(sep); |
| 993 | char *ss; | 1000 | char *ss; |
| 994 | xs_list *list; | ||
| 995 | |||
| 996 | list = xs_list_new(); | ||
| 997 | 1001 | ||
| 998 | while (times > 0 && (ss = strstr(str, sep)) != NULL) { | 1002 | while (times > 0 && (ss = strstr(str, sep)) != NULL) { |
| 999 | /* create a new string with this slice and add it to the list */ | 1003 | /* create a new string with this slice and add it to the list */ |
| 1000 | xs *s = xs_str_new_sz(str, ss - str); | 1004 | xs *s = xs_str_new_sz(str, ss - str); |
| 1001 | list = xs_list_append(list, s); | 1005 | |
| 1006 | if (xs_is_string(s)) | ||
| 1007 | list = xs_list_append(list, s); | ||
| 1002 | 1008 | ||
| 1003 | /* skip past the separator */ | 1009 | /* skip past the separator */ |
| 1004 | str = ss + sz; | 1010 | str = ss + sz; |
| @@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times) | |||
| 1007 | } | 1013 | } |
| 1008 | 1014 | ||
| 1009 | /* add the rest of the string */ | 1015 | /* add the rest of the string */ |
| 1010 | list = xs_list_append(list, str); | 1016 | if (xs_is_string(str)) |
| 1017 | list = xs_list_append(list, str); | ||
| 1011 | 1018 | ||
| 1012 | return list; | 1019 | return list; |
| 1013 | } | 1020 | } |
| @@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size) | |||
| 1487 | /* a general purpose hashing function */ | 1494 | /* a general purpose hashing function */ |
| 1488 | { | 1495 | { |
| 1489 | unsigned int hash = 0x666; | 1496 | unsigned int hash = 0x666; |
| 1490 | int n; | ||
| 1491 | 1497 | ||
| 1492 | for (n = 0; n < size; n++) { | 1498 | for (int n = 0; n < size; n++) { |
| 1493 | hash ^= (unsigned char)data[n]; | 1499 | hash ^= (unsigned char)data[n]; |
| 1494 | hash *= 111111111; | 1500 | hash *= 111111111; |
| 1495 | } | 1501 | } |
| @@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size) | |||
| 1498 | } | 1504 | } |
| 1499 | 1505 | ||
| 1500 | 1506 | ||
| 1507 | uint64_t xs_hash64_func(const char *data, int size) | ||
| 1508 | /* a general purpose hashing function (64 bit) */ | ||
| 1509 | { | ||
| 1510 | uint64_t hash = 0x100; | ||
| 1511 | |||
| 1512 | for (int n = 0; n < size; n++) { | ||
| 1513 | hash ^= (unsigned char)data[n]; | ||
| 1514 | hash *= 1111111111111111111; | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | return hash; | ||
| 1518 | } | ||
| 1519 | |||
| 1520 | |||
| 1501 | #endif /* XS_IMPLEMENTATION */ | 1521 | #endif /* XS_IMPLEMENTATION */ |
| 1502 | 1522 | ||
| 1503 | #endif /* _XS_H */ | 1523 | #endif /* _XS_H */ |
| @@ -27,7 +27,8 @@ xs_str *xs_readline(FILE *f) | |||
| 27 | while ((c = fgetc(f)) != EOF) { | 27 | while ((c = fgetc(f)) != EOF) { |
| 28 | unsigned char rc = c; | 28 | unsigned char rc = c; |
| 29 | 29 | ||
| 30 | s = xs_append_m(s, (char *)&rc, 1); | 30 | if (xs_is_string((char *)&rc)) |
| 31 | s = xs_append_m(s, (char *)&rc, 1); | ||
| 31 | 32 | ||
| 32 | if (c == '\n') | 33 | if (c == '\n') |
| 33 | break; | 34 | break; |
| @@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec) | |||
| 24 | retry: | 24 | retry: |
| 25 | 25 | ||
| 26 | for (;;) { | 26 | for (;;) { |
| 27 | const char *q = spec; | ||
| 27 | char c = *str++; | 28 | char c = *str++; |
| 28 | char p = *spec++; | 29 | char p = *spec++; |
| 29 | 30 | ||
| @@ -63,8 +64,12 @@ retry: | |||
| 63 | spec = b_spec; | 64 | spec = b_spec; |
| 64 | str = ++b_str; | 65 | str = ++b_str; |
| 65 | } | 66 | } |
| 66 | else | 67 | else { |
| 68 | if (*q == '|') | ||
| 69 | spec = q; | ||
| 70 | |||
| 67 | break; | 71 | break; |
| 72 | } | ||
| 68 | } | 73 | } |
| 69 | } | 74 | } |
| 70 | } | 75 | } |
diff --git a/xs_openssl.h b/xs_openssl.h index 9388691..f215bcc 100644 --- a/xs_openssl.h +++ b/xs_openssl.h | |||
| @@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size) | |||
| 83 | s = xs_realloc(s, _xs_blk_size(*size + 1)); | 83 | s = xs_realloc(s, _xs_blk_size(*size + 1)); |
| 84 | s[*size] = '\0'; | 84 | s[*size] = '\0'; |
| 85 | 85 | ||
| 86 | BIO_free_all(mem); | 86 | BIO_free_all(b64); |
| 87 | 87 | ||
| 88 | return s; | 88 | return s; |
| 89 | } | 89 | } |
diff --git a/xs_socket.h b/xs_socket.h index 6e618ba..7bf5298 100644 --- a/xs_socket.h +++ b/xs_socket.h | |||
| @@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv) | |||
| 85 | listen(rs, SOMAXCONN); | 85 | listen(rs, SOMAXCONN); |
| 86 | } | 86 | } |
| 87 | 87 | ||
| 88 | freeaddrinfo(res); | ||
| 89 | |||
| 88 | #else /* WITHOUT_GETADDRINFO */ | 90 | #else /* WITHOUT_GETADDRINFO */ |
| 89 | struct sockaddr_in host; | 91 | struct sockaddr_in host; |
| 90 | 92 | ||
| @@ -17,12 +17,18 @@ xs_str *xs_url_dec(const char *str) | |||
| 17 | xs_str *s = xs_str_new(NULL); | 17 | xs_str *s = xs_str_new(NULL); |
| 18 | 18 | ||
| 19 | while (*str) { | 19 | while (*str) { |
| 20 | if (!xs_is_string(str)) | ||
| 21 | break; | ||
| 22 | |||
| 20 | if (*str == '%') { | 23 | if (*str == '%') { |
| 21 | unsigned int i; | 24 | unsigned int i; |
| 22 | 25 | ||
| 23 | if (sscanf(str + 1, "%02x", &i) == 1) { | 26 | if (sscanf(str + 1, "%02x", &i) == 1) { |
| 24 | unsigned char uc = i; | 27 | unsigned char uc = i; |
| 25 | 28 | ||
| 29 | if (!xs_is_string((char *)&uc)) | ||
| 30 | break; | ||
| 31 | |||
| 26 | s = xs_append_m(s, (char *)&uc, 1); | 32 | s = xs_append_m(s, (char *)&uc, 1); |
| 27 | str += 2; | 33 | str += 2; |
| 28 | } | 34 | } |
| @@ -69,7 +75,7 @@ xs_dict *xs_url_vars(const char *str) | |||
| 69 | 75 | ||
| 70 | vars = xs_dict_new(); | 76 | vars = xs_dict_new(); |
| 71 | 77 | ||
| 72 | if (str != NULL) { | 78 | if (xs_is_string(str)) { |
| 73 | /* split by arguments */ | 79 | /* split by arguments */ |
| 74 | xs *args = xs_split(str, "&"); | 80 | xs *args = xs_split(str, "&"); |
| 75 | 81 | ||
diff --git a/xs_version.h b/xs_version.h index 12f713a..7314133 100644 --- 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 */ | ||