summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar shtrophic2025-02-15 14:37:36 +0100
committerGravatar shtrophic2025-02-15 14:37:36 +0100
commit7611a6bee4bcbad2f1710aafa99aba730e5cf995 (patch)
tree33ab7bee30379e16f6869b2efda5494be8aeb858
parentenforce tls when supported && add tests (diff)
parentVersion 2.72 RELEASED. (diff)
downloadpenes-snac2-7611a6bee4bcbad2f1710aafa99aba730e5cf995.tar.gz
penes-snac2-7611a6bee4bcbad2f1710aafa99aba730e5cf995.tar.xz
penes-snac2-7611a6bee4bcbad2f1710aafa99aba730e5cf995.zip
Merge tag '2.72' into curl-smtp
Version 2.72 RELEASED.
-rw-r--r--README.md2
-rw-r--r--RELEASE_NOTES.md38
-rw-r--r--TODO.md8
-rw-r--r--activitypub.c2
-rw-r--r--data.c82
-rw-r--r--doc/snac.15
-rw-r--r--doc/snac.52
-rw-r--r--doc/snac.86
-rw-r--r--doc/style.css1
-rw-r--r--format.c6
-rw-r--r--html.c393
-rw-r--r--httpd.c3
-rw-r--r--main.c49
-rw-r--r--mastoapi.c9
-rw-r--r--snac.c2
-rw-r--r--snac.h10
-rw-r--r--utils.c3
-rw-r--r--xs.h36
-rw-r--r--xs_fcgi.h3
-rw-r--r--xs_html.h4
-rw-r--r--xs_httpd.h54
-rw-r--r--xs_io.h7
-rw-r--r--xs_json.h6
-rw-r--r--xs_match.h7
-rw-r--r--xs_openssl.h2
-rw-r--r--xs_socket.h2
-rw-r--r--xs_url.h117
-rw-r--r--xs_version.h2
28 files changed, 636 insertions, 225 deletions
diff --git a/README.md b/README.md
index de11b98..b01308e 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,8 @@ 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/).
111- [Caching Snac Proxied Media With Nginx (by Stefano Marinelli)](https://it-notes.dragas.net/2025/02/08/caching-snac-proxied-media-with-nginx/).
110 112
111## Incredibly awesome CSS themes for snac 113## Incredibly awesome CSS themes for snac
112 114
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 23a2a17..f8566a2 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,43 @@
1# Release Notes 1# Release Notes
2 2
3## 2.72
4
5Each post can have more than one attachment from the web UI. The maximum number can be configured in `server.json` via the `max_attachments` value (default: 4).
6
7Each notification includes a link labelled `Context`, that leads to a page with the full conversation tree the post is a part of.
8
9Each followed hashtag has now a directly accesible link.
10
11Fixed a search bug (some matches were missed).
12
13Fixed more crashes (contributed by inz).
14
15Fixed link detection in posts (contributed by inz).
16
17Allow multiple editors for command-line posts (contributed by inz).
18
19Separated maximum and default timeline entry count, allowing larger timelines to be requested without having to increase the default (contributed by lxo).
20
21Turned message date into a link to the local post, so that it can be loaded into a separate tab for interacting with (contributed by lxo).
22
23Special thanks to fellow developer inz for bringing my attention to code places where I should have been more careful.
24
25## 2.71
26
27Fixed memory leak (contributed by inz).
28
29Fixed crash.
30
31## 2.70
32
33Notifications 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).
34
35New command-line option `unmute` to, well, no-longer-mute an actor.
36
37The private timeline now includes an approximate mark between new posts and "already seen" ones.
38
39Fixed a spurious 404 error in the instance root URL for some configurations.
40
3## 2.69 "Yin/Yang of Love" 41## 2.69 "Yin/Yang of Love"
4 42
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. 43Added 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..bdb860c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -14,14 +14,12 @@ 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). 17Each notification should show a link to the full thread, to see it in context.
18 18
19The instance timeline should also show boosts from users. 19The instance timeline should also show boosts from users.
20 20
21Mastoapi: implement /v1/conversations. 21Mastoapi: implement /v1/conversations.
22 22
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). 23Track '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 24
27Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md 25Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
@@ -365,3 +363,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). 363Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100).
366 364
367Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). 365Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100).
366
367Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100).
368
369Add 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 f3b2bae..643baba 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -3072,7 +3072,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
3072 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); 3072 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
3073 3073
3074 /* get the public outbox or the pinned list */ 3074 /* get the public outbox or the pinned list */
3075 xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt) : pinned_list(&snac); 3075 xs *elems = *p_path == 'o' ? timeline_simple_list(&snac, "public", 0, cnt, NULL) : pinned_list(&snac);
3076 3076
3077 xs_list_foreach(elems, v) { 3077 xs_list_foreach(elems, v) {
3078 xs *i = NULL; 3078 xs *i = NULL;
diff --git a/data.c b/data.c
index b148ac7..6995611 100644
--- a/data.c
+++ b/data.c
@@ -1399,11 +1399,13 @@ void timeline_update_indexes(snac *snac, const char *id)
1399 if (valid_status(object_get(id, &msg))) { 1399 if (valid_status(object_get(id, &msg))) {
1400 /* if its ours and is public, also store in public */ 1400 /* if its ours and is public, also store in public */
1401 if (is_msg_public(msg)) { 1401 if (is_msg_public(msg)) {
1402 object_user_cache_add(snac, id, "public"); 1402 if (object_user_cache_add(snac, id, "public") >= 0) {
1403 1403 /* also add it to the instance public timeline */
1404 /* also add it to the instance public timeline */ 1404 xs *ipt = xs_fmt("%s/public.idx", srv_basedir);
1405 xs *ipt = xs_fmt("%s/public.idx", srv_basedir); 1405 index_add(ipt, id);
1406 index_add(ipt, id); 1406 }
1407 else
1408 srv_debug(1, xs_fmt("Not added to public instance index %s", id));
1407 } 1409 }
1408 } 1410 }
1409 } 1411 }
@@ -1487,16 +1489,28 @@ xs_str *user_index_fn(snac *user, const char *idx_name)
1487} 1489}
1488 1490
1489 1491
1490xs_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)
1491/* returns a timeline (with all entries) */ 1493/* returns a timeline (with all entries) */
1492{ 1494{
1493 xs *idx = user_index_fn(user, idx_name); 1495 xs *idx = user_index_fn(user, idx_name);
1494 1496
1495 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;
1496} 1510}
1497 1511
1498 1512
1499xs_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)
1500/* returns a timeline (only top level entries) */ 1514/* returns a timeline (only top level entries) */
1501{ 1515{
1502 int c_max; 1516 int c_max;
@@ -1508,12 +1522,33 @@ xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show)
1508 if (show > c_max) 1522 if (show > c_max)
1509 show = c_max; 1523 show = c_max;
1510 1524
1511 xs *list = timeline_simple_list(snac, idx_name, skip, show); 1525 xs *list = timeline_simple_list(snac, idx_name, skip, show, more);
1512 1526
1513 return timeline_top_level(snac, list); 1527 return timeline_top_level(snac, list);
1514} 1528}
1515 1529
1516 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
1517xs_str *instance_index_fn(void) 1552xs_str *instance_index_fn(void)
1518{ 1553{
1519 return xs_fmt("%s/public.idx", srv_basedir); 1554 return xs_fmt("%s/public.idx", srv_basedir);
@@ -1524,8 +1559,17 @@ xs_list *timeline_instance_list(int skip, int show)
1524/* returns the timeline for the full instance */ 1559/* returns the timeline for the full instance */
1525{ 1560{
1526 xs *idx = instance_index_fn(); 1561 xs *idx = instance_index_fn();
1562 xs *lst = index_list_desc(idx, skip, show);
1527 1563
1528 return index_list_desc(idx, skip, show); 1564 /* make the list unique */
1565 xs_set rep;
1566 xs_set_init(&rep);
1567 const char *md5;
1568
1569 xs_list_foreach(lst, md5)
1570 xs_set_add(&rep, md5);
1571
1572 return xs_set_result(&rep);
1529} 1573}
1530 1574
1531 1575
@@ -2557,7 +2601,7 @@ xs_list *inbox_list(void)
2557 if ((f = fopen(v, "r")) != NULL) { 2601 if ((f = fopen(v, "r")) != NULL) {
2558 xs *line = xs_readline(f); 2602 xs *line = xs_readline(f);
2559 2603
2560 if (line) { 2604 if (line && *line) {
2561 line = xs_strip_i(line); 2605 line = xs_strip_i(line);
2562 ibl = xs_list_append(ibl, line); 2606 ibl = xs_list_append(ibl, line);
2563 } 2607 }
@@ -2698,9 +2742,9 @@ xs_list *content_search(snac *user, const char *regex,
2698 const char *md5s[3] = {0}; 2742 const char *md5s[3] = {0};
2699 int c[3] = {0}; 2743 int c[3] = {0};
2700 2744
2701 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL); /* public */ 2745 tls[0] = timeline_simple_list(user, "public", 0, XS_ALL, NULL); /* public */
2702 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */ 2746 tls[1] = timeline_instance_list(0, XS_ALL); /* instance */
2703 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 */
2704 2748
2705 /* first positioning */ 2749 /* first positioning */
2706 for (int n = 0; n < 3; n++) 2750 for (int n = 0; n < 3; n++)
@@ -2722,7 +2766,17 @@ xs_list *content_search(snac *user, const char *regex,
2722 for (int n = 0; n < 3; n++) { 2766 for (int n = 0; n < 3; n++) {
2723 if (md5s[n] != NULL) { 2767 if (md5s[n] != NULL) {
2724 xs *fn = _object_fn_by_md5(md5s[n], "content_search"); 2768 xs *fn = _object_fn_by_md5(md5s[n], "content_search");
2725 double mt = mtime(fn); 2769 double mt;
2770
2771 while ((mt = mtime(fn)) == 0 && md5s[n] != NULL) {
2772 /* object is not here: move to the next one */
2773 if (xs_list_next(tls[n], &md5s[n], &c[n])) {
2774 xs_free(fn);
2775 fn = _object_fn_by_md5(md5s[n], "content_search_2");
2776 }
2777 else
2778 md5s[n] = NULL;
2779 }
2726 2780
2727 if (mt > mtime) { 2781 if (mt > mtime) {
2728 newest = n; 2782 newest = 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/snac.8 b/doc/snac.8
index e228495..7e3213b 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -154,6 +154,8 @@ to those servers that went timeout in the previous retry. If you want to
154give slow servers a chance to receive your messages, you can increase this 154give slow servers a chance to receive your messages, you can increase this
155value (but also take into account that processing the queue will take longer 155value (but also take into account that processing the queue will take longer
156while waiting for these molasses to respond). 156while waiting for these molasses to respond).
157.It Ic def_timeline_entries
158This is the default timeline entries shown in the web interface.
157.It Ic max_timeline_entries 159.It Ic max_timeline_entries
158This is the maximum timeline entries shown in the web interface. 160This is the maximum timeline entries shown in the web interface.
159.It Ic timeline_purge_days 161.It Ic timeline_purge_days
@@ -205,6 +207,8 @@ The email address of the instance administrator (optional).
205The user name of the instance administrator (optional). 207The user name of the instance administrator (optional).
206.It Ic short_description 208.It Ic short_description
207A textual short description about the instance (optional). 209A textual short description about the instance (optional).
210.It Ic short_description_raw
211Whether to interpret short_descript as raw string or convert to HTML (optional).
208.It Ic fastcgi 212.It Ic fastcgi
209If set to true, 213If set to true,
210.Nm 214.Nm
@@ -256,6 +260,8 @@ need at least a Linux kernel version 5.13.0.
256.It Ic max_public_entries 260.It Ic max_public_entries
257The maximum number of entries (posts) to be returned in user RSS feeds and outboxes 261The maximum number of entries (posts) to be returned in user RSS feeds and outboxes
258(default: 20). 262(default: 20).
263.It Ic max_attachments
264The maximum number of attachments per post (default: 4).
259.El 265.El
260.Pp 266.Pp
261You must restart the server to make effective these changes. 267You must restart the server to make effective these changes.
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/format.c b/format.c
index e5934b8..4c8ed29 100644
--- a/format.c
+++ b/format.c
@@ -78,6 +78,8 @@ xs_dict *emojis(void)
78 return d; 78 return d;
79} 79}
80 80
81/* Non-whitespace without trailing comma, period or closing paren */
82#define NOSPACE "([^[:space:],.)]+|[,.)]+[^[:space:],.)])+"
81 83
82static xs_str *format_line(const char *line, xs_list **attach) 84static xs_str *format_line(const char *line, xs_list **attach)
83/* formats a line */ 85/* formats a line */
@@ -96,8 +98,8 @@ static xs_str *format_line(const char *line, xs_list **attach)
96 "__[^_]+__" "|" //anzu 98 "__[^_]+__" "|" //anzu
97 "!\\[[^]]+\\]\\([^\\)]+\\)" "|" 99 "!\\[[^]]+\\]\\([^\\)]+\\)" "|"
98 "\\[[^]]+\\]\\([^\\)]+\\)" "|" 100 "\\[[^]]+\\]\\([^\\)]+\\)" "|"
99 "[a-z]+:/" "/[^[:space:]]+" "|" 101 "[a-z]+:/" "/" NOSPACE "|"
100 "(mailto|xmpp):[^@[:space:]]+@[^[:space:]]+" 102 "(mailto|xmpp):[^@[:space:]]+@" NOSPACE
101 ")"); 103 ")");
102 int n = 0; 104 int n = 0;
103 105
diff --git a/html.c b/html.c
index 3fd1e4b..e742e94 100644
--- a/html.c
+++ b/html.c
@@ -13,6 +13,7 @@
13#include "xs_html.h" 13#include "xs_html.h"
14#include "xs_curl.h" 14#include "xs_curl.h"
15#include "xs_unicode.h" 15#include "xs_unicode.h"
16#include "xs_url.h"
16 17
17#include "snac.h" 18#include "snac.h"
18 19
@@ -115,7 +116,8 @@ xs_str *actor_name(xs_dict *actor, const char *proxy)
115 116
116xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, 117xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
117 const char *udate, const char *url, int priv, 118 const char *udate, const char *url, int priv,
118 int in_people, const char *proxy, const char *lang) 119 int in_people, const char *proxy, const char *lang,
120 const char *md5)
119{ 121{
120 xs_html *actor_icon = xs_html_tag("p", NULL); 122 xs_html *actor_icon = xs_html_tag("p", NULL);
121 123
@@ -224,12 +226,31 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
224 if (xs_is_string(lang)) 226 if (xs_is_string(lang))
225 date_title = xs_str_cat(date_title, " (", lang, ")"); 227 date_title = xs_str_cat(date_title, " (", lang, ")");
226 228
229 xs_html *date_text = xs_html_text(date_label);
230
231 if (user && md5) {
232 xs *lpost_url = xs_fmt("%s/admin/p/%s#%s_entry",
233 user->actor, md5, md5);
234 date_text = xs_html_tag("a",
235 xs_html_attr("href", lpost_url),
236 xs_html_attr("class", "snac-pubdate"),
237 date_text);
238 }
239 else if (user && url) {
240 xs *lpost_url = xs_fmt("%s/admin?q=%s",
241 user->actor, xs_url_enc(url));
242 date_text = xs_html_tag("a",
243 xs_html_attr("href", lpost_url),
244 xs_html_attr("class", "snac-pubdate"),
245 date_text);
246 }
247
227 xs_html_add(actor_icon, 248 xs_html_add(actor_icon,
228 xs_html_text(" "), 249 xs_html_text(" "),
229 xs_html_tag("time", 250 xs_html_tag("time",
230 xs_html_attr("class", "dt-published snac-pubdate"), 251 xs_html_attr("class", "dt-published snac-pubdate"),
231 xs_html_attr("title", date_title), 252 xs_html_attr("title", date_title),
232 xs_html_text(date_label))); 253 date_text));
233 } 254 }
234 255
235 { 256 {
@@ -261,7 +282,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
261} 282}
262 283
263 284
264xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy) 285xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy, const char *md5)
265{ 286{
266 xs *actor = NULL; 287 xs *actor = NULL;
267 xs_html *actor_icon = NULL; 288 xs_html *actor_icon = NULL;
@@ -292,7 +313,7 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con
292 else 313 else
293 lang = NULL; 314 lang = NULL;
294 315
295 actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang); 316 actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang, md5);
296 } 317 }
297 318
298 return actor_icon; 319 return actor_icon;
@@ -306,7 +327,7 @@ xs_html *html_note(snac *user, const char *summary,
306 const xs_val *cw_yn, const char *cw_text, 327 const xs_val *cw_yn, const char *cw_text,
307 const xs_val *mnt_only, const char *redir, 328 const xs_val *mnt_only, const char *redir,
308 const char *in_reply_to, int poll, 329 const char *in_reply_to, int poll,
309 const char *att_file, const char *att_alt_text, 330 const xs_list *att_files, const xs_list *att_alt_texts,
310 int is_draft) 331 int is_draft)
311/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */ 332/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
312{ 333{
@@ -411,30 +432,71 @@ xs_html *html_note(snac *user, const char *summary,
411 xs_html_tag("p", NULL), 432 xs_html_tag("p", NULL),
412 att = xs_html_tag("details", 433 att = xs_html_tag("details",
413 xs_html_tag("summary", 434 xs_html_tag("summary",
414 xs_html_text(L("Attachment..."))), 435 xs_html_text(L("Attachments..."))),
415 xs_html_tag("p", NULL))); 436 xs_html_tag("p", NULL)));
416 437
417 if (att_file && *att_file) 438 int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
439 int att_n = 0;
440
441 /* fields for the currently existing attachments */
442 if (xs_is_list(att_files) && xs_is_list(att_alt_texts)) {
443 while (att_n < max_attachments) {
444 const char *att_file = xs_list_get(att_files, att_n);
445 const char *att_alt_text = xs_list_get(att_alt_texts, att_n);
446
447 if (!xs_is_string(att_file) || !xs_is_string(att_alt_text))
448 break;
449
450 xs *att_lbl = xs_fmt("attach_url_%d", att_n);
451 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
452
453 if (att_n)
454 xs_html_add(att,
455 xs_html_sctag("br", NULL));
456
457 xs_html_add(att,
458 xs_html_text(L("File:")),
459 xs_html_sctag("input",
460 xs_html_attr("type", "text"),
461 xs_html_attr("name", att_lbl),
462 xs_html_attr("title", L("Clear this field to delete the attachment")),
463 xs_html_attr("value", att_file)));
464
465 xs_html_add(att,
466 xs_html_text(" "),
467 xs_html_sctag("input",
468 xs_html_attr("type", "text"),
469 xs_html_attr("name", alt_lbl),
470 xs_html_attr("value", att_alt_text),
471 xs_html_attr("placeholder", L("Attachment description"))));
472
473 att_n++;
474 }
475 }
476
477 /* the rest of possible attachments */
478 while (att_n < max_attachments) {
479 xs *att_lbl = xs_fmt("attach_%d", att_n);
480 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
481
482 if (att_n)
483 xs_html_add(att,
484 xs_html_sctag("br", NULL));
485
418 xs_html_add(att, 486 xs_html_add(att,
419 xs_html_text(L("File:")),
420 xs_html_sctag("input", 487 xs_html_sctag("input",
421 xs_html_attr("type", "text"), 488 xs_html_attr("type", "file"),
422 xs_html_attr("name", "attach_url"), 489 xs_html_attr("name", att_lbl)));
423 xs_html_attr("title", L("Clear this field to delete the attachment")), 490
424 xs_html_attr("value", att_file)));
425 else
426 xs_html_add(att, 491 xs_html_add(att,
492 xs_html_text(" "),
427 xs_html_sctag("input", 493 xs_html_sctag("input",
428 xs_html_attr("type", "file"), 494 xs_html_attr("type", "text"),
429 xs_html_attr("name", "attach"))); 495 xs_html_attr("name", alt_lbl),
496 xs_html_attr("placeholder", L("Attachment description"))));
430 497
431 xs_html_add(att, 498 att_n++;
432 xs_html_text(" "), 499 }
433 xs_html_sctag("input",
434 xs_html_attr("type", "text"),
435 xs_html_attr("name", "alt_text"),
436 xs_html_attr("value", att_alt_text),
437 xs_html_attr("placeholder", L("Attachment description"))));
438 500
439 /* add poll controls */ 501 /* add poll controls */
440 if (poll) { 502 if (poll) {
@@ -553,10 +615,11 @@ xs_html *html_instance_head(void)
553 615
554static xs_html *html_instance_body(void) 616static xs_html *html_instance_body(void)
555{ 617{
556 const char *host = xs_dict_get(srv_config, "host"); 618 const char *host = xs_dict_get(srv_config, "host");
557 const char *sdesc = xs_dict_get(srv_config, "short_description"); 619 const char *sdesc = xs_dict_get(srv_config, "short_description");
558 const char *email = xs_dict_get(srv_config, "admin_email"); 620 const char *sdescraw = xs_dict_get(srv_config, "short_description_raw");
559 const char *acct = xs_dict_get(srv_config, "admin_account"); 621 const char *email = xs_dict_get(srv_config, "admin_email");
622 const char *acct = xs_dict_get(srv_config, "admin_account");
560 623
561 xs *blurb = xs_replace(snac_blurb, "%host%", host); 624 xs *blurb = xs_replace(snac_blurb, "%host%", host);
562 625
@@ -569,12 +632,21 @@ static xs_html *html_instance_body(void)
569 dl = xs_html_tag("dl", NULL))); 632 dl = xs_html_tag("dl", NULL)));
570 633
571 if (sdesc && *sdesc) { 634 if (sdesc && *sdesc) {
572 xs_html_add(dl, 635 if (!xs_is_null(sdescraw) && xs_type(sdescraw) == XSTYPE_TRUE) {
573 xs_html_tag("di", 636 xs_html_add(dl,
574 xs_html_tag("dt", 637 xs_html_tag("di",
575 xs_html_text(L("Site description"))), 638 xs_html_tag("dt",
576 xs_html_tag("dd", 639 xs_html_text(L("Site description"))),
577 xs_html_text(sdesc)))); 640 xs_html_tag("dd",
641 xs_html_raw(sdesc))));
642 } else {
643 xs_html_add(dl,
644 xs_html_tag("di",
645 xs_html_tag("dt",
646 xs_html_text(L("Site description"))),
647 xs_html_tag("dd",
648 xs_html_text(sdesc))));
649 }
578 } 650 }
579 if (email && *email) { 651 if (email && *email) {
580 xs *mailto = xs_fmt("mailto:%s", email); 652 xs *mailto = xs_fmt("mailto:%s", email);
@@ -1028,7 +1100,7 @@ xs_html *html_top_controls(snac *snac)
1028 NULL, NULL, 1100 NULL, NULL,
1029 xs_stock(XSTYPE_FALSE), "", 1101 xs_stock(XSTYPE_FALSE), "",
1030 xs_stock(XSTYPE_FALSE), NULL, 1102 xs_stock(XSTYPE_FALSE), NULL,
1031 NULL, 1, "", "", 0), 1103 NULL, 1, NULL, NULL, 0),
1032 1104
1033 /** operations **/ 1105 /** operations **/
1034 xs_html_tag("details", 1106 xs_html_tag("details",
@@ -1600,17 +1672,22 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1600 xs *form_id = xs_fmt("%s_edit_form", md5); 1672 xs *form_id = xs_fmt("%s_edit_form", md5);
1601 xs *redir = xs_fmt("%s_entry", md5); 1673 xs *redir = xs_fmt("%s_entry", md5);
1602 1674
1603 const char *att_file = ""; 1675 xs *att_files = xs_list_new();
1604 const char *att_alt_text = ""; 1676 xs *att_alt_texts = xs_list_new();
1677
1605 const xs_list *att_list = xs_dict_get(msg, "attachment"); 1678 const xs_list *att_list = xs_dict_get(msg, "attachment");
1606 1679
1607 /* does it have an attachment? */ 1680 if (xs_is_list(att_list)) {
1608 if (xs_type(att_list) == XSTYPE_LIST && xs_list_len(att_list)) { 1681 const xs_dict *d;
1609 const xs_dict *d = xs_list_get(att_list, 0); 1682
1683 xs_list_foreach(att_list, d) {
1684 const char *att_file = xs_dict_get(d, "url");
1685 const char *att_alt_text = xs_dict_get(d, "name");
1610 1686
1611 if (xs_type(d) == XSTYPE_DICT) { 1687 if (xs_is_string(att_file) && xs_is_string(att_alt_text)) {
1612 att_file = xs_dict_get_def(d, "url", ""); 1688 att_files = xs_list_append(att_files, att_file);
1613 att_alt_text = xs_dict_get_def(d, "name", ""); 1689 att_alt_texts = xs_list_append(att_alt_texts, att_alt_text);
1690 }
1614 } 1691 }
1615 } 1692 }
1616 1693
@@ -1622,7 +1699,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1622 id, NULL, 1699 id, NULL,
1623 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1700 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1624 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1701 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1625 NULL, 0, att_file, att_alt_text, is_draft(snac, id))), 1702 NULL, 0, att_files, att_alt_texts, is_draft(snac, id))),
1626 xs_html_tag("p", NULL)); 1703 xs_html_tag("p", NULL));
1627 } 1704 }
1628 1705
@@ -1641,7 +1718,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1641 NULL, NULL, 1718 NULL, NULL,
1642 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1719 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1643 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1720 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1644 id, 0, "", "", 0)), 1721 id, 0, NULL, NULL, 0)),
1645 xs_html_tag("p", NULL)); 1722 xs_html_tag("p", NULL));
1646 } 1723 }
1647 1724
@@ -1696,7 +1773,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1696 xs_html_tag("div", 1773 xs_html_tag("div",
1697 xs_html_attr("class", "snac-origin"), 1774 xs_html_attr("class", "snac-origin"),
1698 xs_html_text(L("follows you"))), 1775 xs_html_text(L("follows you"))),
1699 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy))); 1776 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL)));
1700 } 1777 }
1701 else 1778 else
1702 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) { 1779 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
@@ -1877,7 +1954,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1877 } 1954 }
1878 1955
1879 xs_html_add(post_header, 1956 xs_html_add(post_header,
1880 html_msg_icon(read_only ? NULL : user, actor, msg, proxy)); 1957 html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5));
1881 1958
1882 /** post content **/ 1959 /** post content **/
1883 1960
@@ -2022,16 +2099,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2022 const char *name = xs_dict_get(v, "name"); 2099 const char *name = xs_dict_get(v, "name");
2023 const xs_dict *replies = xs_dict_get(v, "replies"); 2100 const xs_dict *replies = xs_dict_get(v, "replies");
2024 2101
2025 if (name && replies) { 2102 if (xs_is_string(name) && xs_is_dict(replies)) {
2026 char *ti = (char *)xs_number_str(xs_dict_get(replies, "totalItems")); 2103 const char *ti = xs_number_str(xs_dict_get(replies, "totalItems"));
2027 2104
2028 xs_html_add(poll_result, 2105 if (xs_is_string(ti))
2029 xs_html_tag("tr", 2106 xs_html_add(poll_result,
2030 xs_html_tag("td", 2107 xs_html_tag("tr",
2031 xs_html_text(name), 2108 xs_html_tag("td",
2032 xs_html_text(":")), 2109 xs_html_text(name),
2033 xs_html_tag("td", 2110 xs_html_text(":")),
2034 xs_html_text(ti)))); 2111 xs_html_tag("td",
2112 xs_html_text(ti))));
2035 } 2113 }
2036 } 2114 }
2037 2115
@@ -2629,6 +2707,29 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2629 xs_html_attr("title", L("Post drafts")), 2707 xs_html_attr("title", L("Post drafts")),
2630 xs_html_text("drafts")))); 2708 xs_html_text("drafts"))));
2631 } 2709 }
2710
2711 /* the list of followed hashtags */
2712 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
2713
2714 if (xs_is_list(followed_hashtags) && xs_list_len(followed_hashtags)) {
2715 xs_html *loht = xs_html_tag("ul",
2716 xs_html_attr("class", "snac-list-of-lists"));
2717 xs_html_add(body, loht);
2718
2719 const char *ht;
2720
2721 xs_list_foreach(followed_hashtags, ht) {
2722 xs *url = xs_fmt("%s/admin?q=%s", user->actor, ht);
2723 url = xs_replace_i(url, "#", "%23");
2724
2725 xs_html_add(loht,
2726 xs_html_tag("li",
2727 xs_html_tag("a",
2728 xs_html_attr("href", url),
2729 xs_html_attr("class", "snac-list-link"),
2730 xs_html_text(ht))));
2731 }
2732 }
2632 } 2733 }
2633 2734
2634 xs_html_add(body, 2735 xs_html_add(body,
@@ -2648,10 +2749,32 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2648 xs_html_add(body, 2749 xs_html_add(body,
2649 posts); 2750 posts);
2650 2751
2752 int mark_shown = 0;
2753
2651 while (xs_list_iter(&p, &v)) { 2754 while (xs_list_iter(&p, &v)) {
2652 xs *msg = NULL; 2755 xs *msg = NULL;
2653 int status; 2756 int status;
2654 2757
2758 /* "already seen" mark? */
2759 if (strcmp(v, MD5_ALREADY_SEEN_MARK) == 0) {
2760 if (skip == 0 && !mark_shown) {
2761 xs *s = xs_fmt("%s/admin", user->actor);
2762
2763 xs_html_add(posts,
2764 xs_html_tag("div",
2765 xs_html_attr("class", "snac-no-more-unseen-posts"),
2766 xs_html_text(L("No more unseen posts")),
2767 xs_html_text(" - "),
2768 xs_html_tag("a",
2769 xs_html_attr("href", s),
2770 xs_html_text(L("Back to top")))));
2771 }
2772
2773 mark_shown = 1;
2774
2775 continue;
2776 }
2777
2655 if (utl && user && !is_pinned_by_md5(user, v)) 2778 if (utl && user && !is_pinned_by_md5(user, v))
2656 status = timeline_get_by_md5(user, v, &msg); 2779 status = timeline_get_by_md5(user, v, &msg);
2657 else 2780 else
@@ -2788,7 +2911,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2788 xs_html_tag("div", 2911 xs_html_tag("div",
2789 xs_html_attr("class", "snac-post-header"), 2912 xs_html_attr("class", "snac-post-header"),
2790 html_actor_icon(snac, actor, xs_dict_get(actor, "published"), 2913 html_actor_icon(snac, actor, xs_dict_get(actor, "published"),
2791 NULL, NULL, 0, 1, proxy, NULL))); 2914 NULL, NULL, 0, 1, proxy, NULL, NULL)));
2792 2915
2793 /* content (user bio) */ 2916 /* content (user bio) */
2794 const char *c = xs_dict_get(actor, "summary"); 2917 const char *c = xs_dict_get(actor, "summary");
@@ -2885,7 +3008,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2885 NULL, actor_id, 3008 NULL, actor_id,
2886 xs_stock(XSTYPE_FALSE), "", 3009 xs_stock(XSTYPE_FALSE), "",
2887 xs_stock(XSTYPE_FALSE), NULL, 3010 xs_stock(XSTYPE_FALSE), NULL,
2888 NULL, 0, "", "", 0), 3011 NULL, 0, NULL, NULL, 0),
2889 xs_html_tag("p", NULL)); 3012 xs_html_tag("p", NULL));
2890 3013
2891 xs_html_add(snac_post, snac_controls); 3014 xs_html_add(snac_post, snac_controls);
@@ -2970,6 +3093,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
2970 xs_set rep; 3093 xs_set rep;
2971 xs_set_init(&rep); 3094 xs_set_init(&rep);
2972 3095
3096 /* dict to store previous notification labels */
3097 xs *admiration_labels = xs_dict_new();
3098
2973 const xs_str *v; 3099 const xs_str *v;
2974 3100
2975 xs_list_foreach(n_list, v) { 3101 xs_list_foreach(n_list, v) {
@@ -2983,6 +3109,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
2983 const char *utype = xs_dict_get(noti, "utype"); 3109 const char *utype = xs_dict_get(noti, "utype");
2984 const char *id = xs_dict_get(noti, "objid"); 3110 const char *id = xs_dict_get(noti, "objid");
2985 const char *date = xs_dict_get(noti, "date"); 3111 const char *date = xs_dict_get(noti, "date");
3112 const char *id2 = xs_dict_get_path(noti, "msg.id");
2986 xs *wrk = NULL; 3113 xs *wrk = NULL;
2987 3114
2988 if (xs_is_null(id)) 3115 if (xs_is_null(id))
@@ -2991,12 +3118,15 @@ xs_str *html_notifications(snac *user, int skip, int show)
2991 if (is_hidden(user, id)) 3118 if (is_hidden(user, id))
2992 continue; 3119 continue;
2993 3120
3121 if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
3122 continue;
3123
2994 object_get(id, &obj); 3124 object_get(id, &obj);
2995 3125
2996 const char *msg_id = NULL; 3126 const char *msg_id = NULL;
2997 3127
2998 if (xs_is_dict(obj) && (msg_id = xs_dict_get(obj, "id")) && xs_set_add(&rep, msg_id) != 1) 3128 if (xs_is_dict(obj))
2999 continue; 3129 msg_id = xs_dict_get(obj, "id");
3000 3130
3001 const char *actor_id = xs_dict_get(noti, "actor"); 3131 const char *actor_id = xs_dict_get(noti, "actor");
3002 xs *actor = NULL; 3132 xs *actor = NULL;
@@ -3030,9 +3160,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
3030 3160
3031 xs *s_date = xs_crop_i(xs_dup(date), 0, 10); 3161 xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
3032 3162
3033 xs_html *entry = xs_html_tag("div", 3163 xs_html *this_html_label = xs_html_container(
3034 xs_html_attr("class", "snac-post-with-desc"),
3035 xs_html_tag("p",
3036 xs_html_tag("b", 3164 xs_html_tag("b",
3037 xs_html_text(label), 3165 xs_html_text(label),
3038 xs_html_text(" by "), 3166 xs_html_text(" by "),
@@ -3043,13 +3171,45 @@ xs_str *html_notifications(snac *user, int skip, int show)
3043 xs_html_tag("time", 3171 xs_html_tag("time",
3044 xs_html_attr("class", "dt-published snac-pubdate"), 3172 xs_html_attr("class", "dt-published snac-pubdate"),
3045 xs_html_attr("title", date), 3173 xs_html_attr("title", date),
3046 xs_html_text(s_date)))); 3174 xs_html_text(s_date)));
3175
3176 xs_html *html_label = NULL;
3177
3178 if (xs_is_string(msg_id)) {
3179 const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id);
3180
3181 if (xs_type(prev_label) == XSTYPE_DATA) {
3182 /* there is a previous list of admiration labels! */
3183 xs_data_get(&html_label, prev_label);
3184
3185 xs_html_add(html_label,
3186 xs_html_sctag("br", NULL),
3187 this_html_label);
3188
3189 continue;
3190 }
3191 }
3192
3193 xs_html *entry = NULL;
3194
3195 html_label = xs_html_tag("p",
3196 this_html_label);
3197
3198 /* store in the admiration labels dict */
3199 xs *pl = xs_data_new(&html_label, sizeof(html_label));
3200
3201 if (xs_is_string(msg_id))
3202 admiration_labels = xs_dict_set(admiration_labels, msg_id, pl);
3203
3204 entry = xs_html_tag("div",
3205 xs_html_attr("class", "snac-post-with-desc"),
3206 html_label);
3047 3207
3048 if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { 3208 if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) {
3049 xs_html_add(entry, 3209 xs_html_add(entry,
3050 xs_html_tag("div", 3210 xs_html_tag("div",
3051 xs_html_attr("class", "snac-post"), 3211 xs_html_attr("class", "snac-post"),
3052 html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); 3212 html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
3053 } 3213 }
3054 else 3214 else
3055 if (strcmp(type, "Move") == 0) { 3215 if (strcmp(type, "Move") == 0) {
@@ -3063,18 +3223,23 @@ xs_str *html_notifications(snac *user, int skip, int show)
3063 xs_html_add(entry, 3223 xs_html_add(entry,
3064 xs_html_tag("div", 3224 xs_html_tag("div",
3065 xs_html_attr("class", "snac-post"), 3225 xs_html_attr("class", "snac-post"),
3066 html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); 3226 html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL)));
3067 } 3227 }
3068 } 3228 }
3069 } 3229 }
3070 else 3230 else
3071 if (obj != NULL) { 3231 if (obj != NULL) {
3072 xs *md5 = xs_md5_hex(id, strlen(id)); 3232 xs *md5 = xs_md5_hex(id, strlen(id));
3233 xs *ctxt = xs_fmt("%s/admin/p/%s#%s_entry", user->actor, md5, md5);
3073 3234
3074 xs_html *h = html_entry(user, obj, 0, 0, md5, 1); 3235 xs_html *h = html_entry(user, obj, 0, 0, md5, 1);
3075 3236
3076 if (h != NULL) { 3237 if (h != NULL) {
3077 xs_html_add(entry, 3238 xs_html_add(entry,
3239 xs_html_tag("p",
3240 xs_html_tag("a",
3241 xs_html_attr("href", ctxt),
3242 xs_html_text(L("Context")))),
3078 h); 3243 h);
3079 } 3244 }
3080 } 3245 }
@@ -3111,8 +3276,6 @@ xs_str *html_notifications(snac *user, int skip, int show)
3111 } 3276 }
3112 } 3277 }
3113 3278
3114 xs_set_free(&rep);
3115
3116 if (noti_new == NULL && noti_seen == NULL) 3279 if (noti_new == NULL && noti_seen == NULL)
3117 xs_html_add(body, 3280 xs_html_add(body,
3118 xs_html_tag("h2", 3281 xs_html_tag("h2",
@@ -3132,6 +3295,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
3132 xs_html_text(L("More..."))))); 3295 xs_html_text(L("More...")))));
3133 } 3296 }
3134 3297
3298 xs_set_free(&rep);
3299
3135 xs_html_add(body, 3300 xs_html_add(body,
3136 html_footer()); 3301 html_footer());
3137 3302
@@ -3232,7 +3397,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3232 cache = 0; 3397 cache = 0;
3233 3398
3234 int skip = 0; 3399 int skip = 0;
3235 int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); 3400 int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
3401 xs_dict_get_def(srv_config, "max_timeline_entries", "50")));
3236 int show = def_show; 3402 int show = def_show;
3237 3403
3238 if ((v = xs_dict_get(q_vars, "skip")) != NULL) 3404 if ((v = xs_dict_get(q_vars, "skip")) != NULL)
@@ -3277,21 +3443,17 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3277 } 3443 }
3278 else { 3444 else {
3279 xs *list = NULL; 3445 xs *list = NULL;
3280 xs *next = NULL; 3446 int more = 0;
3281 3447
3282 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) { 3448 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
3283 list = timeline_simple_list(&snac, "public", skip, show); 3449 list = timeline_simple_list(&snac, "public", skip, show, &more);
3284 next = timeline_simple_list(&snac, "public", skip + show, 1); 3450 else
3285 } 3451 list = timeline_list(&snac, "public", skip, show, &more);
3286 else {
3287 list = timeline_list(&snac, "public", skip, show);
3288 next = timeline_list(&snac, "public", skip + show, 1);
3289 }
3290 3452
3291 xs *pins = pinned_list(&snac); 3453 xs *pins = pinned_list(&snac);
3292 pins = xs_list_cat(pins, list); 3454 pins = xs_list_cat(pins, list);
3293 3455
3294 *body = html_timeline(&snac, pins, 1, skip, show, xs_list_len(next), NULL, "", 1, error); 3456 *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error);
3295 3457
3296 *b_size = strlen(*body); 3458 *b_size = strlen(*body);
3297 status = HTTP_STATUS_OK; 3459 status = HTTP_STATUS_OK;
@@ -3440,6 +3602,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3440 } 3602 }
3441 } 3603 }
3442 else { 3604 else {
3605 /** the private timeline **/
3443 double t = history_mtime(&snac, "timeline.html_"); 3606 double t = history_mtime(&snac, "timeline.html_");
3444 3607
3445 /* if enabled by admin, return a cached page if its timestamp is: 3608 /* if enabled by admin, return a cached page if its timestamp is:
@@ -3453,19 +3616,22 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3453 xs_dict_get(req, "if-none-match"), etag); 3616 xs_dict_get(req, "if-none-match"), etag);
3454 } 3617 }
3455 else { 3618 else {
3619 int more = 0;
3620
3456 snac_debug(&snac, 1, xs_fmt("building timeline")); 3621 snac_debug(&snac, 1, xs_fmt("building timeline"));
3457 3622
3458 xs *list = timeline_list(&snac, "private", skip, show); 3623 xs *list = timeline_list(&snac, "private", skip, show, &more);
3459 xs *next = timeline_list(&snac, "private", skip + show, 1);
3460 3624
3461 *body = html_timeline(&snac, list, 0, skip, show, 3625 *body = html_timeline(&snac, list, 0, skip, show,
3462 xs_list_len(next), NULL, "/admin", 1, error); 3626 more, NULL, "/admin", 1, error);
3463 3627
3464 *b_size = strlen(*body); 3628 *b_size = strlen(*body);
3465 status = HTTP_STATUS_OK; 3629 status = HTTP_STATUS_OK;
3466 3630
3467 if (save) 3631 if (save)
3468 history_add(&snac, "timeline.html_", *body, *b_size, etag); 3632 history_add(&snac, "timeline.html_", *body, *b_size, etag);
3633
3634 timeline_add_mark(&snac);
3469 } 3635 }
3470 } 3636 }
3471 } 3637 }
@@ -3481,7 +3647,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3481 const char *md5 = xs_list_get(l, -1); 3647 const char *md5 = xs_list_get(l, -1);
3482 3648
3483 if (md5 && *md5 && timeline_here(&snac, md5)) { 3649 if (md5 && *md5 && timeline_here(&snac, md5)) {
3484 xs *list = xs_list_append(xs_list_new(), md5); 3650 xs *list0 = xs_list_append(xs_list_new(), md5);
3651 xs *list = timeline_top_level(&snac, list0);
3485 3652
3486 *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error); 3653 *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error);
3487 *b_size = strlen(*body); 3654 *b_size = strlen(*body);
@@ -3665,7 +3832,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3665 3832
3666 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20")); 3833 int cnt = xs_number_get(xs_dict_get_def(srv_config, "max_public_entries", "20"));
3667 3834
3668 xs *elems = timeline_simple_list(&snac, "public", 0, cnt); 3835 xs *elems = timeline_simple_list(&snac, "public", 0, cnt, NULL);
3669 xs *bio = xs_dup(xs_dict_get(snac.config, "bio")); 3836 xs *bio = xs_dup(xs_dict_get(snac.config, "bio"));
3670 3837
3671 xs *rss_title = xs_fmt("%s (@%s@%s)", 3838 xs *rss_title = xs_fmt("%s (@%s@%s)",
@@ -3869,52 +4036,56 @@ int html_post_handler(const xs_dict *req, const char *q_path,
3869 /* post note */ 4036 /* post note */
3870 const xs_str *content = xs_dict_get(p_vars, "content"); 4037 const xs_str *content = xs_dict_get(p_vars, "content");
3871 const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to"); 4038 const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
3872 const xs_str *attach_url = xs_dict_get(p_vars, "attach_url");
3873 const xs_list *attach_file = xs_dict_get(p_vars, "attach");
3874 const xs_str *to = xs_dict_get(p_vars, "to"); 4039 const xs_str *to = xs_dict_get(p_vars, "to");
3875 const xs_str *sensitive = xs_dict_get(p_vars, "sensitive"); 4040 const xs_str *sensitive = xs_dict_get(p_vars, "sensitive");
3876 const xs_str *summary = xs_dict_get(p_vars, "summary"); 4041 const xs_str *summary = xs_dict_get(p_vars, "summary");
3877 const xs_str *edit_id = xs_dict_get(p_vars, "edit_id"); 4042 const xs_str *edit_id = xs_dict_get(p_vars, "edit_id");
3878 const xs_str *alt_text = xs_dict_get(p_vars, "alt_text");
3879 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only")); 4043 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
3880 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft")); 4044 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
3881 xs *attach_list = xs_list_new(); 4045 xs *attach_list = xs_list_new();
3882 4046
3883 /* default alt text */ 4047 /* iterate the attachments */
3884 if (xs_is_null(alt_text)) 4048 int max_attachments = xs_number_get(xs_dict_get_def(srv_config, "max_attachments", "4"));
3885 alt_text = "";
3886 4049
3887 /* is attach_url set? */ 4050 for (int att_n = 0; att_n < max_attachments; att_n++) {
3888 if (!xs_is_null(attach_url) && *attach_url != '\0') { 4051 xs *url_lbl = xs_fmt("attach_url_%d", att_n);
3889 xs *l = xs_list_new(); 4052 xs *att_lbl = xs_fmt("attach_%d", att_n);
4053 xs *alt_lbl = xs_fmt("alt_text_%d", att_n);
3890 4054
3891 l = xs_list_append(l, attach_url); 4055 const char *attach_url = xs_dict_get(p_vars, url_lbl);
3892 l = xs_list_append(l, alt_text); 4056 const xs_list *attach_file = xs_dict_get(p_vars, att_lbl);
4057 const char *alt_text = xs_dict_get_def(p_vars, alt_lbl, "");
3893 4058
3894 attach_list = xs_list_append(attach_list, l); 4059 if (xs_is_string(attach_url) && *attach_url != '\0') {
3895 } 4060 xs *l = xs_list_new();
3896 4061
3897 /* is attach_file set? */ 4062 l = xs_list_append(l, attach_url);
3898 if (!xs_is_null(attach_file) && xs_type(attach_file) == XSTYPE_LIST) { 4063 l = xs_list_append(l, alt_text);
3899 const char *fn = xs_list_get(attach_file, 0);
3900 4064
3901 if (*fn != '\0') { 4065 attach_list = xs_list_append(attach_list, l);
3902 char *ext = strrchr(fn, '.'); 4066 }
3903 xs *hash = xs_md5_hex(fn, strlen(fn)); 4067 else
3904 xs *id = xs_fmt("%s%s", hash, ext); 4068 if (xs_is_list(attach_file)) {
3905 xs *url = xs_fmt("%s/s/%s", snac.actor, id); 4069 const char *fn = xs_list_get(attach_file, 0);
3906 int fo = xs_number_get(xs_list_get(attach_file, 1));
3907 int fs = xs_number_get(xs_list_get(attach_file, 2));
3908 4070
3909 /* store */ 4071 if (xs_is_string(fn) && *fn != '\0') {
3910 static_put(&snac, id, payload + fo, fs); 4072 char *ext = strrchr(fn, '.');
4073 xs *hash = xs_md5_hex(fn, strlen(fn));
4074 xs *id = xs_fmt("%s%s", hash, ext);
4075 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
4076 int fo = xs_number_get(xs_list_get(attach_file, 1));
4077 int fs = xs_number_get(xs_list_get(attach_file, 2));
3911 4078
3912 xs *l = xs_list_new(); 4079 /* store */
4080 static_put(&snac, id, payload + fo, fs);
3913 4081
3914 l = xs_list_append(l, url); 4082 xs *l = xs_list_new();
3915 l = xs_list_append(l, alt_text);
3916 4083
3917 attach_list = xs_list_append(attach_list, l); 4084 l = xs_list_append(l, url);
4085 l = xs_list_append(l, alt_text);
4086
4087 attach_list = xs_list_append(attach_list, l);
4088 }
3918 } 4089 }
3919 } 4090 }
3920 4091
diff --git a/httpd.c b/httpd.c
index e0a36b6..5a38aff 100644
--- a/httpd.c
+++ b/httpd.c
@@ -219,7 +219,8 @@ int server_get_handler(xs_dict *req, const char *q_path,
219 if (xs_type(q_vars) == XSTYPE_DICT && (t = xs_dict_get(q_vars, "t"))) { 219 if (xs_type(q_vars) == XSTYPE_DICT && (t = xs_dict_get(q_vars, "t"))) {
220 /** search by tag **/ 220 /** search by tag **/
221 int skip = 0; 221 int skip = 0;
222 int show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); 222 int show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
223 xs_dict_get_def(srv_config, "max_timeline_entries", "50")));
223 const char *v; 224 const char *v;
224 225
225 if ((v = xs_dict_get(q_vars, "skip")) != NULL) 226 if ((v = xs_dict_get(q_vars, "skip")) != NULL)
diff --git a/main.c b/main.c
index a57adb5..1cd6580 100644
--- a/main.c
+++ b/main.c
@@ -11,6 +11,7 @@
11#include "snac.h" 11#include "snac.h"
12 12
13#include <sys/stat.h> 13#include <sys/stat.h>
14#include <sys/wait.h>
14 15
15int usage(void) 16int usage(void)
16{ 17{
@@ -49,6 +50,7 @@ int usage(void)
49 printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); 50 printf("unblock {basedir} {instance_url} Unblocks a full instance\n");
50 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); 51 printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n");
51 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); 52 printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n");
53 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"); 54 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"); 55 printf("search {basedir} {uid} {regex} Searches posts by content\n");
54 printf("export_csv {basedir} {uid} Exports data as CSV files\n"); 56 printf("export_csv {basedir} {uid} Exports data as CSV files\n");
@@ -446,6 +448,18 @@ int main(int argc, char *argv[])
446 return 0; 448 return 0;
447 } 449 }
448 450
451 if (strcmp(cmd, "unmute") == 0) { /** **/
452 if (is_muted(&snac, url)) {
453 unmute(&snac, url);
454
455 printf("%s unmuted\n", url);
456 }
457 else
458 printf("%s actor is not muted\n", url);
459
460 return 0;
461 }
462
449 if (strcmp(cmd, "search") == 0) { /** **/ 463 if (strcmp(cmd, "search") == 0) { /** **/
450 int to; 464 int to;
451 465
@@ -663,19 +677,25 @@ int main(int argc, char *argv[])
663 677
664 if (strcmp(url, "-e") == 0) { 678 if (strcmp(url, "-e") == 0) {
665 /* get the content from an editor */ 679 /* get the content from an editor */
680#define EDITOR "$EDITOR "
681 char cmd[] = EDITOR "/tmp/snac-XXXXXX";
666 FILE *f; 682 FILE *f;
667 683 int fd = mkstemp(cmd + strlen(EDITOR));
668 unlink("/tmp/snac-edit.txt"); 684
669 system("$EDITOR /tmp/snac-edit.txt"); 685 if (fd >= 0) {
670 686 int status = system(cmd);
671 if ((f = fopen("/tmp/snac-edit.txt", "r")) != NULL) { 687
672 content = xs_readall(f); 688 if (WIFEXITED(status) && WEXITSTATUS(status) == 0 && (f = fdopen(fd, "r")) != NULL) {
673 fclose(f); 689 content = xs_readall(f);
674 690 fclose(f);
675 unlink("/tmp/snac-edit.txt"); 691 unlink(cmd + strlen(EDITOR));
676 } 692 } else {
677 else { 693 printf("Nothing to send\n");
678 printf("Nothing to send\n"); 694 close(fd);
695 return 1;
696 }
697 } else {
698 fprintf(stderr, "Temp file creation failed\n");
679 return 1; 699 return 1;
680 } 700 }
681 } 701 }
@@ -687,6 +707,11 @@ int main(int argc, char *argv[])
687 else 707 else
688 content = xs_dup(url); 708 content = xs_dup(url);
689 709
710 if (!content || !*content) {
711 printf("Nothing to send\n");
712 return 1;
713 }
714
690 int scope = 0; 715 int scope = 0;
691 if (strcmp(cmd, "note_mention") == 0) 716 if (strcmp(cmd, "note_mention") == 0)
692 scope = 1; 717 scope = 1;
diff --git a/mastoapi.c b/mastoapi.c
index 54b4333..797a4da 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
@@ -2171,7 +2171,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
2171 2171
2172 { 2172 {
2173 xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32," 2173 xs *d11 = xs_json_loads("{\"characters_reserved_per_url\":32,"
2174 "\"max_characters\":100000,\"max_media_attachments\":8}"); 2174 "\"max_characters\":100000,\"max_media_attachments\":4}");
2175
2176 const xs_number *max_attachments = xs_dict_get(srv_config, "max_attachments");
2177 if (xs_type(max_attachments) == XSTYPE_NUMBER)
2178 d11 = xs_dict_set(d11, "max_media_attachments", max_attachments);
2179
2175 cfg = xs_dict_append(cfg, "statuses", d11); 2180 cfg = xs_dict_append(cfg, "statuses", d11);
2176 2181
2177 xs *d12 = xs_json_loads("{\"max_featured_tags\":0}"); 2182 xs *d12 = xs_json_loads("{\"max_featured_tags\":0}");
diff --git a/snac.c b/snac.c
index 80c7349..9f5b50e 100644
--- a/snac.c
+++ b/snac.c
@@ -69,7 +69,7 @@ xs_str *tid(int offset)
69 69
70 gettimeofday(&tv, NULL); 70 gettimeofday(&tv, NULL);
71 71
72 return xs_fmt("%10d.%06d", tv.tv_sec + offset, tv.tv_usec); 72 return xs_fmt("%010ld.%06ld", (long)tv.tv_sec + (long)offset, (long)tv.tv_usec);
73} 73}
74 74
75 75
diff --git a/snac.h b/snac.h
index f2220a3..5769b9b 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.69" 4#define VERSION "2.72"
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 dbf798a..37281e2 100644
--- a/utils.c
+++ b/utils.c
@@ -28,6 +28,7 @@ static const char *default_srv_config = "{"
28 "\"queue_timeout\": 6," 28 "\"queue_timeout\": 6,"
29 "\"queue_timeout_2\": 8," 29 "\"queue_timeout_2\": 8,"
30 "\"cssurls\": [\"\"]," 30 "\"cssurls\": [\"\"],"
31 "\"def_timeline_entries\": 50,"
31 "\"max_timeline_entries\": 50," 32 "\"max_timeline_entries\": 50,"
32 "\"timeline_purge_days\": 120," 33 "\"timeline_purge_days\": 120,"
33 "\"local_purge_days\": 0," 34 "\"local_purge_days\": 0,"
@@ -36,6 +37,7 @@ static const char *default_srv_config = "{"
36 "\"admin_account\": \"\"," 37 "\"admin_account\": \"\","
37 "\"title\": \"\"," 38 "\"title\": \"\","
38 "\"short_description\": \"\"," 39 "\"short_description\": \"\","
40 "\"short_description_raw\": false,"
39 "\"protocol\": \"https\"," 41 "\"protocol\": \"https\","
40 "\"fastcgi\": false" 42 "\"fastcgi\": false"
41 "}"; 43 "}";
@@ -72,6 +74,7 @@ static const char *default_css =
72 ".snac-list-of-lists { padding-left: 0; }\n" 74 ".snac-list-of-lists { padding-left: 0; }\n"
73 ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n" 75 ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n"
74 " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n" 76 " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n"
77 ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n"
75 "@media (prefers-color-scheme: dark) { \n" 78 "@media (prefers-color-scheme: dark) { \n"
76 " body, input, textarea { background-color: #000; color: #fff; }\n" 79 " body, input, textarea { background-color: #000; color: #fff; }\n"
77 " a { color: #7799dd }\n" 80 " 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_fcgi.h b/xs_fcgi.h
index 870e3e8..0b53dac 100644
--- a/xs_fcgi.h
+++ b/xs_fcgi.h
@@ -173,6 +173,9 @@ xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *fcgi_id)
173 xs *v = xs_str_new_sz((char *)&buf[offset], vsz); 173 xs *v = xs_str_new_sz((char *)&buf[offset], vsz);
174 offset += vsz; 174 offset += vsz;
175 175
176 if (!xs_is_string(k) || !xs_is_string(v))
177 continue;
178
176 cgi_vars = xs_dict_append(cgi_vars, k, v); 179 cgi_vars = xs_dict_append(cgi_vars, k, v);
177 180
178 if (strcmp(k, "REQUEST_METHOD") == 0) 181 if (strcmp(k, "REQUEST_METHOD") == 0)
diff --git a/xs_html.h b/xs_html.h
index 8331f93..29886c6 100644
--- a/xs_html.h
+++ b/xs_html.h
@@ -114,7 +114,7 @@ xs_html *xs_html_text(const char *content)
114 xs_html *a = XS_HTML_NEW(); 114 xs_html *a = XS_HTML_NEW();
115 115
116 a->type = XS_HTML_TEXT; 116 a->type = XS_HTML_TEXT;
117 a->content = xs_html_encode(content); 117 a->content = xs_is_string(content) ? xs_html_encode(content) : xs_str_new(NULL);
118 118
119 return a; 119 return a;
120} 120}
@@ -126,7 +126,7 @@ xs_html *xs_html_raw(const char *content)
126 xs_html *a = XS_HTML_NEW(); 126 xs_html *a = XS_HTML_NEW();
127 127
128 a->type = XS_HTML_TEXT; 128 a->type = XS_HTML_TEXT;
129 a->content = xs_dup(content); 129 a->content = xs_is_string(content) ? xs_dup(content) : xs_str_new(NULL);
130 130
131 return a; 131 return a;
132} 132}
diff --git a/xs_httpd.h b/xs_httpd.h
index cf0d811..4cc8263 100644
--- a/xs_httpd.h
+++ b/xs_httpd.h
@@ -15,41 +15,48 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
15{ 15{
16 xs *q_vars = NULL; 16 xs *q_vars = NULL;
17 xs *p_vars = NULL; 17 xs *p_vars = NULL;
18 xs *l1, *l2; 18 xs *l1;
19 const char *v; 19 const char *v;
20 char *saveptr;
20 21
21 xs_socket_timeout(fileno(f), 2.0, 0.0); 22 xs_socket_timeout(fileno(f), 2.0, 0.0);
22 23
23 /* read the first line and split it */ 24 /* read the first line and split it */
24 l1 = xs_strip_i(xs_readline(f)); 25 l1 = xs_strip_i(xs_readline(f));
25 l2 = xs_split(l1, " "); 26 char *raw_path;
27 const char *mtd;
28 const char *proto;
29
30 if (!(mtd = strtok_r(l1, " ", &saveptr)) ||
31 !(raw_path = strtok_r(NULL, " ", &saveptr)) ||
32 !(proto = strtok_r(NULL, " ", &saveptr)) ||
33 strtok_r(NULL, " ", &saveptr))
34 return NULL;
26 35
27 if (xs_list_len(l2) != 3) { 36 if (!xs_is_string(mtd) || !xs_is_string(raw_path) || !xs_is_string(proto))
28 /* error or timeout */
29 return NULL; 37 return NULL;
30 }
31 38
32 xs_dict *req = xs_dict_new(); 39 xs_dict *req = xs_dict_new();
33 40
34 req = xs_dict_append(req, "method", xs_list_get(l2, 0)); 41 req = xs_dict_append(req, "method", mtd);
35 req = xs_dict_append(req, "raw_path", xs_list_get(l2, 1)); 42 req = xs_dict_append(req, "raw_path", raw_path);
36 req = xs_dict_append(req, "proto", xs_list_get(l2, 2)); 43 req = xs_dict_append(req, "proto", proto);
37 44
38 { 45 {
39 /* split the path with its optional variables */ 46 char *q = strchr(raw_path, '?');
40 const xs_val *udp = xs_list_get(l2, 1);
41 xs *pnv = xs_split_n(udp, "?", 1);
42
43 /* store the path */
44 req = xs_dict_append(req, "path", xs_list_get(pnv, 0));
45 47
46 /* get the variables */ 48 /* get the variables */
47 q_vars = xs_url_vars(xs_list_get(pnv, 1)); 49 if (q) {
50 *q++ = '\0';
51 q_vars = xs_url_vars(q);
52 }
53 /* store the path */
54 req = xs_dict_append(req, "path", raw_path);
48 } 55 }
49 56
50 /* read the headers */ 57 /* read the headers */
51 for (;;) { 58 for (;;) {
52 xs *l, *p = NULL; 59 xs *l;
53 60
54 l = xs_strip_i(xs_readline(f)); 61 l = xs_strip_i(xs_readline(f));
55 62
@@ -58,11 +65,18 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
58 break; 65 break;
59 66
60 /* split header and content */ 67 /* split header and content */
61 p = xs_split_n(l, ": ", 1); 68 char *cnt = strchr(l, ':');
69 if (!cnt)
70 continue;
71
72 *cnt++ = '\0';
73 cnt += strspn(cnt, " \r\n\t\v\f");
74 l = xs_rstrip_chars_i(l, " \r\n\t\v\f");
75
76 if (!xs_is_string(cnt))
77 continue;
62 78
63 if (xs_list_len(p) == 2) 79 req = xs_dict_append(req, xs_tolower_i(l), cnt);
64 req = xs_dict_append(req, xs_tolower_i(
65 (xs_str *)xs_list_get(p, 0)), xs_list_get(p, 1));
66 } 80 }
67 81
68 xs_socket_timeout(fileno(f), 5.0, 0.0); 82 xs_socket_timeout(fileno(f), 5.0, 0.0);
diff --git a/xs_io.h b/xs_io.h
index 110b0eb..9c5018e 100644
--- a/xs_io.h
+++ b/xs_io.h
@@ -14,7 +14,7 @@ xs_val *xs_readall(FILE *f);
14xs_str *xs_readline(FILE *f) 14xs_str *xs_readline(FILE *f)
15/* reads a line from a file */ 15/* reads a line from a file */
16{ 16{
17 xs_str *s = NULL; 17 xs_str *s = xs_str_new(NULL);
18 18
19 errno = 0; 19 errno = 0;
20 20
@@ -22,12 +22,11 @@ xs_str *xs_readline(FILE *f)
22 if (!feof(f)) { 22 if (!feof(f)) {
23 int c; 23 int c;
24 24
25 s = xs_str_new(NULL);
26
27 while ((c = fgetc(f)) != EOF) { 25 while ((c = fgetc(f)) != EOF) {
28 unsigned char rc = c; 26 unsigned char rc = c;
29 27
30 s = xs_append_m(s, (char *)&rc, 1); 28 if (xs_is_string((char *)&rc))
29 s = xs_append_m(s, (char *)&rc, 1);
31 30
32 if (c == '\n') 31 if (c == '\n')
33 break; 32 break;
diff --git a/xs_json.h b/xs_json.h
index 660c277..d1b18e4 100644
--- a/xs_json.h
+++ b/xs_json.h
@@ -280,6 +280,12 @@ static xs_val *_xs_json_load_lexer(FILE *f, js_type *t)
280 else { 280 else {
281 char cc = c; 281 char cc = c;
282 v = xs_insert_m(v, offset, &cc, 1); 282 v = xs_insert_m(v, offset, &cc, 1);
283
284 if (!xs_is_string(v)) {
285 *t = JS_ERROR;
286 break;
287 }
288
283 offset++; 289 offset++;
284 } 290 }
285 } 291 }
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..37d2391 100644
--- a/xs_url.h
+++ b/xs_url.h
@@ -11,18 +11,57 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
11 11
12#ifdef XS_IMPLEMENTATION 12#ifdef XS_IMPLEMENTATION
13 13
14char *xs_url_dec_in(char *str, int qs)
15{
16 char *w = str;
17 char *r;
18
19 for (r = str; *r != '\0'; r++) {
20 switch (*r) {
21 case '%': {
22 unsigned hex;
23 if (!r[1] || !r[2])
24 return NULL;
25 if (sscanf(r + 1, "%2x", &hex) != 1)
26 return NULL;
27 *w++ = hex;
28 r += 2;
29 break;
30 }
31
32 case '+':
33 if (qs) {
34 *w++ = ' ';
35 break;
36 }
37 /* fall-through */
38 default:
39 *w++ = *r;
40 }
41 }
42
43 *w++ = '\0';
44 return str;
45}
46
14xs_str *xs_url_dec(const char *str) 47xs_str *xs_url_dec(const char *str)
15/* decodes an URL */ 48/* decodes an URL */
16{ 49{
17 xs_str *s = xs_str_new(NULL); 50 xs_str *s = xs_str_new(NULL);
18 51
19 while (*str) { 52 while (*str) {
53 if (!xs_is_string(str))
54 break;
55
20 if (*str == '%') { 56 if (*str == '%') {
21 unsigned int i; 57 unsigned int i;
22 58
23 if (sscanf(str + 1, "%02x", &i) == 1) { 59 if (sscanf(str + 1, "%02x", &i) == 1) {
24 unsigned char uc = i; 60 unsigned char uc = i;
25 61
62 if (!xs_is_string((char *)&uc))
63 break;
64
26 s = xs_append_m(s, (char *)&uc, 1); 65 s = xs_append_m(s, (char *)&uc, 1);
27 str += 2; 66 str += 2;
28 } 67 }
@@ -69,43 +108,45 @@ xs_dict *xs_url_vars(const char *str)
69 108
70 vars = xs_dict_new(); 109 vars = xs_dict_new();
71 110
72 if (str != NULL) { 111 if (xs_is_string(str)) {
73 /* split by arguments */ 112 xs *dup = xs_dup(str);
74 xs *args = xs_split(str, "&"); 113 char *k;
75 114 char *saveptr;
76 const xs_val *v; 115 for (k = strtok_r(dup, "&", &saveptr);
77 116 k;
78 xs_list_foreach(args, v) { 117 k = strtok_r(NULL, "&", &saveptr)) {
79 xs *dv = xs_url_dec(v); 118 char *v = strchr(k, '=');
80 xs *kv = xs_split_n(dv, "=", 1); 119 if (!v)
81 120 continue;
82 if (xs_list_len(kv) == 2) { 121 *v++ = '\0';
83 const char *key = xs_list_get(kv, 0); 122 k = xs_url_dec_in(k, 1);
84 const char *pv = xs_dict_get(vars, key); 123 v = xs_url_dec_in(v, 1);
85 124 if (!xs_is_string(k) || !xs_is_string(v))
86 if (!xs_is_null(pv)) { 125 continue;
87 /* there is a previous value: convert to a list and append */ 126
88 xs *vlist = NULL; 127 const char *pv = xs_dict_get(vars, k);
89 if (xs_type(pv) == XSTYPE_LIST) 128 if (!xs_is_null(pv)) {
90 vlist = xs_dup(pv); 129 /* there is a previous value: convert to a list and append */
91 else { 130 xs *vlist = NULL;
92 vlist = xs_list_new(); 131 if (xs_type(pv) == XSTYPE_LIST)
93 vlist = xs_list_append(vlist, pv); 132 vlist = xs_dup(pv);
94 }
95
96 vlist = xs_list_append(vlist, xs_list_get(kv, 1));
97 vars = xs_dict_set(vars, key, vlist);
98 }
99 else { 133 else {
100 /* ends with []? force to always be a list */ 134 vlist = xs_list_new();
101 if (xs_endswith(key, "[]")) { 135 vlist = xs_list_append(vlist, pv);
102 xs *vlist = xs_list_new(); 136 }
103 vlist = xs_list_append(vlist, xs_list_get(kv, 1)); 137
104 vars = xs_dict_append(vars, key, vlist); 138 vlist = xs_list_append(vlist, v);
105 } 139 vars = xs_dict_set(vars, k, vlist);
106 else 140 }
107 vars = xs_dict_append(vars, key, xs_list_get(kv, 1)); 141 else {
142 /* ends with []? force to always be a list */
143 if (xs_endswith(k, "[]")) {
144 xs *vlist = xs_list_new();
145 vlist = xs_list_append(vlist, v);
146 vars = xs_dict_append(vars, k, vlist);
108 } 147 }
148 else
149 vars = xs_dict_append(vars, k, v);
109 } 150 }
110 } 151 }
111 } 152 }
@@ -233,7 +274,8 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
233 l1 = xs_list_append(l1, vpo); 274 l1 = xs_list_append(l1, vpo);
234 l1 = xs_list_append(l1, vps); 275 l1 = xs_list_append(l1, vps);
235 276
236 p_vars = xs_dict_append(p_vars, vn, l1); 277 if (xs_is_string(vn))
278 p_vars = xs_dict_append(p_vars, vn, l1);
237 } 279 }
238 else { 280 else {
239 /* regular variable; just copy */ 281 /* regular variable; just copy */
@@ -241,7 +283,8 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
241 memcpy(vc, payload + po, ps); 283 memcpy(vc, payload + po, ps);
242 vc[ps] = '\0'; 284 vc[ps] = '\0';
243 285
244 p_vars = xs_dict_append(p_vars, vn, vc); 286 if (xs_is_string(vn) && xs_is_string(vc))
287 p_vars = xs_dict_append(p_vars, vn, vc);
245 } 288 }
246 289
247 /* move on */ 290 /* move on */
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 */