summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--RELEASE_NOTES.md16
-rw-r--r--TODO.md8
-rw-r--r--activitypub.c2
-rw-r--r--data.c45
-rw-r--r--doc/snac.15
-rw-r--r--doc/snac.52
-rw-r--r--doc/style.css1
-rw-r--r--html.c50
-rw-r--r--main.c13
-rw-r--r--mastoapi.c2
-rw-r--r--snac.h10
-rw-r--r--utils.c1
-rw-r--r--xs.h36
-rw-r--r--xs_io.h3
-rw-r--r--xs_match.h7
-rw-r--r--xs_openssl.h2
-rw-r--r--xs_socket.h2
-rw-r--r--xs_url.h8
-rw-r--r--xs_version.h2
20 files changed, 173 insertions, 43 deletions
diff --git a/README.md b/README.md
index de11b98..8d5a59d 100644
--- a/README.md
+++ b/README.md
@@ -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
5Fixed memory leak (contributed by inz).
6
7Fixed crash.
8
9## 2.70
10
11Notifications 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
13New command-line option `unmute` to, well, no-longer-mute an actor.
14
15The private timeline now includes an approximate mark between new posts and "already seen" ones.
16
17Fixed 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
5Added 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. 21Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature.
diff --git a/TODO.md b/TODO.md
index 86c706a..045b4cc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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
17Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information).
18
19The instance timeline should also show boosts from users. 17The instance timeline should also show boosts from users.
20 18
21Mastoapi: implement /v1/conversations. 19Mastoapi: implement /v1/conversations.
22 20
23Implement following of hashtags (this is not trivial).
24
25Track '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). 21Track '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
27Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md 23Implement "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
365Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). 361Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
366 362
367Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). 363Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
364
365Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
366
367Add 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;
diff --git a/data.c b/data.c
index 40382d2..50c6121 100644
--- a/data.c
+++ b/data.c
@@ -1489,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name)
1489} 1489}
1490 1490
1491 1491
1492xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show) 1492xs_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
1501xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) 1513xs_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
1531void 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
1519xs_str *instance_index_fn(void) 1552xs_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++)
diff --git a/doc/snac.1 b/doc/snac.1
index d33bccb..327e071 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -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
235Adds a new user to the server. This is an interactive command; 235Adds a new user to the server. This is an interactive command;
236necessary information will be prompted for. 236necessary information will be prompted for.
237.It Cm deluser Ar basedir Ar uid
238Deletes a user, unfollowing all accounts first.
237.It Cm resetpwd Ar basedir Ar uid 239.It Cm resetpwd Ar basedir Ar uid
238Resets a user's password to a new, random one. 240Resets 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
257attached to the post. 259attached 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 ...
259Like the previous one, but creates an "unlisted" (or "quiet public") post. 261Like 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 ...
263Like the previous one, but creates a post only for accounts mentioned
264in the post body.
260.It Cm block Ar basedir Ar instance_url 265.It Cm block Ar basedir Ar instance_url
261Blocks a full instance, given its URL or domain name. All subsequent 266Blocks a full instance, given its URL or domain name. All subsequent
262incoming activities with identifiers from that instance will be immediately 267incoming activities with identifiers from that instance will be immediately
diff --git a/doc/snac.5 b/doc/snac.5
index 3550995..d873908 100644
--- a/doc/snac.5
+++ b/doc/snac.5
@@ -78,7 +78,7 @@ converted to related emojis:
78.Ss Accepted HTML 78.Ss Accepted HTML
79All HTML tags in entries are neutered except the following ones: 79All HTML tags in entries are neutered except the following ones:
80.Bd -literal 80.Bd -literal
81a p br blockquote ul ol li cite small 81a p br blockquote ul ol li cite small h2 h3
82span i b u s pre code em strong hr img del 82span 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 }
diff --git a/html.c b/html.c
index 6c42842..54920fb 100644
--- a/html.c
+++ b/html.c
@@ -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)",
diff --git a/main.c b/main.c
index a57adb5..347c495 100644
--- a/main.c
+++ b/main.c
@@ -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
diff --git a/mastoapi.c b/mastoapi.c
index 54b4333..3c445c2 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -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
diff --git a/snac.h b/snac.h
index 344dbaa..7b5c54c 100644
--- a/snac.h
+++ b/snac.h
@@ -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
25extern double disk_layout; 27extern double disk_layout;
26extern xs_str *srv_basedir; 28extern xs_str *srv_basedir;
27extern xs_dict *srv_config; 29extern xs_dict *srv_config;
@@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5);
157int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); 159int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg);
158int timeline_del(snac *snac, const char *id); 160int timeline_del(snac *snac, const char *id);
159xs_str *user_index_fn(snac *user, const char *idx_name); 161xs_str *user_index_fn(snac *user, const char *idx_name);
160xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show); 162xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more);
161xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); 163xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more);
162int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); 164int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
163int timeline_admire(snac *snac, const char *id, const char *admirer, int like); 165int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
164 166
165xs_list *timeline_top_level(snac *snac, const xs_list *list); 167xs_list *timeline_top_level(snac *snac, const xs_list *list);
168void timeline_add_mark(snac *user);
169
166xs_list *local_list(snac *snac, int max); 170xs_list *local_list(snac *snac, int max);
167xs_str *instance_index_fn(void); 171xs_str *instance_index_fn(void);
168xs_list *timeline_instance_list(int skip, int show); 172xs_list *timeline_instance_list(int skip, int show);
diff --git a/utils.c b/utils.c
index 4429437..ae1c1b6 100644
--- a/utils.c
+++ b/utils.c
@@ -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"
diff --git a/xs.h b/xs.h
index 05d84f5..b53885e 100644
--- a/xs.h
+++ b/xs.h
@@ -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
16typedef enum { 17typedef 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);
142void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); 143void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size);
143 144
144unsigned int xs_hash_func(const char *data, int size); 145unsigned int xs_hash_func(const char *data, int size);
146uint64_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)
989xs_list *xs_split_n(const char *str, const char *sep, int times) 991xs_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
1507uint64_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 */
diff --git a/xs_io.h b/xs_io.h
index 110b0eb..8fc5d6f 100644
--- a/xs_io.h
+++ b/xs_io.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;
diff --git a/xs_match.h b/xs_match.h
index 0b89ac8..76c1bf3 100644
--- a/xs_match.h
+++ b/xs_match.h
@@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec)
24retry: 24retry:
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
diff --git a/xs_url.h b/xs_url.h
index 3c24736..8e2e243 100644
--- a/xs_url.h
+++ b/xs_url.h
@@ -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 */