diff options
| author | 2025-02-15 14:37:36 +0100 | |
|---|---|---|
| committer | 2025-02-15 14:37:36 +0100 | |
| commit | 7611a6bee4bcbad2f1710aafa99aba730e5cf995 (patch) | |
| tree | 33ab7bee30379e16f6869b2efda5494be8aeb858 | |
| parent | enforce tls when supported && add tests (diff) | |
| parent | Version 2.72 RELEASED. (diff) | |
| download | penes-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.md | 2 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 38 | ||||
| -rw-r--r-- | TODO.md | 8 | ||||
| -rw-r--r-- | activitypub.c | 2 | ||||
| -rw-r--r-- | data.c | 82 | ||||
| -rw-r--r-- | doc/snac.1 | 5 | ||||
| -rw-r--r-- | doc/snac.5 | 2 | ||||
| -rw-r--r-- | doc/snac.8 | 6 | ||||
| -rw-r--r-- | doc/style.css | 1 | ||||
| -rw-r--r-- | format.c | 6 | ||||
| -rw-r--r-- | html.c | 393 | ||||
| -rw-r--r-- | httpd.c | 3 | ||||
| -rw-r--r-- | main.c | 49 | ||||
| -rw-r--r-- | mastoapi.c | 9 | ||||
| -rw-r--r-- | snac.c | 2 | ||||
| -rw-r--r-- | snac.h | 10 | ||||
| -rw-r--r-- | utils.c | 3 | ||||
| -rw-r--r-- | xs.h | 36 | ||||
| -rw-r--r-- | xs_fcgi.h | 3 | ||||
| -rw-r--r-- | xs_html.h | 4 | ||||
| -rw-r--r-- | xs_httpd.h | 54 | ||||
| -rw-r--r-- | xs_io.h | 7 | ||||
| -rw-r--r-- | xs_json.h | 6 | ||||
| -rw-r--r-- | xs_match.h | 7 | ||||
| -rw-r--r-- | xs_openssl.h | 2 | ||||
| -rw-r--r-- | xs_socket.h | 2 | ||||
| -rw-r--r-- | xs_url.h | 117 | ||||
| -rw-r--r-- | xs_version.h | 2 |
28 files changed, 636 insertions, 225 deletions
| @@ -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 | |||
| 5 | Each 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 | |||
| 7 | Each notification includes a link labelled `Context`, that leads to a page with the full conversation tree the post is a part of. | ||
| 8 | |||
| 9 | Each followed hashtag has now a directly accesible link. | ||
| 10 | |||
| 11 | Fixed a search bug (some matches were missed). | ||
| 12 | |||
| 13 | Fixed more crashes (contributed by inz). | ||
| 14 | |||
| 15 | Fixed link detection in posts (contributed by inz). | ||
| 16 | |||
| 17 | Allow multiple editors for command-line posts (contributed by inz). | ||
| 18 | |||
| 19 | Separated maximum and default timeline entry count, allowing larger timelines to be requested without having to increase the default (contributed by lxo). | ||
| 20 | |||
| 21 | Turned 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 | |||
| 23 | Special thanks to fellow developer inz for bringing my attention to code places where I should have been more careful. | ||
| 24 | |||
| 25 | ## 2.71 | ||
| 26 | |||
| 27 | Fixed memory leak (contributed by inz). | ||
| 28 | |||
| 29 | Fixed crash. | ||
| 30 | |||
| 31 | ## 2.70 | ||
| 32 | |||
| 33 | Notifications are now shown in a more compact way (i.e. all reactions are shown just above your post, instead of repeating the post *ad nauseam* for every reaction). | ||
| 34 | |||
| 35 | New command-line option `unmute` to, well, no-longer-mute an actor. | ||
| 36 | |||
| 37 | The private timeline now includes an approximate mark between new posts and "already seen" ones. | ||
| 38 | |||
| 39 | Fixed 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 | ||
| 5 | Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature. | 43 | Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature. |
| @@ -14,14 +14,12 @@ Important: deleting a follower should do more that just delete the object, see h | |||
| 14 | 14 | ||
| 15 | ## Wishlist | 15 | ## Wishlist |
| 16 | 16 | ||
| 17 | Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information). | 17 | Each notification should show a link to the full thread, to see it in context. |
| 18 | 18 | ||
| 19 | The instance timeline should also show boosts from users. | 19 | The instance timeline should also show boosts from users. |
| 20 | 20 | ||
| 21 | Mastoapi: implement /v1/conversations. | 21 | Mastoapi: implement /v1/conversations. |
| 22 | 22 | ||
| 23 | Implement following of hashtags (this is not trivial). | ||
| 24 | |||
| 25 | Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id). | 23 | Track 'Event' data types standardization; how to add plan-to-attend and similar activities (more info: https://event-federation.eu/). Friendica interacts with events via activities `Accept` (will go), `TentativeAccept` (will try to go) or `Reject` (cannot go) (`object` field as id, not object). `Undo` for any of these activities cancel (`object` as an object, not id). |
| 26 | 24 | ||
| 27 | Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md | 25 | Implement "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 | |||
| 365 | Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). | 363 | Add support for /share?text=tt&website=url (whatever it is, see https://mastodonshare.com/ for details) (2025-01-06T18:43:52+0100). |
| 366 | 364 | ||
| 367 | Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). | 365 | Add support for /authorize_interaction (whatever it is) (2025-01-16T14:45:28+0100). |
| 366 | |||
| 367 | Implement following of hashtags (this is not trivial) (2025-01-30T16:12:16+0100). | ||
| 368 | |||
| 369 | Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information) (2025-01-30T16:12:34+0100). | ||
diff --git a/activitypub.c b/activitypub.c index 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; |
| @@ -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 | ||
| 1490 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show) | 1492 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more) |
| 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 | ||
| 1499 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show) | 1513 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more) |
| 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 | ||
| 1531 | void timeline_add_mark(snac *user) | ||
| 1532 | /* adds an "already seen" mark to the private timeline */ | ||
| 1533 | { | ||
| 1534 | xs *fn = xs_fmt("%s/private.idx", user->basedir); | ||
| 1535 | char last_entry[MD5_HEX_SIZE] = ""; | ||
| 1536 | FILE *f; | ||
| 1537 | |||
| 1538 | /* get the last entry in the index */ | ||
| 1539 | if ((f = fopen(fn, "r")) != NULL) { | ||
| 1540 | index_desc_first(f, last_entry, 0); | ||
| 1541 | fclose(f); | ||
| 1542 | } | ||
| 1543 | |||
| 1544 | /* is the last entry *not* a mark? */ | ||
| 1545 | if (strcmp(last_entry, MD5_ALREADY_SEEN_MARK) != 0) { | ||
| 1546 | /* add it */ | ||
| 1547 | index_add_md5(fn, MD5_ALREADY_SEEN_MARK); | ||
| 1548 | } | ||
| 1549 | } | ||
| 1550 | |||
| 1551 | |||
| 1517 | xs_str *instance_index_fn(void) | 1552 | xs_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; |
| @@ -234,6 +234,8 @@ Purges old data from the timeline of all users. | |||
| 234 | .It Cm adduser Ar basedir Op uid | 234 | .It Cm adduser Ar basedir Op uid |
| 235 | Adds a new user to the server. This is an interactive command; | 235 | Adds a new user to the server. This is an interactive command; |
| 236 | necessary information will be prompted for. | 236 | necessary information will be prompted for. |
| 237 | .It Cm deluser Ar basedir Ar uid | ||
| 238 | Deletes a user, unfollowing all accounts first. | ||
| 237 | .It Cm resetpwd Ar basedir Ar uid | 239 | .It Cm resetpwd Ar basedir Ar uid |
| 238 | Resets a user's password to a new, random one. | 240 | Resets a user's password to a new, random one. |
| 239 | .It Cm queue Ar basedir Ar uid | 241 | .It Cm queue Ar basedir Ar uid |
| @@ -257,6 +259,9 @@ The rest of command line arguments are treated as media files to be | |||
| 257 | attached to the post. | 259 | attached to the post. |
| 258 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... | 260 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... |
| 259 | Like the previous one, but creates an "unlisted" (or "quiet public") post. | 261 | Like the previous one, but creates an "unlisted" (or "quiet public") post. |
| 262 | .It Cm note_mention Ar basedir Ar uid Ar text Op file file ... | ||
| 263 | Like the previous one, but creates a post only for accounts mentioned | ||
| 264 | in the post body. | ||
| 260 | .It Cm block Ar basedir Ar instance_url | 265 | .It Cm block Ar basedir Ar instance_url |
| 261 | Blocks a full instance, given its URL or domain name. All subsequent | 266 | Blocks a full instance, given its URL or domain name. All subsequent |
| 262 | incoming activities with identifiers from that instance will be immediately | 267 | incoming activities with identifiers from that instance will be immediately |
| @@ -78,7 +78,7 @@ converted to related emojis: | |||
| 78 | .Ss Accepted HTML | 78 | .Ss Accepted HTML |
| 79 | All HTML tags in entries are neutered except the following ones: | 79 | All HTML tags in entries are neutered except the following ones: |
| 80 | .Bd -literal | 80 | .Bd -literal |
| 81 | a p br blockquote ul ol li cite small | 81 | a p br blockquote ul ol li cite small h2 h3 |
| 82 | span i b u s pre code em strong hr img del | 82 | span i b u s pre code em strong hr img del |
| 83 | .Ed | 83 | .Ed |
| 84 | .Pp | 84 | .Pp |
| @@ -154,6 +154,8 @@ to those servers that went timeout in the previous retry. If you want to | |||
| 154 | give slow servers a chance to receive your messages, you can increase this | 154 | give slow servers a chance to receive your messages, you can increase this |
| 155 | value (but also take into account that processing the queue will take longer | 155 | value (but also take into account that processing the queue will take longer |
| 156 | while waiting for these molasses to respond). | 156 | while waiting for these molasses to respond). |
| 157 | .It Ic def_timeline_entries | ||
| 158 | This is the default timeline entries shown in the web interface. | ||
| 157 | .It Ic max_timeline_entries | 159 | .It Ic max_timeline_entries |
| 158 | This is the maximum timeline entries shown in the web interface. | 160 | This 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). | |||
| 205 | The user name of the instance administrator (optional). | 207 | The user name of the instance administrator (optional). |
| 206 | .It Ic short_description | 208 | .It Ic short_description |
| 207 | A textual short description about the instance (optional). | 209 | A textual short description about the instance (optional). |
| 210 | .It Ic short_description_raw | ||
| 211 | Whether to interpret short_descript as raw string or convert to HTML (optional). | ||
| 208 | .It Ic fastcgi | 212 | .It Ic fastcgi |
| 209 | If set to true, | 213 | If 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 |
| 257 | The maximum number of entries (posts) to be returned in user RSS feeds and outboxes | 261 | The 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 | ||
| 264 | The maximum number of attachments per post (default: 4). | ||
| 259 | .El | 265 | .El |
| 260 | .Pp | 266 | .Pp |
| 261 | You must restart the server to make effective these changes. | 267 | You 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 } |
| @@ -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 | ||
| 82 | static xs_str *format_line(const char *line, xs_list **attach) | 84 | static 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 | ||
| @@ -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 | ||
| 116 | xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, | 117 | xs_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 | ||
| 264 | xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy) | 285 | xs_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 | ||
| 554 | static xs_html *html_instance_body(void) | 616 | static 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 | ||
| @@ -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) |
| @@ -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 | ||
| 15 | int usage(void) | 16 | int 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; |
| @@ -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}"); |
| @@ -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 | ||
| @@ -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 | |||
| 25 | extern double disk_layout; | 27 | extern double disk_layout; |
| 26 | extern xs_str *srv_basedir; | 28 | extern xs_str *srv_basedir; |
| 27 | extern xs_dict *srv_config; | 29 | extern xs_dict *srv_config; |
| @@ -157,12 +159,14 @@ int timeline_here(snac *snac, const char *md5); | |||
| 157 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); | 159 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); |
| 158 | int timeline_del(snac *snac, const char *id); | 160 | int timeline_del(snac *snac, const char *id); |
| 159 | xs_str *user_index_fn(snac *user, const char *idx_name); | 161 | xs_str *user_index_fn(snac *user, const char *idx_name); |
| 160 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show); | 162 | xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more); |
| 161 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show); | 163 | xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more); |
| 162 | int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); | 164 | int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); |
| 163 | int timeline_admire(snac *snac, const char *id, const char *admirer, int like); | 165 | int timeline_admire(snac *snac, const char *id, const char *admirer, int like); |
| 164 | 166 | ||
| 165 | xs_list *timeline_top_level(snac *snac, const xs_list *list); | 167 | xs_list *timeline_top_level(snac *snac, const xs_list *list); |
| 168 | void timeline_add_mark(snac *user); | ||
| 169 | |||
| 166 | xs_list *local_list(snac *snac, int max); | 170 | xs_list *local_list(snac *snac, int max); |
| 167 | xs_str *instance_index_fn(void); | 171 | xs_str *instance_index_fn(void); |
| 168 | xs_list *timeline_instance_list(int skip, int show); | 172 | xs_list *timeline_instance_list(int skip, int show); |
| @@ -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" |
| @@ -12,6 +12,7 @@ | |||
| 12 | #include <stdarg.h> | 12 | #include <stdarg.h> |
| 13 | #include <signal.h> | 13 | #include <signal.h> |
| 14 | #include <errno.h> | 14 | #include <errno.h> |
| 15 | #include <stdint.h> | ||
| 15 | 16 | ||
| 16 | typedef enum { | 17 | typedef enum { |
| 17 | XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ | 18 | XSTYPE_STRING = 0x02, /* C string (\0 delimited) (NOT STORED) */ |
| @@ -142,6 +143,7 @@ void xs_data_get(void *data, const xs_data *value); | |||
| 142 | void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); | 143 | void *xs_memmem(const char *haystack, int h_size, const char *needle, int n_size); |
| 143 | 144 | ||
| 144 | unsigned int xs_hash_func(const char *data, int size); | 145 | unsigned int xs_hash_func(const char *data, int size); |
| 146 | uint64_t xs_hash64_func(const char *data, int size); | ||
| 145 | 147 | ||
| 146 | #ifdef XS_ASSERT | 148 | #ifdef XS_ASSERT |
| 147 | #include <assert.h> | 149 | #include <assert.h> |
| @@ -632,7 +634,7 @@ xs_str *xs_crop_i(xs_str *str, int start, int end) | |||
| 632 | end = sz + end; | 634 | end = sz + end; |
| 633 | 635 | ||
| 634 | /* crop from the top */ | 636 | /* crop from the top */ |
| 635 | if (end > 0 && end < sz) | 637 | if (end >= 0 && end < sz) |
| 636 | str[end] = '\0'; | 638 | str[end] = '\0'; |
| 637 | 639 | ||
| 638 | /* crop from the bottom */ | 640 | /* crop from the bottom */ |
| @@ -989,16 +991,20 @@ xs_str *xs_join(const xs_list *list, const char *sep) | |||
| 989 | xs_list *xs_split_n(const char *str, const char *sep, int times) | 991 | xs_list *xs_split_n(const char *str, const char *sep, int times) |
| 990 | /* splits a string into a list upto n times */ | 992 | /* splits a string into a list upto n times */ |
| 991 | { | 993 | { |
| 994 | xs_list *list = xs_list_new(); | ||
| 995 | |||
| 996 | if (!xs_is_string(str) || !xs_is_string(sep)) | ||
| 997 | return list; | ||
| 998 | |||
| 992 | int sz = strlen(sep); | 999 | int sz = strlen(sep); |
| 993 | char *ss; | 1000 | char *ss; |
| 994 | xs_list *list; | ||
| 995 | |||
| 996 | list = xs_list_new(); | ||
| 997 | 1001 | ||
| 998 | while (times > 0 && (ss = strstr(str, sep)) != NULL) { | 1002 | while (times > 0 && (ss = strstr(str, sep)) != NULL) { |
| 999 | /* create a new string with this slice and add it to the list */ | 1003 | /* create a new string with this slice and add it to the list */ |
| 1000 | xs *s = xs_str_new_sz(str, ss - str); | 1004 | xs *s = xs_str_new_sz(str, ss - str); |
| 1001 | list = xs_list_append(list, s); | 1005 | |
| 1006 | if (xs_is_string(s)) | ||
| 1007 | list = xs_list_append(list, s); | ||
| 1002 | 1008 | ||
| 1003 | /* skip past the separator */ | 1009 | /* skip past the separator */ |
| 1004 | str = ss + sz; | 1010 | str = ss + sz; |
| @@ -1007,7 +1013,8 @@ xs_list *xs_split_n(const char *str, const char *sep, int times) | |||
| 1007 | } | 1013 | } |
| 1008 | 1014 | ||
| 1009 | /* add the rest of the string */ | 1015 | /* add the rest of the string */ |
| 1010 | list = xs_list_append(list, str); | 1016 | if (xs_is_string(str)) |
| 1017 | list = xs_list_append(list, str); | ||
| 1011 | 1018 | ||
| 1012 | return list; | 1019 | return list; |
| 1013 | } | 1020 | } |
| @@ -1487,9 +1494,8 @@ unsigned int xs_hash_func(const char *data, int size) | |||
| 1487 | /* a general purpose hashing function */ | 1494 | /* a general purpose hashing function */ |
| 1488 | { | 1495 | { |
| 1489 | unsigned int hash = 0x666; | 1496 | unsigned int hash = 0x666; |
| 1490 | int n; | ||
| 1491 | 1497 | ||
| 1492 | for (n = 0; n < size; n++) { | 1498 | for (int n = 0; n < size; n++) { |
| 1493 | hash ^= (unsigned char)data[n]; | 1499 | hash ^= (unsigned char)data[n]; |
| 1494 | hash *= 111111111; | 1500 | hash *= 111111111; |
| 1495 | } | 1501 | } |
| @@ -1498,6 +1504,20 @@ unsigned int xs_hash_func(const char *data, int size) | |||
| 1498 | } | 1504 | } |
| 1499 | 1505 | ||
| 1500 | 1506 | ||
| 1507 | uint64_t xs_hash64_func(const char *data, int size) | ||
| 1508 | /* a general purpose hashing function (64 bit) */ | ||
| 1509 | { | ||
| 1510 | uint64_t hash = 0x100; | ||
| 1511 | |||
| 1512 | for (int n = 0; n < size; n++) { | ||
| 1513 | hash ^= (unsigned char)data[n]; | ||
| 1514 | hash *= 1111111111111111111; | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | return hash; | ||
| 1518 | } | ||
| 1519 | |||
| 1520 | |||
| 1501 | #endif /* XS_IMPLEMENTATION */ | 1521 | #endif /* XS_IMPLEMENTATION */ |
| 1502 | 1522 | ||
| 1503 | #endif /* _XS_H */ | 1523 | #endif /* _XS_H */ |
| @@ -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) |
| @@ -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 | } |
| @@ -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); |
| @@ -14,7 +14,7 @@ xs_val *xs_readall(FILE *f); | |||
| 14 | xs_str *xs_readline(FILE *f) | 14 | xs_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; |
| @@ -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 | } |
| @@ -24,6 +24,7 @@ int xs_match(const char *str, const char *spec) | |||
| 24 | retry: | 24 | retry: |
| 25 | 25 | ||
| 26 | for (;;) { | 26 | for (;;) { |
| 27 | const char *q = spec; | ||
| 27 | char c = *str++; | 28 | char c = *str++; |
| 28 | char p = *spec++; | 29 | char p = *spec++; |
| 29 | 30 | ||
| @@ -63,8 +64,12 @@ retry: | |||
| 63 | spec = b_spec; | 64 | spec = b_spec; |
| 64 | str = ++b_str; | 65 | str = ++b_str; |
| 65 | } | 66 | } |
| 66 | else | 67 | else { |
| 68 | if (*q == '|') | ||
| 69 | spec = q; | ||
| 70 | |||
| 67 | break; | 71 | break; |
| 72 | } | ||
| 68 | } | 73 | } |
| 69 | } | 74 | } |
| 70 | } | 75 | } |
diff --git a/xs_openssl.h b/xs_openssl.h index 9388691..f215bcc 100644 --- a/xs_openssl.h +++ b/xs_openssl.h | |||
| @@ -83,7 +83,7 @@ xs_val *xs_base64_dec(const xs_str *data, int *size) | |||
| 83 | s = xs_realloc(s, _xs_blk_size(*size + 1)); | 83 | s = xs_realloc(s, _xs_blk_size(*size + 1)); |
| 84 | s[*size] = '\0'; | 84 | s[*size] = '\0'; |
| 85 | 85 | ||
| 86 | BIO_free_all(mem); | 86 | BIO_free_all(b64); |
| 87 | 87 | ||
| 88 | return s; | 88 | return s; |
| 89 | } | 89 | } |
diff --git a/xs_socket.h b/xs_socket.h index 6e618ba..7bf5298 100644 --- a/xs_socket.h +++ b/xs_socket.h | |||
| @@ -85,6 +85,8 @@ int xs_socket_server(const char *addr, const char *serv) | |||
| 85 | listen(rs, SOMAXCONN); | 85 | listen(rs, SOMAXCONN); |
| 86 | } | 86 | } |
| 87 | 87 | ||
| 88 | freeaddrinfo(res); | ||
| 89 | |||
| 88 | #else /* WITHOUT_GETADDRINFO */ | 90 | #else /* WITHOUT_GETADDRINFO */ |
| 89 | struct sockaddr_in host; | 91 | struct sockaddr_in host; |
| 90 | 92 | ||
| @@ -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 | ||
| 14 | char *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 | |||
| 14 | xs_str *xs_url_dec(const char *str) | 47 | xs_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 */ | ||