diff options
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | Makefile | 8 | ||||
| -rw-r--r-- | Makefile.NetBSD | 8 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 32 | ||||
| -rw-r--r-- | activitypub.c | 13 | ||||
| -rw-r--r-- | data.c | 15 | ||||
| -rw-r--r-- | doc/snac.1 | 15 | ||||
| -rw-r--r-- | doc/snac.8 | 14 | ||||
| -rw-r--r-- | doc/style.css | 1 | ||||
| -rw-r--r-- | format.c | 22 | ||||
| -rw-r--r-- | html.c | 333 | ||||
| -rw-r--r-- | http.c | 2 | ||||
| -rw-r--r-- | httpd.c | 2 | ||||
| -rw-r--r-- | main.c | 40 | ||||
| -rw-r--r-- | mastoapi.c | 89 | ||||
| -rw-r--r-- | po/pt_BR.po | 12 | ||||
| -rw-r--r-- | rss.c | 12 | ||||
| -rw-r--r-- | snac.c | 153 | ||||
| -rw-r--r-- | snac.h | 10 | ||||
| -rw-r--r-- | upgrade.c | 2 | ||||
| -rw-r--r-- | utils.c | 75 | ||||
| -rw-r--r-- | webfinger.c | 2 | ||||
| -rw-r--r-- | xs.h | 2 | ||||
| -rw-r--r-- | xs_curl.h | 2 | ||||
| -rw-r--r-- | xs_fcgi.h | 2 | ||||
| -rw-r--r-- | xs_glob.h | 2 | ||||
| -rw-r--r-- | xs_hex.h | 2 | ||||
| -rw-r--r-- | xs_html.h | 2 | ||||
| -rw-r--r-- | xs_http.h | 2 | ||||
| -rw-r--r-- | xs_httpd.h | 2 | ||||
| -rw-r--r-- | xs_io.h | 2 | ||||
| -rw-r--r-- | xs_json.h | 4 | ||||
| -rw-r--r-- | xs_list_tools.h | 169 | ||||
| -rw-r--r-- | xs_match.h | 2 | ||||
| -rw-r--r-- | xs_mime.h | 2 | ||||
| -rw-r--r-- | xs_openssl.h | 2 | ||||
| -rw-r--r-- | xs_po.h | 2 | ||||
| -rw-r--r-- | xs_random.h | 2 | ||||
| -rw-r--r-- | xs_regex.h | 2 | ||||
| -rw-r--r-- | xs_set.h | 2 | ||||
| -rw-r--r-- | xs_socket.h | 2 | ||||
| -rw-r--r-- | xs_time.h | 2 | ||||
| -rw-r--r-- | xs_unicode.h | 2 | ||||
| -rw-r--r-- | xs_unix_socket.h | 2 | ||||
| -rw-r--r-- | xs_url.h | 2 | ||||
| -rw-r--r-- | xs_version.h | 2 | ||||
| -rw-r--r-- | xs_webmention.h | 2 |
47 files changed, 970 insertions, 111 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | MIT License | 1 | MIT License |
| 2 | 2 | ||
| 3 | Copyright (c) 2022 - 2025 grunfink et al. (Fediverse: @grunfink@comam.es) | 3 | Copyright (c) 2022 - 2026 grunfink et al. (Fediverse: @grunfink@comam.es) |
| 4 | 4 | ||
| 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| 6 | 6 | ||
| @@ -46,7 +46,7 @@ update-po: | |||
| 46 | xgettext --omit-header -j -o $$a --language=C --keyword=L --from-code=utf-8 *.c ; \ | 46 | xgettext --omit-header -j -o $$a --language=C --keyword=L --from-code=utf-8 *.c ; \ |
| 47 | done | 47 | done |
| 48 | 48 | ||
| 49 | activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ | 49 | activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_url.h xs_mime.h \ |
| 50 | xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ | 50 | xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ |
| 51 | xs_webmention.h xs_http.h xs_http_codes.h snac.h | 51 | xs_webmention.h xs_http.h xs_http_codes.h snac.h |
| 52 | data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ | 52 | data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ |
| @@ -68,16 +68,16 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ | |||
| 68 | xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ | 68 | xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ |
| 69 | xs_unicode.h xs_http.h xs_http_codes.h snac.h | 69 | xs_unicode.h xs_http.h xs_http_codes.h snac.h |
| 70 | rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ | 70 | rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ |
| 71 | xs_openssl.h xs_json.h xs_http.h xs_http_codes.h snac.h | 71 | xs_openssl.h xs_json.h xs_http.h xs_http_codes.h xs_unicode.h snac.h |
| 72 | sandbox.o: sandbox.c xs.h snac.h | 72 | sandbox.o: sandbox.c xs.h snac.h |
| 73 | snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ | 73 | snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ |
| 74 | xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ | 74 | xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ |
| 75 | xs_http.h xs_http_codes.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h \ | 75 | xs_http.h xs_http_codes.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h \ |
| 76 | xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h xs_po.h \ | 76 | xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h xs_po.h \ |
| 77 | xs_webmention.h snac.h | 77 | xs_webmention.h xs_list_tools.h snac.h |
| 78 | upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h | 78 | upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h |
| 79 | utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ | 79 | utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ |
| 80 | xs_random.h xs_glob.h xs_curl.h xs_regex.h xs_http.h xs_http_codes.h \ | 80 | xs_random.h xs_glob.h xs_curl.h xs_regex.h xs_http.h xs_http_codes.h \ |
| 81 | snac.h | 81 | xs_list_tools.h xs_set.h snac.h |
| 82 | webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h xs_http.h \ | 82 | webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h xs_http.h \ |
| 83 | xs_http_codes.h snac.h | 83 | xs_http_codes.h snac.h |
diff --git a/Makefile.NetBSD b/Makefile.NetBSD index b5005ee..e752910 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD | |||
| @@ -35,7 +35,7 @@ uninstall: | |||
| 35 | rm $(PREFIX_MAN)/man5/snac.5 | 35 | rm $(PREFIX_MAN)/man5/snac.5 |
| 36 | rm $(PREFIX_MAN)/man8/snac.8 | 36 | rm $(PREFIX_MAN)/man8/snac.8 |
| 37 | 37 | ||
| 38 | activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ | 38 | activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_url.h xs_mime.h \ |
| 39 | xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ | 39 | xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ |
| 40 | xs_webmention.h xs_http.h xs_http_codes.h snac.h | 40 | xs_webmention.h xs_http.h xs_http_codes.h snac.h |
| 41 | data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ | 41 | data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ |
| @@ -57,16 +57,16 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ | |||
| 57 | xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ | 57 | xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ |
| 58 | xs_unicode.h xs_http.h xs_http_codes.h snac.h | 58 | xs_unicode.h xs_http.h xs_http_codes.h snac.h |
| 59 | rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ | 59 | rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ |
| 60 | xs_openssl.h xs_json.h xs_http.h xs_http_codes.h snac.h | 60 | xs_openssl.h xs_json.h xs_http.h xs_http_codes.h xs_unicode.h snac.h |
| 61 | sandbox.o: sandbox.c xs.h snac.h | 61 | sandbox.o: sandbox.c xs.h snac.h |
| 62 | snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ | 62 | snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ |
| 63 | xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ | 63 | xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ |
| 64 | xs_http.h xs_http_codes.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h \ | 64 | xs_http.h xs_http_codes.h xs_httpd.h xs_mime.h xs_regex.h xs_set.h \ |
| 65 | xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h xs_po.h \ | 65 | xs_time.h xs_glob.h xs_random.h xs_match.h xs_fcgi.h xs_html.h xs_po.h \ |
| 66 | xs_webmention.h snac.h | 66 | xs_webmention.h xs_list_tools.h snac.h |
| 67 | upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h | 67 | upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h |
| 68 | utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ | 68 | utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ |
| 69 | xs_random.h xs_glob.h xs_curl.h xs_regex.h xs_http.h xs_http_codes.h \ | 69 | xs_random.h xs_glob.h xs_curl.h xs_regex.h xs_http.h xs_http_codes.h \ |
| 70 | snac.h | 70 | xs_list_tools.h xs_set.h snac.h |
| 71 | webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h xs_http.h \ | 71 | webfinger.o: webfinger.c xs.h xs_json.h xs_curl.h xs_mime.h xs_http.h \ |
| 72 | xs_http_codes.h snac.h | 72 | xs_http_codes.h snac.h |
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9d41588..e3f7b89 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md | |||
| @@ -2,9 +2,39 @@ | |||
| 2 | 2 | ||
| 3 | ## UNRELEASED | 3 | ## UNRELEASED |
| 4 | 4 | ||
| 5 | Fixed crash in pronouns processing (contributed by byte). | ||
| 6 | |||
| 7 | Added counters in the people page (contributed by byte). | ||
| 8 | |||
| 9 | New command-line option `refresh`, to refresh all follower and following `Actor` objects, marking them as broken if they are. | ||
| 10 | |||
| 11 | ## 2.88 | ||
| 12 | |||
| 13 | If `disable_emojireact` is set to `true` in `server.json`, EmojiReacts (incoming and outgoing) are totally disabled. | ||
| 14 | |||
| 15 | New command-line option `top_ten`, that returns the top ten most popular posts by a user (ordered by the sum of likes and boosts) (contributed by aov). | ||
| 16 | |||
| 17 | Added a new set of per-user muted words; if a post contains any of them, it's hidden behind a dropdown (contributed by byte). | ||
| 18 | |||
| 19 | If an account has a metadata named `pronouns`, it's shown by the name (contributed by violette). | ||
| 20 | |||
| 21 | Mastodon API: children of a post are returned recursively, not just the first level (contributed by violette). | ||
| 22 | |||
| 23 | Implemented optional metadata stripping for images and videos using external tools (contributed by Stefano Marinelli). | ||
| 24 | |||
| 25 | ## 2.87 | ||
| 26 | |||
| 27 | Hide EmojiReacts from muted actors and blocked instances. | ||
| 28 | |||
| 29 | ## 2.86 | ||
| 30 | |||
| 31 | Truncate RSS titles at UTF-8 character boundaries (contributed by lxo). | ||
| 32 | |||
| 33 | Link contacts to single-user people pages. Also, user's posts are shown (contributed by lxo). | ||
| 34 | |||
| 5 | Added emoji reactions (contributed by violette). | 35 | Added emoji reactions (contributed by violette). |
| 6 | 36 | ||
| 7 | Mastodon API: Fix for some client notifications (contributed by violette). | 37 | Mastodon API: Fix for some client notifications (contributed by violette), fix for a status visibility error (contributed by fruye). |
| 8 | 38 | ||
| 9 | If the query variable `terse` of a public post page is set to anything, no header is shown. | 39 | If the query variable `terse` of a public post page is set to anything, no header is shown. |
| 10 | 40 | ||
diff --git a/activitypub.c b/activitypub.c index 59df31a..c34e510 100644 --- a/activitypub.c +++ b/activitypub.c | |||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_json.h" | 5 | #include "xs_json.h" |
| @@ -1611,7 +1611,7 @@ xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid_o) | |||
| 1611 | xs *dict = xs_dict_new(); | 1611 | xs *dict = xs_dict_new(); |
| 1612 | xs *icon = xs_dict_new(); | 1612 | xs *icon = xs_dict_new(); |
| 1613 | xs *accounts = xs_list_new(); | 1613 | xs *accounts = xs_list_new(); |
| 1614 | xs *emjs = emojis(); | 1614 | xs *emjs = emojis_rm_categories(); |
| 1615 | 1615 | ||
| 1616 | /* may be a default emoji */ | 1616 | /* may be a default emoji */ |
| 1617 | xs *eidd = xs_dup(eid); | 1617 | xs *eidd = xs_dup(eid); |
| @@ -2578,6 +2578,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) | |||
| 2578 | return -1; | 2578 | return -1; |
| 2579 | } | 2579 | } |
| 2580 | 2580 | ||
| 2581 | if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { | ||
| 2582 | srv_log(xs_fmt("Dropping EmojiReact from %s due to admin configuration", actor)); | ||
| 2583 | return -1; | ||
| 2584 | } | ||
| 2585 | |||
| 2581 | const char *object, *utype; | 2586 | const char *object, *utype; |
| 2582 | 2587 | ||
| 2583 | object = xs_dict_get(msg, "object"); | 2588 | object = xs_dict_get(msg, "object"); |
| @@ -2953,7 +2958,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) | |||
| 2953 | if (xs_is_null(object)) | 2958 | if (xs_is_null(object)) |
| 2954 | snac_log(snac, xs_fmt("malformed message: no 'id' field")); | 2959 | snac_log(snac, xs_fmt("malformed message: no 'id' field")); |
| 2955 | else | 2960 | else |
| 2956 | if (timeline_admire(snac, object, actor, 1, xs_dup(msg)) == HTTP_STATUS_CREATED) | 2961 | if (timeline_admire(snac, object, actor, 1, msg) == HTTP_STATUS_CREATED) |
| 2957 | snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); | 2962 | snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); |
| 2958 | else | 2963 | else |
| 2959 | snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); | 2964 | snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); |
| @@ -3390,7 +3395,7 @@ void process_user_queue_item(snac *user, xs_dict *q_item) | |||
| 3390 | actor_add(actor, actor_o); | 3395 | actor_add(actor, actor_o); |
| 3391 | } | 3396 | } |
| 3392 | else { | 3397 | else { |
| 3393 | if (status == HTTP_STATUS_GONE) { | 3398 | if (status == HTTP_STATUS_GONE || status == HTTP_STATUS_NOT_FOUND) { |
| 3394 | actor_failure(actor, 1); | 3399 | actor_failure(actor, 1); |
| 3395 | snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status)); | 3400 | snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status)); |
| 3396 | } | 3401 | } |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_hex.h" | 5 | #include "xs_hex.h" |
| @@ -89,8 +89,15 @@ int srv_open(const char *basedir, int auto_upgrade) | |||
| 89 | else { | 89 | else { |
| 90 | if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout) | 90 | if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout) |
| 91 | error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first"); | 91 | error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first"); |
| 92 | else | 92 | else { |
| 93 | ret = 1; | 93 | if (!check_strip_tool()) { |
| 94 | const char *mp = xs_dict_get(srv_config, "mogrify_path"); | ||
| 95 | if (mp == NULL) mp = "mogrify"; | ||
| 96 | error = xs_fmt("ERROR: strip_exif enabled but '%s' not found or not working (set 'mogrify_path' in server.json)", mp); | ||
| 97 | } | ||
| 98 | else | ||
| 99 | ret = 1; | ||
| 100 | } | ||
| 94 | } | 101 | } |
| 95 | } | 102 | } |
| 96 | 103 | ||
| @@ -2710,6 +2717,8 @@ void static_put(snac *snac, const char *id, const char *data, int size) | |||
| 2710 | if (fn && (f = fopen(fn, "wb")) != NULL) { | 2717 | if (fn && (f = fopen(fn, "wb")) != NULL) { |
| 2711 | fwrite(data, size, 1, f); | 2718 | fwrite(data, size, 1, f); |
| 2712 | fclose(f); | 2719 | fclose(f); |
| 2720 | |||
| 2721 | strip_media(fn); | ||
| 2713 | } | 2722 | } |
| 2714 | } | 2723 | } |
| 2715 | 2724 | ||
| @@ -110,6 +110,9 @@ URLs to RSS feeds of ActivityPub objects are also allowed | |||
| 110 | .It Blocked hashtags... | 110 | .It Blocked hashtags... |
| 111 | Enter here the list of hashtags you want to block, one | 111 | Enter here the list of hashtags you want to block, one |
| 112 | per line, with or without the # symbol. | 112 | per line, with or without the # symbol. |
| 113 | .It Muted words... | ||
| 114 | Enter here a list of words to be silenced. If a post includes | ||
| 115 | any of this words, it's hidden behind a dropdown. | ||
| 113 | .El | 116 | .El |
| 114 | .Pp | 117 | .Pp |
| 115 | The user setup dialog allows some user information to be | 118 | The user setup dialog allows some user information to be |
| @@ -218,13 +221,20 @@ also private and cannot be liked nor boosted. | |||
| 218 | For each entry in the timeline, a set of reasonable actions | 221 | For each entry in the timeline, a set of reasonable actions |
| 219 | in the form of buttons will be shown. These can be: | 222 | in the form of buttons will be shown. These can be: |
| 220 | .Bl -tag -offset indent | 223 | .Bl -tag -offset indent |
| 221 | .It Reply | 224 | .It Reply... |
| 222 | Unveils a text area to write your intelligent and acute comment | 225 | Unveils a text area to write your intelligent and acute comment |
| 223 | to an uninformed fellow. This note is sent to the original | 226 | to an uninformed fellow. This note is sent to the original |
| 224 | author as well as to your followers. The note can include | 227 | author as well as to your followers. The note can include |
| 225 | mentions in the @user@format; these people will also become | 228 | mentions in the @user@format; these people will also become |
| 226 | recipients of the message. If you reply to a boost or like, | 229 | recipients of the message. If you reply to a boost or like, |
| 227 | you are really replying to the note, not to the admirer of it. | 230 | you are really replying to the note, not to the admirer of it. |
| 231 | .It Emoji react... | ||
| 232 | Unveils a text area that allows a user to react with an emoji | ||
| 233 | by typing its identifier, that should match one of the emojis | ||
| 234 | defined in the | ||
| 235 | .Pa emojis.json | ||
| 236 | file. By now, only those emoji identifiers surrounded by colons | ||
| 237 | can be used. | ||
| 228 | .It Like | 238 | .It Like |
| 229 | Click this if you admire this post. The poster and your | 239 | Click this if you admire this post. The poster and your |
| 230 | followers will be informed. | 240 | followers will be informed. |
| @@ -405,6 +415,9 @@ Removes an existing list. | |||
| 405 | Adds an account (by its @name@host handle or actor URL) to a list. | 415 | Adds an account (by its @name@host handle or actor URL) to a list. |
| 406 | .It Cm list_del Ar basedir Ar uid Ar name Ar actor_url | 416 | .It Cm list_del Ar basedir Ar uid Ar name Ar actor_url |
| 407 | Deletes an actor (by its actor URL) from a list. | 417 | Deletes an actor (by its actor URL) from a list. |
| 418 | .It Cm top_ten Ar basedir Ar uid Op N | ||
| 419 | Returns the ids of the top ten (or top N) most popular posts (considering the | ||
| 420 | sum of likes and boosts). | ||
| 408 | .El | 421 | .El |
| 409 | .Ss Migrating an account to/from Mastodon | 422 | .Ss Migrating an account to/from Mastodon |
| 410 | See | 423 | See |
| @@ -296,6 +296,20 @@ outgoing messages (default: 15). Anyway, whenever any incoming activity from a | |||
| 296 | failed instance is detected, this counter is reset for it. | 296 | failed instance is detected, this counter is reset for it. |
| 297 | .It Ic vkey | 297 | .It Ic vkey |
| 298 | Public vapid key. Used for notification on some client. | 298 | Public vapid key. Used for notification on some client. |
| 299 | .It Ic disable_emojireact | ||
| 300 | If set to true, all EmojiReact support (for input and output) is disabled. | ||
| 301 | .It Ic strip_exif | ||
| 302 | If set to true, EXIF and other metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp) and videos (mp4, m4v, mov, webm, mkv, avi). This requires the | ||
| 303 | .Nm mogrify | ||
| 304 | (from ImageMagick) and | ||
| 305 | .Nm ffmpeg | ||
| 306 | tools to be installed. If | ||
| 307 | .Nm snac | ||
| 308 | cannot find or execute these tools at startup, it will refuse to run. | ||
| 309 | .It Ic mogrify_path | ||
| 310 | Overrides the default "mogrify" command name or path. Use this if the tool is not in the system PATH or has a different name. | ||
| 311 | .It Ic ffmpeg_path | ||
| 312 | Overrides the default "ffmpeg" command name or path. Use this if the tool is not in the system PATH or has a different name. | ||
| 299 | .El | 313 | .El |
| 300 | .Pp | 314 | .Pp |
| 301 | You must restart the server to make effective these changes. | 315 | You must restart the server to make effective these changes. |
diff --git a/doc/style.css b/doc/style.css index 2981926..9c8764a 100644 --- a/doc/style.css +++ b/doc/style.css | |||
| @@ -40,3 +40,4 @@ blockquote { font-style: italic; } | |||
| 40 | a { color: #7799dd } | 40 | a { color: #7799dd } |
| 41 | a:visited { color: #aa99dd } | 41 | a:visited { color: #aa99dd } |
| 42 | } | 42 | } |
| 43 | select { max-width: 40%; } | ||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_regex.h" | 5 | #include "xs_regex.h" |
| @@ -79,6 +79,24 @@ xs_dict *emojis(void) | |||
| 79 | return d; | 79 | return d; |
| 80 | } | 80 | } |
| 81 | 81 | ||
| 82 | |||
| 83 | xs_dict *emojis_rm_categories() { | ||
| 84 | xs *emjs = emojis(); | ||
| 85 | char *res = xs_dict_new(); | ||
| 86 | const char *k, *v; | ||
| 87 | xs_dict_foreach(emjs, k, v) { | ||
| 88 | if (xs_type(v) == XSTYPE_DICT) { | ||
| 89 | const char *v2; | ||
| 90 | xs_dict_foreach(v, k, v2) | ||
| 91 | res = xs_dict_append(res, k, v2); | ||
| 92 | } | ||
| 93 | else | ||
| 94 | res = xs_dict_append(res, k, v); | ||
| 95 | } | ||
| 96 | return res; | ||
| 97 | } | ||
| 98 | |||
| 99 | |||
| 82 | /* Non-whitespace without trailing comma, period or closing paren */ | 100 | /* Non-whitespace without trailing comma, period or closing paren */ |
| 83 | #define NOSPACE "([^[:space:],.)]+|[,.)]+[^[:space:],.)])+" | 101 | #define NOSPACE "([^[:space:],.)]+|[,.)]+[^[:space:],.)])+" |
| 84 | 102 | ||
| @@ -405,7 +423,7 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag | |||
| 405 | 423 | ||
| 406 | { | 424 | { |
| 407 | /* traditional emoticons */ | 425 | /* traditional emoticons */ |
| 408 | xs *d = emojis(); | 426 | xs *d = emojis_rm_categories(); |
| 409 | int c = 0; | 427 | int c = 0; |
| 410 | const char *k, *v; | 428 | const char *k, *v; |
| 411 | 429 | ||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -16,6 +16,7 @@ | |||
| 16 | #include "xs_url.h" | 16 | #include "xs_url.h" |
| 17 | #include "xs_random.h" | 17 | #include "xs_random.h" |
| 18 | #include "xs_http.h" | 18 | #include "xs_http.h" |
| 19 | #include "xs_list_tools.h" | ||
| 19 | 20 | ||
| 20 | #include "snac.h" | 21 | #include "snac.h" |
| 21 | 22 | ||
| @@ -161,6 +162,34 @@ xs_str *actor_name(xs_dict *actor, const char *proxy) | |||
| 161 | } | 162 | } |
| 162 | 163 | ||
| 163 | 164 | ||
| 165 | xs_str *actor_pronouns(xs_dict *actor) | ||
| 166 | /* gets the actor name */ | ||
| 167 | { | ||
| 168 | const xs_list *attachment; | ||
| 169 | const xs_dict *d; | ||
| 170 | const char *v; | ||
| 171 | const char *pronouns = ""; | ||
| 172 | xs_str *ret; | ||
| 173 | |||
| 174 | if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) { | ||
| 175 | xs_list_foreach(attachment, d) { | ||
| 176 | xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); | ||
| 177 | /* make sure that we are reading the correct metadata */ | ||
| 178 | if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { | ||
| 179 | /* safeguard from NULL values */ | ||
| 180 | v = xs_dict_get(d, "value"); | ||
| 181 | pronouns = v ? v : pronouns; | ||
| 182 | break; | ||
| 183 | } | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | /* strip all HTML tags */ | ||
| 188 | ret = xs_regex_replace(pronouns, "</?[^>]+>", ""); | ||
| 189 | return ret; | ||
| 190 | } | ||
| 191 | |||
| 192 | |||
| 164 | xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) | 193 | xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) |
| 165 | /* needed when we have local text with no tags attached */ | 194 | /* needed when we have local text with no tags attached */ |
| 166 | { | 195 | { |
| @@ -194,6 +223,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, | |||
| 194 | int fwer = 0; | 223 | int fwer = 0; |
| 195 | 224 | ||
| 196 | xs *name = actor_name(actor, proxy); | 225 | xs *name = actor_name(actor, proxy); |
| 226 | xs *pronouns = actor_pronouns(actor); | ||
| 197 | 227 | ||
| 198 | /* get the avatar */ | 228 | /* get the avatar */ |
| 199 | if ((v = xs_dict_get(actor, "icon")) != NULL) { | 229 | if ((v = xs_dict_get(actor, "icon")) != NULL) { |
| @@ -221,23 +251,35 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, | |||
| 221 | anchored link to the people page instead of the actor url */ | 251 | anchored link to the people page instead of the actor url */ |
| 222 | if (fwer || fwing) { | 252 | if (fwer || fwing) { |
| 223 | xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); | 253 | xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); |
| 224 | href = xs_fmt("%s/people#%s", user->actor, md5); | 254 | href = xs_fmt("%s/people/%s", user->actor, md5); |
| 225 | } | 255 | } |
| 226 | } | 256 | } |
| 227 | 257 | ||
| 228 | if (href == NULL) | 258 | if (href == NULL) |
| 229 | href = xs_dup(actor_id); | 259 | href = xs_dup(actor_id); |
| 230 | 260 | ||
| 261 | xs_html *name_link = xs_html_tag("a", | ||
| 262 | xs_html_attr("href", href), | ||
| 263 | xs_html_attr("class", "p-author h-card snac-author"), | ||
| 264 | xs_html_raw(name)); /* name is already html-escaped */ | ||
| 265 | |||
| 266 | if (*pronouns) { | ||
| 267 | xs_html_add(name_link, | ||
| 268 | xs_html_text(" ["), | ||
| 269 | xs_html_tag("span", | ||
| 270 | xs_html_attr("class", "snac-pronouns"), | ||
| 271 | xs_html_attr("title", "user's pronouns"), | ||
| 272 | xs_html_raw(pronouns)), | ||
| 273 | xs_html_text("]")); | ||
| 274 | } | ||
| 275 | |||
| 231 | xs_html_add(actor_icon, | 276 | xs_html_add(actor_icon, |
| 232 | xs_html_sctag("img", | 277 | xs_html_sctag("img", |
| 233 | xs_html_attr("loading", "lazy"), | 278 | xs_html_attr("loading", "lazy"), |
| 234 | xs_html_attr("class", "snac-avatar"), | 279 | xs_html_attr("class", "snac-avatar"), |
| 235 | xs_html_attr("src", avatar), | 280 | xs_html_attr("src", avatar), |
| 236 | xs_html_attr("alt", "[?]")), | 281 | xs_html_attr("alt", "[?]")), |
| 237 | xs_html_tag("a", | 282 | name_link); |
| 238 | xs_html_attr("href", href), | ||
| 239 | xs_html_attr("class", "p-author h-card snac-author"), | ||
| 240 | xs_html_raw(name))); /* name is already html-escaped */ | ||
| 241 | 283 | ||
| 242 | if (!xs_is_null(url)) { | 284 | if (!xs_is_null(url)) { |
| 243 | xs *md5 = xs_md5_hex(url, strlen(url)); | 285 | xs *md5 = xs_md5_hex(url, strlen(url)); |
| @@ -250,6 +292,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, | |||
| 250 | xs_html_text("»"))); | 292 | xs_html_text("»"))); |
| 251 | } | 293 | } |
| 252 | 294 | ||
| 295 | |||
| 253 | if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { | 296 | if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { |
| 254 | xs_html_add(actor_icon, | 297 | xs_html_add(actor_icon, |
| 255 | xs_html_text(" "), | 298 | xs_html_text(" "), |
| @@ -1864,6 +1907,38 @@ xs_html *html_top_controls(snac *user) | |||
| 1864 | xs_html_attr("class", "button"), | 1907 | xs_html_attr("class", "button"), |
| 1865 | xs_html_attr("value", L("Update hashtags"))))))); | 1908 | xs_html_attr("value", L("Update hashtags"))))))); |
| 1866 | 1909 | ||
| 1910 | xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor); | ||
| 1911 | xs *muted_words = xs_join(xs_dict_get_def(user->config, | ||
| 1912 | "muted_words", xs_stock(XSTYPE_LIST)), "\n"); | ||
| 1913 | |||
| 1914 | xs_html_add(top_controls, | ||
| 1915 | xs_html_tag("details", | ||
| 1916 | xs_html_tag("summary", | ||
| 1917 | xs_html_text(L("Muted words..."))), | ||
| 1918 | xs_html_tag("p", | ||
| 1919 | xs_html_text(L("One word per line, partial matches count"))), | ||
| 1920 | xs_html_tag("div", | ||
| 1921 | xs_html_attr("class", "snac-muted-words"), | ||
| 1922 | xs_html_tag("form", | ||
| 1923 | xs_html_attr("autocomplete", "off"), | ||
| 1924 | xs_html_attr("method", "post"), | ||
| 1925 | xs_html_attr("action", muted_words_action), | ||
| 1926 | xs_html_attr("enctype", "multipart/form-data"), | ||
| 1927 | |||
| 1928 | xs_html_tag("textarea", | ||
| 1929 | xs_html_attr("name", "muted_words"), | ||
| 1930 | xs_html_attr("cols", "40"), | ||
| 1931 | xs_html_attr("rows", "4"), | ||
| 1932 | xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"), | ||
| 1933 | xs_html_text(muted_words)), | ||
| 1934 | |||
| 1935 | xs_html_tag("br", NULL), | ||
| 1936 | |||
| 1937 | xs_html_sctag("input", | ||
| 1938 | xs_html_attr("type", "submit"), | ||
| 1939 | xs_html_attr("class", "button"), | ||
| 1940 | xs_html_attr("value", L("Update muted words"))))))); | ||
| 1941 | |||
| 1867 | return top_controls; | 1942 | return top_controls; |
| 1868 | } | 1943 | } |
| 1869 | 1944 | ||
| @@ -2103,7 +2178,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, | |||
| 2103 | xs_html_tag("p", NULL)); | 2178 | xs_html_tag("p", NULL)); |
| 2104 | } | 2179 | } |
| 2105 | 2180 | ||
| 2106 | { /** emoji react **/ | 2181 | if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/ |
| 2107 | /* the post textarea */ | 2182 | /* the post textarea */ |
| 2108 | xs *div_id = xs_fmt("%s_reply", md5); | 2183 | xs *div_id = xs_fmt("%s_reply", md5); |
| 2109 | xs *form_id = xs_fmt("%s_reply_form", md5); | 2184 | xs *form_id = xs_fmt("%s_reply_form", md5); |
| @@ -2144,6 +2219,30 @@ xs_html *html_entry_controls(snac *user, const char *actor, | |||
| 2144 | } | 2219 | } |
| 2145 | 2220 | ||
| 2146 | 2221 | ||
| 2222 | static const xs_str* words_in_content(const xs_list *words, const xs_val *content) | ||
| 2223 | /* returns a word that matches any of the words in content */ | ||
| 2224 | { | ||
| 2225 | if (!xs_is_list(words) || !xs_is_string(content)) { | ||
| 2226 | return NULL; | ||
| 2227 | } | ||
| 2228 | xs *c = xs_split(content, " "); | ||
| 2229 | xs *sc = xs_list_sort(c, NULL); | ||
| 2230 | |||
| 2231 | const xs_str *wv; | ||
| 2232 | const xs_str *cv; | ||
| 2233 | xs_list_foreach(words, wv) { | ||
| 2234 | xs_list_foreach(sc, cv) { | ||
| 2235 | xs_tolower_i((xs_str*)cv); | ||
| 2236 | if(xs_str_in(cv, wv) != -1){ | ||
| 2237 | return wv; | ||
| 2238 | } | ||
| 2239 | } | ||
| 2240 | } | ||
| 2241 | |||
| 2242 | return NULL; | ||
| 2243 | } | ||
| 2244 | |||
| 2245 | |||
| 2147 | xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | 2246 | xs_html *html_entry(snac *user, xs_dict *msg, int read_only, |
| 2148 | int level, const char *md5, int hide_children) | 2247 | int level, const char *md5, int hide_children) |
| 2149 | { | 2248 | { |
| @@ -2355,7 +2454,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2355 | } | 2454 | } |
| 2356 | 2455 | ||
| 2357 | if (!read_only && (fwers || fwing)) | 2456 | if (!read_only && (fwers || fwing)) |
| 2358 | href = xs_fmt("%s/people#%s", user->actor, p); | 2457 | href = xs_fmt("%s/people/%s", user->actor, p); |
| 2359 | else | 2458 | else |
| 2360 | href = xs_dup(id); | 2459 | href = xs_dup(id); |
| 2361 | 2460 | ||
| @@ -2438,6 +2537,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2438 | xs_html_text(v), | 2537 | xs_html_text(v), |
| 2439 | xs_html_text(L(" [SENSITIVE CONTENT]")))); | 2538 | xs_html_text(L(" [SENSITIVE CONTENT]")))); |
| 2440 | } | 2539 | } |
| 2540 | else | ||
| 2541 | if (user && | ||
| 2542 | /* muted_words is all lowercase and sorted for performance */ | ||
| 2543 | (v = words_in_content(xs_dict_get(user->config, "muted_words"), | ||
| 2544 | xs_dict_get(msg, "content"))) != NULL) { | ||
| 2545 | snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id)); | ||
| 2546 | snac_content = xs_html_tag("details", | ||
| 2547 | xs_html_tag("summary", | ||
| 2548 | xs_html_text(L("Muted: ")), | ||
| 2549 | xs_html_text(v))); | ||
| 2550 | } | ||
| 2441 | else { | 2551 | else { |
| 2442 | snac_content = xs_html_tag("div", NULL); | 2552 | snac_content = xs_html_tag("div", NULL); |
| 2443 | } | 2553 | } |
| @@ -2447,7 +2557,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2447 | 2557 | ||
| 2448 | /* add all emoji reacts */ | 2558 | /* add all emoji reacts */ |
| 2449 | int is_emoji = 0; | 2559 | int is_emoji = 0; |
| 2450 | { | 2560 | if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { |
| 2451 | int c = 0; | 2561 | int c = 0; |
| 2452 | const xs_dict *k; | 2562 | const xs_dict *k; |
| 2453 | xs *ls = xs_list_new(); | 2563 | xs *ls = xs_list_new(); |
| @@ -2460,6 +2570,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2460 | const char *content = xs_dict_get(m, "content"); | 2570 | const char *content = xs_dict_get(m, "content"); |
| 2461 | const char *actor = xs_dict_get(m, "actor"); | 2571 | const char *actor = xs_dict_get(m, "actor"); |
| 2462 | const xs_list *contentl = xs_dict_get(sfrl, content); | 2572 | const xs_list *contentl = xs_dict_get(sfrl, content); |
| 2573 | |||
| 2574 | if ((user && is_muted(user, actor)) || is_instance_blocked(actor)) | ||
| 2575 | continue; | ||
| 2576 | |||
| 2463 | xs *actors = xs_list_new(); | 2577 | xs *actors = xs_list_new(); |
| 2464 | actors = xs_list_append(actors, actor); | 2578 | actors = xs_list_append(actors, actor); |
| 2465 | char me = actor && user && strcmp(actor, user->actor) == 0; | 2579 | char me = actor && user && strcmp(actor, user->actor) == 0; |
| @@ -3593,13 +3707,19 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, | |||
| 3593 | } | 3707 | } |
| 3594 | 3708 | ||
| 3595 | 3709 | ||
| 3596 | xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy) | 3710 | xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count) |
| 3597 | { | 3711 | { |
| 3598 | xs_html *snac_posts; | 3712 | xs_html *snac_posts; |
| 3713 | xs *header_cnt; | ||
| 3714 | if (do_count) | ||
| 3715 | header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list)); | ||
| 3716 | else | ||
| 3717 | header_cnt = xs_fmt("%s\n", header); | ||
| 3718 | |||
| 3599 | xs_html *people = xs_html_tag("div", | 3719 | xs_html *people = xs_html_tag("div", |
| 3600 | xs_html_tag("h2", | 3720 | xs_html_tag("h2", |
| 3601 | xs_html_attr("class", "snac-header"), | 3721 | xs_html_attr("class", "snac-header"), |
| 3602 | xs_html_text(header)), | 3722 | xs_html_text(header_cnt)), |
| 3603 | snac_posts = xs_html_tag("details", | 3723 | snac_posts = xs_html_tag("details", |
| 3604 | xs_html_attr("open", NULL), | 3724 | xs_html_attr("open", NULL), |
| 3605 | xs_html_tag("summary", | 3725 | xs_html_tag("summary", |
| @@ -3840,12 +3960,12 @@ xs_str *html_people(snac *user) | |||
| 3840 | 3960 | ||
| 3841 | if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { | 3961 | if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { |
| 3842 | xs_html_add(lists, | 3962 | xs_html_add(lists, |
| 3843 | html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); | 3963 | html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, 1)); |
| 3844 | } | 3964 | } |
| 3845 | 3965 | ||
| 3846 | xs_html_add(lists, | 3966 | xs_html_add(lists, |
| 3847 | html_people_list(user, wing, L("People you follow"), "i", proxy), | 3967 | html_people_list(user, wing, L("People you follow"), "i", proxy, 1), |
| 3848 | html_people_list(user, wers, L("People that follow you"), "e", proxy)); | 3968 | html_people_list(user, wers, L("People that follow you"), "e", proxy, 1)); |
| 3849 | 3969 | ||
| 3850 | xs_html *html = xs_html_tag("html", | 3970 | xs_html *html = xs_html_tag("html", |
| 3851 | html_user_head(user, NULL, NULL), | 3971 | html_user_head(user, NULL, NULL), |
| @@ -3856,6 +3976,108 @@ xs_str *html_people(snac *user) | |||
| 3856 | return xs_html_render_s(html, "<!DOCTYPE html>\n"); | 3976 | return xs_html_render_s(html, "<!DOCTYPE html>\n"); |
| 3857 | } | 3977 | } |
| 3858 | 3978 | ||
| 3979 | /* Filter list to display only posts by actor. We'll probably show | ||
| 3980 | fewer than show posts. Should we try harder to find some? */ | ||
| 3981 | xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, | ||
| 3982 | int skip, int show, int show_more, const char *page) | ||
| 3983 | { | ||
| 3984 | const char *proxy = NULL; | ||
| 3985 | xs_list *p = (xs_list *)list; | ||
| 3986 | const char *v; | ||
| 3987 | |||
| 3988 | if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) | ||
| 3989 | proxy = user->actor; | ||
| 3990 | |||
| 3991 | xs_html *body = html_user_body(user, 0); | ||
| 3992 | |||
| 3993 | xs_html *lists = xs_html_tag("div", | ||
| 3994 | xs_html_attr("class", "snac-posts")); | ||
| 3995 | |||
| 3996 | xs *foll = xs_list_append(xs_list_new(), actor); | ||
| 3997 | |||
| 3998 | xs_html_add(lists, | ||
| 3999 | html_people_list(user, foll, L("Contact's posts"), "p", proxy, 0)); | ||
| 4000 | |||
| 4001 | xs_html_add(body, lists); | ||
| 4002 | |||
| 4003 | while (xs_list_iter(&p, &v)) { | ||
| 4004 | xs *msg = NULL; | ||
| 4005 | int status; | ||
| 4006 | |||
| 4007 | status = timeline_get_by_md5(user, v, &msg); | ||
| 4008 | |||
| 4009 | if (!valid_status(status)) | ||
| 4010 | continue; | ||
| 4011 | |||
| 4012 | const char *id = xs_dict_get(msg, "id"); | ||
| 4013 | const char *by = get_atto(msg); | ||
| 4014 | xs *actor_md5 = NULL; | ||
| 4015 | xs_list *boosts = NULL; | ||
| 4016 | xs_list *likes = NULL; | ||
| 4017 | xs_list *reacts = NULL; | ||
| 4018 | /* Besides actor's posts, also show actor's boosts, and also | ||
| 4019 | posts by user with likes or reacts by actor. I.e., any | ||
| 4020 | actor's actions that user could have seen in the timeline | ||
| 4021 | or in notifications. */ | ||
| 4022 | if (!(by && strcmp(actor, by) == 0) && | ||
| 4023 | xs_list_in((boosts = object_announces(id)), | ||
| 4024 | (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 && | ||
| 4025 | (!(by && strcmp(user->actor, by) == 0) || | ||
| 4026 | (xs_list_in((likes = object_likes(id)), actor_md5) == -1 && | ||
| 4027 | xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1))) | ||
| 4028 | continue; | ||
| 4029 | |||
| 4030 | xs_html *entry = html_entry(user, msg, 0, 0, v, 1); | ||
| 4031 | |||
| 4032 | if (entry != NULL) | ||
| 4033 | xs_html_add(lists, | ||
| 4034 | entry); | ||
| 4035 | } | ||
| 4036 | |||
| 4037 | if (show_more) { | ||
| 4038 | xs *m = NULL; | ||
| 4039 | xs *m10 = NULL; | ||
| 4040 | xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show); | ||
| 4041 | |||
| 4042 | xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor); | ||
| 4043 | |||
| 4044 | if (page != NULL) | ||
| 4045 | url = xs_str_cat(url, page); | ||
| 4046 | |||
| 4047 | if (xs_str_in(url, "?") != -1) | ||
| 4048 | m = xs_fmt("%s&%s", url, ss); | ||
| 4049 | else | ||
| 4050 | m = xs_fmt("%s?%s", url, ss); | ||
| 4051 | |||
| 4052 | m10 = xs_fmt("%s0", m); | ||
| 4053 | |||
| 4054 | xs_html *more_links = xs_html_tag("p", | ||
| 4055 | xs_html_tag("a", | ||
| 4056 | xs_html_attr("href", url), | ||
| 4057 | xs_html_attr("name", "snac-more"), | ||
| 4058 | xs_html_text(L("Back to top"))), | ||
| 4059 | xs_html_text(" - "), | ||
| 4060 | xs_html_tag("a", | ||
| 4061 | xs_html_attr("href", m), | ||
| 4062 | xs_html_attr("name", "snac-more"), | ||
| 4063 | xs_html_text(L("More..."))), | ||
| 4064 | xs_html_text(" - "), | ||
| 4065 | xs_html_tag("a", | ||
| 4066 | xs_html_attr("href", m10), | ||
| 4067 | xs_html_attr("name", "snac-more"), | ||
| 4068 | xs_html_text(L("More (x 10)...")))); | ||
| 4069 | |||
| 4070 | xs_html_add(body, | ||
| 4071 | more_links); | ||
| 4072 | } | ||
| 4073 | |||
| 4074 | xs_html *html = xs_html_tag("html", | ||
| 4075 | html_user_head(user, NULL, NULL), | ||
| 4076 | xs_html_add(body, | ||
| 4077 | html_footer(user))); | ||
| 4078 | |||
| 4079 | return xs_html_render_s(html, "<!DOCTYPE html>\n"); | ||
| 4080 | } | ||
| 3859 | 4081 | ||
| 3860 | xs_str *html_notifications(snac *user, int skip, int show) | 4082 | xs_str *html_notifications(snac *user, int skip, int show) |
| 3861 | { | 4083 | { |
| @@ -3924,6 +4146,9 @@ xs_str *html_notifications(snac *user, int skip, int show) | |||
| 3924 | if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) | 4146 | if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) |
| 3925 | continue; | 4147 | continue; |
| 3926 | 4148 | ||
| 4149 | if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) | ||
| 4150 | continue; | ||
| 4151 | |||
| 3927 | object_get(id, &obj); | 4152 | object_get(id, &obj); |
| 3928 | 4153 | ||
| 3929 | const char *msg_id = NULL; | 4154 | const char *msg_id = NULL; |
| @@ -4271,8 +4496,12 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4271 | cache = 0; | 4496 | cache = 0; |
| 4272 | 4497 | ||
| 4273 | int skip = 0; | 4498 | int skip = 0; |
| 4499 | const char *max_show_default = "50"; | ||
| 4500 | int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries", | ||
| 4501 | max_show_default)); | ||
| 4274 | int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", | 4502 | int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", |
| 4275 | xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); | 4503 | xs_dict_get_def(srv_config, "max_timeline_entries", |
| 4504 | max_show_default))); | ||
| 4276 | int show = def_show; | 4505 | int show = def_show; |
| 4277 | 4506 | ||
| 4278 | if ((v = xs_dict_get(q_vars, "skip")) != NULL) | 4507 | if ((v = xs_dict_get(q_vars, "skip")) != NULL) |
| @@ -4298,6 +4527,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4298 | /* a show of 0 has no sense */ | 4527 | /* a show of 0 has no sense */ |
| 4299 | if (show == 0) | 4528 | if (show == 0) |
| 4300 | show = def_show; | 4529 | show = def_show; |
| 4530 | if (show > max_show) | ||
| 4531 | show = max_show; | ||
| 4301 | 4532 | ||
| 4302 | if (p_path == NULL) { /** public timeline **/ | 4533 | if (p_path == NULL) { /** public timeline **/ |
| 4303 | xs *h = xs_str_localtime(0, "%Y-%m.html"); | 4534 | xs *h = xs_str_localtime(0, "%Y-%m.html"); |
| @@ -4343,12 +4574,14 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4343 | status = HTTP_STATUS_UNAUTHORIZED; | 4574 | status = HTTP_STATUS_UNAUTHORIZED; |
| 4344 | } | 4575 | } |
| 4345 | else { | 4576 | else { |
| 4346 | xs *q = NULL; | 4577 | const char *q = NULL; |
| 4347 | const char *q1 = xs_dict_get(q_vars, "q"); | 4578 | xs *cq = xs_dup(xs_dict_get(q_vars, "q")); |
| 4348 | xs *url_acct = NULL; | 4579 | xs *url_acct = NULL; |
| 4349 | 4580 | ||
| 4350 | if (xs_is_string(q1)) | 4581 | if (xs_is_string(cq)) { |
| 4351 | q = xs_strip_i(xs_dup(q1)); | 4582 | cq = xs_strip_i(cq); |
| 4583 | q = cq; | ||
| 4584 | } | ||
| 4352 | 4585 | ||
| 4353 | /* searching for an URL? */ | 4586 | /* searching for an URL? */ |
| 4354 | if (q && xs_match(q, "https://*|http://*")) { | 4587 | if (q && xs_match(q, "https://*|http://*")) { |
| @@ -4413,11 +4646,11 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4413 | actor_add(actor, actor_obj); | 4646 | actor_add(actor, actor_obj); |
| 4414 | 4647 | ||
| 4415 | /* create a people list with only one element */ | 4648 | /* create a people list with only one element */ |
| 4416 | l = xs_list_append(xs_list_new(), actor); | 4649 | l = xs_list_append(l, actor); |
| 4417 | 4650 | ||
| 4418 | xs *title = xs_fmt(L("Search results for account %s"), q); | 4651 | xs *title = xs_fmt(L("Search results for account %s"), q); |
| 4419 | 4652 | ||
| 4420 | page = html_people_list(&snac, l, title, "wf", NULL); | 4653 | page = html_people_list(&snac, l, title, "wf", NULL, 1); |
| 4421 | } | 4654 | } |
| 4422 | } | 4655 | } |
| 4423 | 4656 | ||
| @@ -4553,6 +4786,37 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4553 | } | 4786 | } |
| 4554 | } | 4787 | } |
| 4555 | else | 4788 | else |
| 4789 | if (xs_startswith(p_path, "people/")) { /** a single actor **/ | ||
| 4790 | if (!login(&snac, req)) { | ||
| 4791 | *body = xs_dup(uid); | ||
| 4792 | status = HTTP_STATUS_UNAUTHORIZED; | ||
| 4793 | } | ||
| 4794 | else { | ||
| 4795 | xs *actor_dict = NULL; | ||
| 4796 | const char *actor_id = NULL; | ||
| 4797 | xs *actor = NULL; | ||
| 4798 | xs_list *page_lst = xs_split_n(p_path, "?", 2); | ||
| 4799 | xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0)); | ||
| 4800 | xs_list *l = xs_split_n(page, "/", 3); | ||
| 4801 | const char *actor_md5 = xs_list_get(l, 2); | ||
| 4802 | |||
| 4803 | if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) && | ||
| 4804 | (actor_id = xs_dict_get(actor_dict, "id")) != NULL && | ||
| 4805 | valid_status(actor_get(actor_id, &actor))) { | ||
| 4806 | int more = 0; | ||
| 4807 | xs *list = timeline_simple_list(&snac, "private", skip, show, &more); | ||
| 4808 | |||
| 4809 | *body = html_people_one(&snac, actor_id, list, skip, show, more, page); | ||
| 4810 | *b_size = strlen(*body); | ||
| 4811 | status = HTTP_STATUS_OK; | ||
| 4812 | } | ||
| 4813 | else { | ||
| 4814 | *body = xs_dup(uid); | ||
| 4815 | status = HTTP_STATUS_NOT_FOUND; | ||
| 4816 | } | ||
| 4817 | } | ||
| 4818 | } | ||
| 4819 | else | ||
| 4556 | if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ | 4820 | if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ |
| 4557 | if (!login(&snac, req)) { | 4821 | if (!login(&snac, req)) { |
| 4558 | *body = xs_dup(uid); | 4822 | *body = xs_dup(uid); |
| @@ -5665,6 +5929,33 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 5665 | 5929 | ||
| 5666 | status = HTTP_STATUS_SEE_OTHER; | 5930 | status = HTTP_STATUS_SEE_OTHER; |
| 5667 | } | 5931 | } |
| 5932 | else | ||
| 5933 | if (p_path && strcmp(p_path, "admin/muted-words") == 0) { | ||
| 5934 | const char *words = xs_dict_get(p_vars, "muted_words"); | ||
| 5935 | |||
| 5936 | if (xs_is_string(words)) { | ||
| 5937 | xs *new_words = xs_list_new(); | ||
| 5938 | xs *l = xs_split(words, "\n"); | ||
| 5939 | const char *v; | ||
| 5940 | |||
| 5941 | xs_list_foreach(l, v) { | ||
| 5942 | xs *s1 = xs_strip_i(xs_dup(v)); | ||
| 5943 | s1 = xs_replace_i(s1, " ", ""); | ||
| 5944 | |||
| 5945 | if (*s1 == '\0') | ||
| 5946 | continue; | ||
| 5947 | |||
| 5948 | xs *s2 = xs_utf8_to_lower(s1); | ||
| 5949 | |||
| 5950 | new_words = xs_list_insert_sorted(new_words, s2); | ||
| 5951 | } | ||
| 5952 | |||
| 5953 | snac.config = xs_dict_set(snac.config, "muted_words", new_words); | ||
| 5954 | user_persist(&snac, 0); | ||
| 5955 | } | ||
| 5956 | |||
| 5957 | status = HTTP_STATUS_SEE_OTHER; | ||
| 5958 | } | ||
| 5668 | 5959 | ||
| 5669 | if (status == HTTP_STATUS_SEE_OTHER) { | 5960 | if (status == HTTP_STATUS_SEE_OTHER) { |
| 5670 | const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); | 5961 | const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -18,7 +18,7 @@ | |||
| 18 | int usage(const char *cmd) | 18 | int usage(const char *cmd) |
| 19 | { | 19 | { |
| 20 | printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); | 20 | printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); |
| 21 | printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n"); | 21 | printf("Copyright (c) 2022 - 2026 grunfink et al. / MIT license\n"); |
| 22 | printf("\n"); | 22 | printf("\n"); |
| 23 | 23 | ||
| 24 | if (cmd == NULL) { | 24 | if (cmd == NULL) { |
| @@ -76,7 +76,9 @@ int usage(const char *cmd) | |||
| 76 | "list_create {basedir} {uid} {name} Creates a new list\n" | 76 | "list_create {basedir} {uid} {name} Creates a new list\n" |
| 77 | "list_remove {basedir} {uid} {name} Removes an existing list\n" | 77 | "list_remove {basedir} {uid} {name} Removes an existing list\n" |
| 78 | "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" | 78 | "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" |
| 79 | "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"; | 79 | "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n" |
| 80 | "top_ten {basedir} {uid} [{N}] Prints the most popular posts\n" | ||
| 81 | "refresh {basedir} {uid} Refreshes all actors\n"; | ||
| 80 | 82 | ||
| 81 | if (cmd == NULL) | 83 | if (cmd == NULL) |
| 82 | printf("%s", cmds); | 84 | printf("%s", cmds); |
| @@ -350,6 +352,38 @@ int main(int argc, char *argv[]) | |||
| 350 | return 0; | 352 | return 0; |
| 351 | } | 353 | } |
| 352 | 354 | ||
| 355 | if (strcmp(cmd, "top_ten") == 0) { /** **/ | ||
| 356 | int count = 10; | ||
| 357 | const char *n = GET_ARGV(); | ||
| 358 | if (xs_is_string(n)) | ||
| 359 | count = atoi(n); | ||
| 360 | |||
| 361 | xs *l = user_top_ten(&snac, count); | ||
| 362 | const xs_list *i; | ||
| 363 | |||
| 364 | xs_list_foreach(l, i) { | ||
| 365 | printf("%s %ld★ %ld↺\n", xs_list_get(i, 0), | ||
| 366 | xs_number_get_l(xs_list_get(i, 1)), | ||
| 367 | xs_number_get_l(xs_list_get(i, 2))); | ||
| 368 | } | ||
| 369 | |||
| 370 | return 0; | ||
| 371 | } | ||
| 372 | |||
| 373 | if (strcmp(cmd, "refresh") == 0) { /** **/ | ||
| 374 | xs *fwers = follower_list(&snac); | ||
| 375 | xs *fwing = following_list(&snac); | ||
| 376 | const char *id; | ||
| 377 | |||
| 378 | xs_list_foreach(fwers, id) | ||
| 379 | enqueue_actor_refresh(&snac, id, 0); | ||
| 380 | |||
| 381 | xs_list_foreach(fwing, id) | ||
| 382 | enqueue_actor_refresh(&snac, id, 0); | ||
| 383 | |||
| 384 | return 0; | ||
| 385 | } | ||
| 386 | |||
| 353 | if ((url = GET_ARGV()) == NULL) | 387 | if ((url = GET_ARGV()) == NULL) |
| 354 | return usage(cmd); | 388 | return usage(cmd); |
| 355 | 389 | ||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #ifndef NO_MASTODON_API | 4 | #ifndef NO_MASTODON_API |
| 5 | 5 | ||
| @@ -1171,6 +1171,10 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) | |||
| 1171 | const char *content = xs_dict_get(msg, "content"); | 1171 | const char *content = xs_dict_get(msg, "content"); |
| 1172 | const char *actor = xs_dict_get(msg, "actor"); | 1172 | const char *actor = xs_dict_get(msg, "actor"); |
| 1173 | const xs_list *contentl = xs_dict_get(sfrl, content); | 1173 | const xs_list *contentl = xs_dict_get(sfrl, content); |
| 1174 | |||
| 1175 | if ((snac && is_muted(snac, actor)) || is_instance_blocked(actor)) | ||
| 1176 | continue; | ||
| 1177 | |||
| 1174 | /* NOTE: idk when there are no actor, but i encountered that bug. | 1178 | /* NOTE: idk when there are no actor, but i encountered that bug. |
| 1175 | * Probably because of one of my previous attempts. | 1179 | * Probably because of one of my previous attempts. |
| 1176 | * Keeping this just in case, can remove later */ | 1180 | * Keeping this just in case, can remove later */ |
| @@ -1805,6 +1809,37 @@ xs_list *mastoapi_account_lists(snac *user, const char *uid) | |||
| 1805 | } | 1809 | } |
| 1806 | 1810 | ||
| 1807 | 1811 | ||
| 1812 | xs_list *build_childrens(const xs_dict *msg, snac *snac1) { | ||
| 1813 | xs_list *ret = xs_list_new(); | ||
| 1814 | xs *children = object_children(xs_dict_get(msg, "id")); | ||
| 1815 | char *p = children; | ||
| 1816 | const xs_str *v; | ||
| 1817 | |||
| 1818 | while (xs_list_iter(&p, &v)) { | ||
| 1819 | xs *m2 = NULL; | ||
| 1820 | |||
| 1821 | if (valid_status(timeline_get_by_md5(snac1, v, &m2))) { | ||
| 1822 | if (xs_is_null(xs_dict_get(m2, "name"))) { | ||
| 1823 | xs *st = mastoapi_status(snac1, m2); | ||
| 1824 | |||
| 1825 | if (st) { | ||
| 1826 | /* childrens children */ | ||
| 1827 | xs *childs = build_childrens(m2, snac1); | ||
| 1828 | ret = xs_list_append(ret, st); | ||
| 1829 | if (xs_list_len(childs)) { | ||
| 1830 | char *p2 = childs; | ||
| 1831 | while (xs_list_iter(&p2, &v)) | ||
| 1832 | ret = xs_list_append(ret, v); | ||
| 1833 | |||
| 1834 | } | ||
| 1835 | } | ||
| 1836 | } | ||
| 1837 | } | ||
| 1838 | } | ||
| 1839 | return ret; | ||
| 1840 | } | ||
| 1841 | |||
| 1842 | |||
| 1808 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, | 1843 | int mastoapi_get_handler(const xs_dict *req, const char *q_path, |
| 1809 | char **body, int *b_size, char **ctype, xs_str **link) | 1844 | char **body, int *b_size, char **ctype, xs_str **link) |
| 1810 | { | 1845 | { |
| @@ -2612,19 +2647,33 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2612 | if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ | 2647 | if (strcmp(cmd, "/v1/custom_emojis") == 0) { /** **/ |
| 2613 | xs *emo = emojis(); | 2648 | xs *emo = emojis(); |
| 2614 | xs *list = xs_list_new(); | 2649 | xs *list = xs_list_new(); |
| 2615 | int c = 0; | ||
| 2616 | const xs_str *k; | 2650 | const xs_str *k; |
| 2617 | const xs_val *v; | 2651 | const xs_val *v; |
| 2618 | while(xs_dict_next(emo, &k, &v, &c)) { | 2652 | xs_dict_foreach(emo, k, v) { |
| 2619 | xs *current = xs_dict_new(); | 2653 | xs *current = xs_dict_new(); |
| 2620 | if (xs_startswith(v, "https://") && xs_startswith((xs_mime_by_ext(v)), "image/")) { | 2654 | if ((xs_startswith(v, "https://") && xs_startswith((xs_mime_by_ext(v)), "image/")) || xs_type(v) == XSTYPE_DICT) { |
| 2621 | /* remove first and last colon */ | 2655 | /* remove first and last colon */ |
| 2622 | xs *shortcode = xs_replace(k, ":", ""); | 2656 | if (xs_type(v) == XSTYPE_DICT) { |
| 2623 | current = xs_dict_append(current, "shortcode", shortcode); | 2657 | const char *v2; |
| 2624 | current = xs_dict_append(current, "url", v); | 2658 | const char *cat = k; |
| 2625 | current = xs_dict_append(current, "static_url", v); | 2659 | xs_dict_foreach(v, k, v2) { |
| 2626 | current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE)); | 2660 | xs *shortcode = xs_replace(k, ":", ""); |
| 2627 | list = xs_list_append(list, current); | 2661 | current = xs_dict_append(current, "shortcode", shortcode); |
| 2662 | current = xs_dict_append(current, "url", v2); | ||
| 2663 | current = xs_dict_append(current, "static_url", v2); | ||
| 2664 | current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE)); | ||
| 2665 | current = xs_dict_append(current, "category", cat); | ||
| 2666 | list = xs_list_append(list, current); | ||
| 2667 | } | ||
| 2668 | } | ||
| 2669 | else { | ||
| 2670 | xs *shortcode = xs_replace(k, ":", ""); | ||
| 2671 | current = xs_dict_append(current, "shortcode", shortcode); | ||
| 2672 | current = xs_dict_append(current, "url", v); | ||
| 2673 | current = xs_dict_append(current, "static_url", v); | ||
| 2674 | current = xs_dict_append(current, "visible_in_picker", xs_stock(XSTYPE_TRUE)); | ||
| 2675 | list = xs_list_append(list, current); | ||
| 2676 | } | ||
| 2628 | } | 2677 | } |
| 2629 | } | 2678 | } |
| 2630 | *body = xs_json_dumps(list, 0); | 2679 | *body = xs_json_dumps(list, 0); |
| @@ -2817,8 +2866,6 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2817 | /* return ancestors and children */ | 2866 | /* return ancestors and children */ |
| 2818 | xs *anc = xs_list_new(); | 2867 | xs *anc = xs_list_new(); |
| 2819 | xs *des = xs_list_new(); | 2868 | xs *des = xs_list_new(); |
| 2820 | xs_list *p; | ||
| 2821 | const xs_str *v; | ||
| 2822 | char pid[MD5_HEX_SIZE]; | 2869 | char pid[MD5_HEX_SIZE]; |
| 2823 | 2870 | ||
| 2824 | /* build the [grand]parent list, moving up */ | 2871 | /* build the [grand]parent list, moving up */ |
| @@ -2838,21 +2885,9 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2838 | } | 2885 | } |
| 2839 | 2886 | ||
| 2840 | /* build the children list */ | 2887 | /* build the children list */ |
| 2841 | xs *children = object_children(xs_dict_get(msg, "id")); | 2888 | xs *childs = build_childrens(msg, &snac1); |
| 2842 | p = children; | 2889 | if (xs_list_len(childs) > 0) |
| 2843 | 2890 | des = xs_list_cat(des, childs); | |
| 2844 | while (xs_list_iter(&p, &v)) { | ||
| 2845 | xs *m2 = NULL; | ||
| 2846 | |||
| 2847 | if (valid_status(timeline_get_by_md5(&snac1, v, &m2))) { | ||
| 2848 | if (xs_is_null(xs_dict_get(m2, "name"))) { | ||
| 2849 | xs *st = mastoapi_status(&snac1, m2); | ||
| 2850 | |||
| 2851 | if (st) | ||
| 2852 | des = xs_list_append(des, st); | ||
| 2853 | } | ||
| 2854 | } | ||
| 2855 | } | ||
| 2856 | 2891 | ||
| 2857 | out = xs_dict_new(); | 2892 | out = xs_dict_new(); |
| 2858 | out = xs_dict_append(out, "ancestors", anc); | 2893 | out = xs_dict_append(out, "ancestors", anc); |
diff --git a/po/pt_BR.po b/po/pt_BR.po index e930490..71b0132 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po | |||
| @@ -4,13 +4,13 @@ | |||
| 4 | msgid "" | 4 | msgid "" |
| 5 | msgstr "" | 5 | msgstr "" |
| 6 | "Project-Id-Version: snac\n" | 6 | "Project-Id-Version: snac\n" |
| 7 | "PO-Revision-Date: 2025-10-11 16:48-0300\n" | 7 | "PO-Revision-Date: 2025-12-22 07:55-0300\n" |
| 8 | "Last-Translator: Daltux <@daltux@snac.daltux.net>\n" | 8 | "Last-Translator: Daltux <@daltux@snac.daltux.net>\n" |
| 9 | "Language: pt_BR\n" | 9 | "Language: pt_BR\n" |
| 10 | "MIME-Version: 1.0\n" | 10 | "MIME-Version: 1.0\n" |
| 11 | "Content-Type: text/plain; charset=UTF-8\n" | 11 | "Content-Type: text/plain; charset=UTF-8\n" |
| 12 | "Content-Transfer-Encoding: 8bit\n" | 12 | "Content-Transfer-Encoding: 8bit\n" |
| 13 | "X-Generator: Poedit 3.7\n" | 13 | "X-Generator: Geany / PoHelper 2.0\n" |
| 14 | 14 | ||
| 15 | #: html.c:534 | 15 | #: html.c:534 |
| 16 | msgid "Sensitive content: " | 16 | msgid "Sensitive content: " |
| @@ -800,17 +800,17 @@ msgid "Direct Message" | |||
| 800 | msgstr "Mensagem direta" | 800 | msgstr "Mensagem direta" |
| 801 | 801 | ||
| 802 | #: html.c:488 html.c:2534 html.c:2559 html.c:5177 | 802 | #: html.c:488 html.c:2534 html.c:2559 html.c:5177 |
| 803 | msgid "EmojiUnreact" | 803 | msgid "Desfazer EmojiReação" |
| 804 | msgstr "" | 804 | msgstr "" |
| 805 | 805 | ||
| 806 | #: html.c:488 html.c:1440 html.c:2534 html.c:2559 html.c:5188 | 806 | #: html.c:488 html.c:1440 html.c:2534 html.c:2559 html.c:5188 |
| 807 | msgid "EmojiReact" | 807 | msgid "EmojiReact" |
| 808 | msgstr "" | 808 | msgstr "EmojiReação" |
| 809 | 809 | ||
| 810 | #: html.c:2115 | 810 | #: html.c:2115 |
| 811 | msgid "Emoji react..." | 811 | msgid "Emoji react..." |
| 812 | msgstr "" | 812 | msgstr "EmojiReagir..." |
| 813 | 813 | ||
| 814 | #: html.c:2609 | 814 | #: html.c:2609 |
| 815 | msgid "Emoji reactions: " | 815 | msgid "Emoji reactions: " |
| 816 | msgstr "" | 816 | msgstr "EmojiReações: " |
| @@ -1,5 +1,5 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | 1 | /* snac - A simple, minimalistic ActivityPub instance */ |
| 2 | /* copyright (c) 2025 grunfink et al. / MIT license */ | 2 | /* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_html.h" | 5 | #include "xs_html.h" |
| @@ -10,6 +10,7 @@ | |||
| 10 | #include "xs_openssl.h" | 10 | #include "xs_openssl.h" |
| 11 | #include "xs_json.h" | 11 | #include "xs_json.h" |
| 12 | #include "xs_http.h" | 12 | #include "xs_http.h" |
| 13 | #include "xs_unicode.h" | ||
| 13 | 14 | ||
| 14 | #include "snac.h" | 15 | #include "snac.h" |
| 15 | 16 | ||
| @@ -74,7 +75,14 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline, | |||
| 74 | title = xs_regex_replace_i(title, "&[^;]+;", " "); | 75 | title = xs_regex_replace_i(title, "&[^;]+;", " "); |
| 75 | int i; | 76 | int i; |
| 76 | 77 | ||
| 77 | for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); | 78 | for (i = 0; title[i] && title[i] != '\n' && i < 50; ) { |
| 79 | const char *p = &title[i]; | ||
| 80 | unsigned int cp = xs_utf8_dec(&p); | ||
| 81 | int n = p - title; | ||
| 82 | if (cp == 0xfffd || n > 50) | ||
| 83 | break; | ||
| 84 | i = n; | ||
| 85 | } | ||
| 78 | 86 | ||
| 79 | if (title[i] != '\0') { | 87 | if (title[i] != '\0') { |
| 80 | title[i] = '\0'; | 88 | title[i] = '\0'; |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #define XS_IMPLEMENTATION | 4 | #define XS_IMPLEMENTATION |
| 5 | 5 | ||
| @@ -27,11 +27,15 @@ | |||
| 27 | #include "xs_html.h" | 27 | #include "xs_html.h" |
| 28 | #include "xs_po.h" | 28 | #include "xs_po.h" |
| 29 | #include "xs_webmention.h" | 29 | #include "xs_webmention.h" |
| 30 | #include "xs_list_tools.h" | ||
| 30 | 31 | ||
| 31 | #include "snac.h" | 32 | #include "snac.h" |
| 32 | 33 | ||
| 33 | #include <sys/time.h> | 34 | #include <sys/time.h> |
| 34 | #include <sys/stat.h> | 35 | #include <sys/stat.h> |
| 36 | #include <sys/wait.h> | ||
| 37 | #include <limits.h> | ||
| 38 | #include <stdlib.h> | ||
| 35 | 39 | ||
| 36 | xs_str *srv_basedir = NULL; | 40 | xs_str *srv_basedir = NULL; |
| 37 | xs_dict *srv_config = NULL; | 41 | xs_dict *srv_config = NULL; |
| @@ -170,3 +174,150 @@ int check_password(const char *uid, const char *passwd, const char *hash) | |||
| 170 | 174 | ||
| 171 | return ret; | 175 | return ret; |
| 172 | } | 176 | } |
| 177 | |||
| 178 | |||
| 179 | int strip_media(const char *fn) | ||
| 180 | /* strips EXIF data from a file */ | ||
| 181 | { | ||
| 182 | int ret = 0; | ||
| 183 | |||
| 184 | const xs_val *v = xs_dict_get(srv_config, "strip_exif"); | ||
| 185 | |||
| 186 | if (xs_type(v) == XSTYPE_TRUE) { | ||
| 187 | /* Heuristic: find 'user/' in the path to make it relative */ | ||
| 188 | /* This works for ~/user/..., /var/snac/user/..., etc. */ | ||
| 189 | const char *r_fn = strstr(fn, "user/"); | ||
| 190 | |||
| 191 | if (r_fn == NULL) { | ||
| 192 | /* Fallback: try to strip ~/ if present */ | ||
| 193 | if (strncmp(fn, "~/", 2) == 0) | ||
| 194 | r_fn = fn + 2; | ||
| 195 | else | ||
| 196 | r_fn = fn; | ||
| 197 | } | ||
| 198 | |||
| 199 | xs *l_fn = xs_tolower_i(xs_dup(r_fn)); | ||
| 200 | |||
| 201 | /* check image extensions */ | ||
| 202 | if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") || | ||
| 203 | xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") || | ||
| 204 | xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") || | ||
| 205 | xs_endswith(l_fn, ".avif") || xs_endswith(l_fn, ".tiff") || | ||
| 206 | xs_endswith(l_fn, ".gif") || xs_endswith(l_fn, ".bmp")) { | ||
| 207 | |||
| 208 | const char *mp = xs_dict_get(srv_config, "mogrify_path"); | ||
| 209 | if (mp == NULL) | ||
| 210 | mp = "mogrify"; | ||
| 211 | |||
| 212 | xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn); | ||
| 213 | |||
| 214 | ret = system(cmd); | ||
| 215 | |||
| 216 | if (ret != 0) { | ||
| 217 | int code = 0; | ||
| 218 | if (WIFEXITED(ret)) | ||
| 219 | code = WEXITSTATUS(ret); | ||
| 220 | |||
| 221 | if (code == 127) | ||
| 222 | srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", r_fn, mp)); | ||
| 223 | else | ||
| 224 | srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret)); | ||
| 225 | } | ||
| 226 | else | ||
| 227 | srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn)); | ||
| 228 | } | ||
| 229 | else | ||
| 230 | /* check video extensions */ | ||
| 231 | if (xs_endswith(l_fn, ".mp4") || xs_endswith(l_fn, ".m4v") || | ||
| 232 | xs_endswith(l_fn, ".mov") || xs_endswith(l_fn, ".webm") || | ||
| 233 | xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) { | ||
| 234 | |||
| 235 | const char *fp = xs_dict_get(srv_config, "ffmpeg_path"); | ||
| 236 | if (fp == NULL) | ||
| 237 | fp = "ffmpeg"; | ||
| 238 | |||
| 239 | /* ffmpeg cannot modify in-place, so we need a temp file */ | ||
| 240 | /* we must preserve valid extension for ffmpeg to guess the format */ | ||
| 241 | const char *ext = strrchr(r_fn, '.'); | ||
| 242 | if (ext == NULL) ext = ""; | ||
| 243 | xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext); | ||
| 244 | |||
| 245 | /* -map_metadata -1 strips all global metadata */ | ||
| 246 | /* -c copy copies input streams without re-encoding */ | ||
| 247 | /* we don't silence stderr so we can debug issues */ | ||
| 248 | /* we explicitly cd to srv_basedir to ensure relative paths work */ | ||
| 249 | xs *cmd = xs_fmt("cd \"%s\" && %s -y -i \"%s\" -map_metadata -1 -c copy \"%s\"", srv_basedir, fp, r_fn, tmp_fn); | ||
| 250 | |||
| 251 | ret = system(cmd); | ||
| 252 | |||
| 253 | if (ret != 0) { | ||
| 254 | int code = 0; | ||
| 255 | if (WIFEXITED(ret)) | ||
| 256 | code = WEXITSTATUS(ret); | ||
| 257 | |||
| 258 | if (code == 127) | ||
| 259 | srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'ffmpeg_path' in server.json.", r_fn, fp)); | ||
| 260 | else { | ||
| 261 | srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret)); | ||
| 262 | srv_log(xs_fmt("strip_media: command was: %s", cmd)); | ||
| 263 | } | ||
| 264 | |||
| 265 | /* try to cleanup, just in case */ | ||
| 266 | /* unlink needs full path too if we are not in basedir */ | ||
| 267 | xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn); | ||
| 268 | unlink(full_tmp_fn); | ||
| 269 | } | ||
| 270 | else { | ||
| 271 | /* rename tmp file to original */ | ||
| 272 | /* use full path for source because it was created relative to basedir */ | ||
| 273 | xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn); | ||
| 274 | |||
| 275 | if (rename(full_tmp_fn, fn) == 0) | ||
| 276 | srv_debug(1, xs_fmt("strip_media: stripped %s", fn)); | ||
| 277 | else | ||
| 278 | srv_log(xs_fmt("strip_media: error renaming %s to %s", full_tmp_fn, fn)); | ||
| 279 | } | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | return ret; | ||
| 284 | } | ||
| 285 | |||
| 286 | |||
| 287 | int check_strip_tool(void) | ||
| 288 | { | ||
| 289 | const xs_val *v = xs_dict_get(srv_config, "strip_exif"); | ||
| 290 | int ret = 1; | ||
| 291 | |||
| 292 | if (xs_type(v) == XSTYPE_TRUE) { | ||
| 293 | /* check mogrify */ | ||
| 294 | { | ||
| 295 | const char *mp = xs_dict_get(srv_config, "mogrify_path"); | ||
| 296 | if (mp == NULL) | ||
| 297 | mp = "mogrify"; | ||
| 298 | |||
| 299 | xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp); | ||
| 300 | |||
| 301 | if (system(cmd) != 0) { | ||
| 302 | srv_log(xs_fmt("check_strip_tool: '%s' not working", mp)); | ||
| 303 | ret = 0; | ||
| 304 | } | ||
| 305 | } | ||
| 306 | |||
| 307 | /* check ffmpeg */ | ||
| 308 | if (ret) { | ||
| 309 | const char *fp = xs_dict_get(srv_config, "ffmpeg_path"); | ||
| 310 | if (fp == NULL) | ||
| 311 | fp = "ffmpeg"; | ||
| 312 | |||
| 313 | xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", fp); | ||
| 314 | |||
| 315 | if (system(cmd) != 0) { | ||
| 316 | srv_log(xs_fmt("check_strip_tool: '%s' not working", fp)); | ||
| 317 | ret = 0; | ||
| 318 | } | ||
| 319 | } | ||
| 320 | } | ||
| 321 | |||
| 322 | return ret; | ||
| 323 | } | ||
| @@ -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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #define VERSION "2.86-dev" | 4 | #define VERSION "2.88" |
| 5 | 5 | ||
| 6 | #define USER_AGENT "snac/" VERSION | 6 | #define USER_AGENT "snac/" VERSION |
| 7 | 7 | ||
| @@ -105,6 +105,9 @@ int validate_uid(const char *uid); | |||
| 105 | xs_str *hash_password(const char *uid, const char *passwd, const char *nonce); | 105 | xs_str *hash_password(const char *uid, const char *passwd, const char *nonce); |
| 106 | int check_password(const char *uid, const char *passwd, const char *hash); | 106 | int check_password(const char *uid, const char *passwd, const char *hash); |
| 107 | 107 | ||
| 108 | int strip_media(const char *fn); | ||
| 109 | int check_strip_tool(void); | ||
| 110 | |||
| 108 | void srv_archive(const char *direction, const char *url, xs_dict *req, | 111 | void srv_archive(const char *direction, const char *url, xs_dict *req, |
| 109 | const char *payload, int p_size, | 112 | const char *payload, int p_size, |
| 110 | int status, xs_dict *headers, | 113 | int status, xs_dict *headers, |
| @@ -412,6 +415,7 @@ int activitypub_post_handler(const xs_dict *req, const char *q_path, | |||
| 412 | char **body, int *b_size, char **ctype); | 415 | char **body, int *b_size, char **ctype); |
| 413 | 416 | ||
| 414 | xs_dict *emojis(void); | 417 | xs_dict *emojis(void); |
| 418 | xs_dict *emojis_rm_categories(void); | ||
| 415 | xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy); | 419 | xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy); |
| 416 | xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag); | 420 | xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag); |
| 417 | xs_str *sanitize(const char *content); | 421 | xs_str *sanitize(const char *content); |
| @@ -494,3 +498,5 @@ void rss_to_timeline(snac *user, const char *url); | |||
| 494 | void rss_poll_hashtags(void); | 498 | void rss_poll_hashtags(void); |
| 495 | 499 | ||
| 496 | void data_fsck(void); | 500 | void data_fsck(void); |
| 501 | |||
| 502 | xs_list *user_top_ten(snac *user, int count); | ||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_io.h" | 5 | #include "xs_io.h" |
| @@ -11,6 +11,8 @@ | |||
| 11 | #include "xs_curl.h" | 11 | #include "xs_curl.h" |
| 12 | #include "xs_regex.h" | 12 | #include "xs_regex.h" |
| 13 | #include "xs_http.h" | 13 | #include "xs_http.h" |
| 14 | #include "xs_list_tools.h" | ||
| 15 | #include "xs_set.h" | ||
| 14 | 16 | ||
| 15 | #include "snac.h" | 17 | #include "snac.h" |
| 16 | 18 | ||
| @@ -86,6 +88,7 @@ static const char * const default_css = | |||
| 86 | " a { color: #7799dd }\n" | 88 | " a { color: #7799dd }\n" |
| 87 | " a:visited { color: #aa99dd }\n" | 89 | " a:visited { color: #aa99dd }\n" |
| 88 | "}\n" | 90 | "}\n" |
| 91 | "select { max-width: 40%; }\n" | ||
| 89 | ; | 92 | ; |
| 90 | 93 | ||
| 91 | const char *snac_blurb = | 94 | const char *snac_blurb = |
| @@ -1002,6 +1005,76 @@ void import_csv(snac *user) | |||
| 1002 | snac_log(user, xs_fmt("Cannot open file %s", fn)); | 1005 | snac_log(user, xs_fmt("Cannot open file %s", fn)); |
| 1003 | } | 1006 | } |
| 1004 | 1007 | ||
| 1008 | |||
| 1009 | static int top_ten_sort(const void *v1, const void *v2) | ||
| 1010 | { | ||
| 1011 | const xs_list *l1 = *(const xs_list **)v1; | ||
| 1012 | const xs_list *l2 = *(const xs_list **)v2; | ||
| 1013 | |||
| 1014 | const char *c1 = xs_list_get(l1, 3); | ||
| 1015 | const char *c2 = xs_list_get(l2, 3); | ||
| 1016 | |||
| 1017 | return xs_cmp(c2, c1); | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | |||
| 1021 | xs_list *user_top_ten(snac *user, int count) | ||
| 1022 | /* returns the top ten more popular posts by a user */ | ||
| 1023 | { | ||
| 1024 | xs *idx = xs_fmt("%s/private.idx", user->basedir); | ||
| 1025 | xs *list = index_list(idx, XS_ALL); | ||
| 1026 | xs *u_list = xs_list_new(); | ||
| 1027 | xs_set u; | ||
| 1028 | |||
| 1029 | xs_set_init(&u); | ||
| 1030 | |||
| 1031 | const char *md5; | ||
| 1032 | |||
| 1033 | xs_list_foreach(list, md5) { | ||
| 1034 | xs *obj = NULL; | ||
| 1035 | |||
| 1036 | if (!valid_status(object_get_by_md5(md5, &obj))) | ||
| 1037 | continue; | ||
| 1038 | |||
| 1039 | const char *id = xs_dict_get_def(obj, "id", "-"); | ||
| 1040 | |||
| 1041 | if (!is_msg_mine(user, id)) | ||
| 1042 | continue; | ||
| 1043 | |||
| 1044 | if (xs_set_add(&u, id) != 1) | ||
| 1045 | continue; | ||
| 1046 | |||
| 1047 | /* get metrics */ | ||
| 1048 | int ls = object_likes_len(id); | ||
| 1049 | int as = object_announces_len(id); | ||
| 1050 | |||
| 1051 | /* build the entry and convert to list */ | ||
| 1052 | xs *s = xs_fmt("%s\t%d\t%d\t%010d", id, ls, as, ls + as); | ||
| 1053 | xs *l = xs_split(s, "\t"); | ||
| 1054 | |||
| 1055 | u_list = xs_list_append(u_list, l); | ||
| 1056 | } | ||
| 1057 | |||
| 1058 | /* sort by the sum of likes and boosts */ | ||
| 1059 | xs *s_list = xs_list_sort(u_list, top_ten_sort); | ||
| 1060 | |||
| 1061 | xs_list *r = xs_list_new(); | ||
| 1062 | const xs_list *i; | ||
| 1063 | |||
| 1064 | xs_list_foreach(s_list, i) { | ||
| 1065 | r = xs_list_append(r, i); | ||
| 1066 | |||
| 1067 | if (--count <= 0) | ||
| 1068 | break; | ||
| 1069 | } | ||
| 1070 | |||
| 1071 | xs_set_free(&u); | ||
| 1072 | |||
| 1073 | return r; | ||
| 1074 | } | ||
| 1075 | |||
| 1076 | |||
| 1077 | |||
| 1005 | static const struct { | 1078 | static const struct { |
| 1006 | const char *proto; | 1079 | const char *proto; |
| 1007 | unsigned short default_port; | 1080 | unsigned short default_port; |
diff --git a/webfinger.c b/webfinger.c index 1ce5e76..264cb85 100644 --- a/webfinger.c +++ b/webfinger.c | |||
| @@ -1,5 +1,5 @@ | |||
| 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 - 2026 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #include "xs.h" | 4 | #include "xs.h" |
| 5 | #include "xs_json.h" | 5 | #include "xs_json.h" |
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_H | 3 | #ifndef _XS_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_CURL_H | 3 | #ifndef _XS_CURL_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | /* | 3 | /* |
| 4 | This is an intentionally-dead-simple FastCGI implementation; | 4 | This is an intentionally-dead-simple FastCGI implementation; |
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_GLOB_H | 3 | #ifndef _XS_GLOB_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_HEX_H | 3 | #ifndef _XS_HEX_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_HTML_H | 3 | #ifndef _XS_HTML_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_HTTP_H | 3 | #ifndef _XS_HTTP_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_HTTPD_H | 3 | #ifndef _XS_HTTPD_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_IO_H | 3 | #ifndef _XS_IO_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_JSON_H | 3 | #ifndef _XS_JSON_H |
| 4 | 4 | ||
| @@ -28,6 +28,8 @@ int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d); | |||
| 28 | 28 | ||
| 29 | /** IMPLEMENTATION **/ | 29 | /** IMPLEMENTATION **/ |
| 30 | 30 | ||
| 31 | #include "xs_unicode.h" | ||
| 32 | |||
| 31 | /** JSON dumps **/ | 33 | /** JSON dumps **/ |
| 32 | 34 | ||
| 33 | static void _xs_json_dump_str(const char *data, FILE *f) | 35 | static void _xs_json_dump_str(const char *data, FILE *f) |
diff --git a/xs_list_tools.h b/xs_list_tools.h new file mode 100644 index 0000000..33d4b87 --- /dev/null +++ b/xs_list_tools.h | |||
| @@ -0,0 +1,169 @@ | |||
| 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ | ||
| 2 | |||
| 3 | #ifndef _XS_LIST_TOOLS_H | ||
| 4 | |||
| 5 | #define _XS_LIST_TOOLS_H | ||
| 6 | |||
| 7 | xs_list *xs_list_insert_sorted(xs_list *list, const xs_val *nv); | ||
| 8 | xs_list *xs_list_reverse(const xs_list *l); | ||
| 9 | xs_val **xs_list_to_array(const xs_list *l, int *len); | ||
| 10 | int xs_list_sort_cmp(const void *p1, const void *p2); | ||
| 11 | int xs_list_sort_inv_cmp(const void *p1, const void *p2); | ||
| 12 | int xs_list_sort_dict_cmp(const char *field, const void *p1, const void *p2); | ||
| 13 | xs_list *xs_list_sort(const xs_list *l, int (*cmp)(const void *, const void *)); | ||
| 14 | xs_list *xs_list_shuffle(const xs_list *l); | ||
| 15 | |||
| 16 | #ifdef XS_IMPLEMENTATION | ||
| 17 | |||
| 18 | #include "xs_random.h" | ||
| 19 | |||
| 20 | xs_list *xs_list_insert_sorted(xs_list *list, const xs_val *nv) | ||
| 21 | /* inserts a string in the list in its ordered position */ | ||
| 22 | { | ||
| 23 | XS_ASSERT_TYPE(list, XSTYPE_LIST); | ||
| 24 | |||
| 25 | int offset = xs_size(list); | ||
| 26 | |||
| 27 | const xs_val *v; | ||
| 28 | xs_list_foreach(list, v) { | ||
| 29 | /* if this element is greater or equal, insert here */ | ||
| 30 | if (xs_cmp(v, nv) >= 0) { | ||
| 31 | offset = v - list; | ||
| 32 | break; | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | return _xs_list_write_litem(list, offset - 1, nv, xs_size(nv)); | ||
| 37 | } | ||
| 38 | |||
| 39 | |||
| 40 | xs_list *xs_list_reverse(const xs_list *l) | ||
| 41 | /* creates a new list as a reverse version of l */ | ||
| 42 | { | ||
| 43 | xs_list *n = xs_dup(l); | ||
| 44 | const xs_val *v; | ||
| 45 | |||
| 46 | /* move to one byte before the EOM */ | ||
| 47 | char *p = n + xs_size(n) - 1; | ||
| 48 | |||
| 49 | xs_list_foreach(l, v) { | ||
| 50 | /* size of v, plus the LITEM */ | ||
| 51 | int z = xs_size(v) + 1; | ||
| 52 | |||
| 53 | p -= z; | ||
| 54 | |||
| 55 | /* copy v, including its LITEM */ | ||
| 56 | memcpy(p, v - 1, z); | ||
| 57 | } | ||
| 58 | |||
| 59 | return n; | ||
| 60 | } | ||
| 61 | |||
| 62 | |||
| 63 | xs_val **xs_list_to_array(const xs_list *l, int *len) | ||
| 64 | /* converts a list to an array of values */ | ||
| 65 | /* must be freed after use */ | ||
| 66 | { | ||
| 67 | *len = xs_list_len(l); | ||
| 68 | xs_val **a = xs_realloc(NULL, *len * sizeof(xs_val *)); | ||
| 69 | const xs_val *v; | ||
| 70 | int n = 0; | ||
| 71 | |||
| 72 | xs_list_foreach(l, v) | ||
| 73 | a[n++] = (xs_val *)v; | ||
| 74 | |||
| 75 | return a; | ||
| 76 | } | ||
| 77 | |||
| 78 | |||
| 79 | int xs_list_sort_cmp(const void *p1, const void *p2) | ||
| 80 | /* default list sorting function */ | ||
| 81 | { | ||
| 82 | const xs_val *v1 = *(xs_val **)p1; | ||
| 83 | const xs_val *v2 = *(xs_val **)p2; | ||
| 84 | |||
| 85 | return xs_cmp(v1, v2); | ||
| 86 | } | ||
| 87 | |||
| 88 | |||
| 89 | int xs_list_sort_inv_cmp(const void *p1, const void *p2) | ||
| 90 | /* default list inverse sorting function */ | ||
| 91 | { | ||
| 92 | const xs_val *v1 = *(xs_val **)p1; | ||
| 93 | const xs_val *v2 = *(xs_val **)p2; | ||
| 94 | |||
| 95 | return xs_cmp(v2, v1); | ||
| 96 | } | ||
| 97 | |||
| 98 | |||
| 99 | int xs_list_sort_dict_cmp(const char *field, const void *p1, const void *p2) | ||
| 100 | /* compare sorting function for a field an array of dicts */ | ||
| 101 | { | ||
| 102 | const xs_dict *d1 = *(xs_val **)p1; | ||
| 103 | const xs_dict *d2 = *(xs_val **)p2; | ||
| 104 | |||
| 105 | if (xs_type(d1) != XSTYPE_DICT || xs_type(d2) != XSTYPE_DICT) | ||
| 106 | return 0; | ||
| 107 | |||
| 108 | return xs_cmp(xs_dict_get_def(d1, field, ""), | ||
| 109 | xs_dict_get_def(d2, field, "")); | ||
| 110 | } | ||
| 111 | |||
| 112 | |||
| 113 | xs_list *xs_list_sort(const xs_list *l, int (*cmp)(const void *, const void *)) | ||
| 114 | /* returns a sorted copy of l. cmp can be null for standard sorting */ | ||
| 115 | { | ||
| 116 | int sz; | ||
| 117 | xs_val **a = xs_list_to_array(l, &sz); | ||
| 118 | xs_list *nl = xs_dup(l); | ||
| 119 | char *p = nl + 1 + _XS_TYPE_SIZE; | ||
| 120 | |||
| 121 | /* sort the array */ | ||
| 122 | qsort(a, sz, sizeof(xs_val *), cmp ? cmp : xs_list_sort_cmp); | ||
| 123 | |||
| 124 | /* transfer the sorted list over the copy */ | ||
| 125 | for (int n = 0; n < sz; n++) { | ||
| 126 | /* get the litem */ | ||
| 127 | const char *e = a[n] - 1; | ||
| 128 | int z = xs_size(e); | ||
| 129 | |||
| 130 | memcpy(p, e, z); | ||
| 131 | p += z; | ||
| 132 | } | ||
| 133 | |||
| 134 | xs_free(a); | ||
| 135 | |||
| 136 | return nl; | ||
| 137 | } | ||
| 138 | |||
| 139 | |||
| 140 | xs_list *xs_list_shuffle(const xs_list *l) | ||
| 141 | /* returns a shuffled list */ | ||
| 142 | { | ||
| 143 | int sz; | ||
| 144 | xs_val **a = xs_list_to_array(l, &sz); | ||
| 145 | xs_list *nl = xs_list_new(); | ||
| 146 | unsigned int seed = 0; | ||
| 147 | |||
| 148 | xs_rnd_buf(&seed, sizeof(seed)); | ||
| 149 | |||
| 150 | /* shuffle */ | ||
| 151 | for (int n = sz - 1; n > 0; n--) { | ||
| 152 | int m = xs_rnd_int32_d(&seed) % n; | ||
| 153 | void *p = a[n]; | ||
| 154 | a[n] = a[m]; | ||
| 155 | a[m] = p; | ||
| 156 | } | ||
| 157 | |||
| 158 | for (int n = 0; n < sz; n++) | ||
| 159 | nl = xs_list_append(nl, a[n]); | ||
| 160 | |||
| 161 | xs_free(a); | ||
| 162 | |||
| 163 | return nl; | ||
| 164 | } | ||
| 165 | |||
| 166 | |||
| 167 | #endif /* XS_IMPLEMENTATION */ | ||
| 168 | |||
| 169 | #endif /* XS_LIST_TOOLS_H */ | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_MATCH_H | 3 | #ifndef _XS_MATCH_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_MIME_H | 3 | #ifndef _XS_MIME_H |
| 4 | 4 | ||
diff --git a/xs_openssl.h b/xs_openssl.h index f215bcc..64b59dd 100644 --- a/xs_openssl.h +++ b/xs_openssl.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_OPENSSL_H | 3 | #ifndef _XS_OPENSSL_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_PO_H | 3 | #ifndef _XS_PO_H |
| 4 | 4 | ||
diff --git a/xs_random.h b/xs_random.h index f936099..357f9cb 100644 --- a/xs_random.h +++ b/xs_random.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_RANDOM_H | 3 | #ifndef _XS_RANDOM_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_REGEX_H | 3 | #ifndef _XS_REGEX_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_SET_H | 3 | #ifndef _XS_SET_H |
| 4 | 4 | ||
diff --git a/xs_socket.h b/xs_socket.h index 7bf5298..70bfe98 100644 --- a/xs_socket.h +++ b/xs_socket.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_SOCKET_H | 3 | #ifndef _XS_SOCKET_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_TIME_H | 3 | #ifndef _XS_TIME_H |
| 4 | 4 | ||
diff --git a/xs_unicode.h b/xs_unicode.h index 0b4de1c..7686dcd 100644 --- a/xs_unicode.h +++ b/xs_unicode.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_UNICODE_H | 3 | #ifndef _XS_UNICODE_H |
| 4 | 4 | ||
diff --git a/xs_unix_socket.h b/xs_unix_socket.h index 462a5b3..1ef7d39 100644 --- a/xs_unix_socket.h +++ b/xs_unix_socket.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_UNIX_SOCKET_H | 3 | #ifndef _XS_UNIX_SOCKET_H |
| 4 | 4 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_URL_H | 3 | #ifndef _XS_URL_H |
| 4 | 4 | ||
diff --git a/xs_version.h b/xs_version.h index 34a7a45..92a865e 100644 --- a/xs_version.h +++ b/xs_version.h | |||
| @@ -1 +1 @@ | |||
| /* 0a8b987d7bb945fe7844411727d03ac73f417455 2025-10-14T05:21:05+02:00 */ | /* 270f9376eabd4f8e0ed3ae22a1f8eb6e06ea8b8b 2026-01-10T20:39:12+01:00 */ | ||
diff --git a/xs_webmention.h b/xs_webmention.h index f9578b4..035b3a6 100644 --- a/xs_webmention.h +++ b/xs_webmention.h | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | /* copyright (c) 2025 grunfink et al. / MIT license */ | 1 | /* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ |
| 2 | 2 | ||
| 3 | #ifndef _XS_WEBMENTION_H | 3 | #ifndef _XS_WEBMENTION_H |
| 4 | 4 | ||