diff options
| author | 2025-07-16 20:21:20 +0200 | |
|---|---|---|
| committer | 2025-07-16 20:21:20 +0200 | |
| commit | 3554a73aa50526631961efcca78c6c8eb2a16911 (patch) | |
| tree | fc4c21a65337ab8ecb4fb00aa551ee21997faf6f | |
| parent | po/de_DE.po aktualisiert (diff) | |
| parent | Updated RELEASE_NOTES. (diff) | |
| download | penes-snac2-3554a73aa50526631961efcca78c6c8eb2a16911.tar.gz penes-snac2-3554a73aa50526631961efcca78c6c8eb2a16911.tar.xz penes-snac2-3554a73aa50526631961efcca78c6c8eb2a16911.zip | |
Merge pull request 'master' (#9) from grunfink/snac2:master into master
Reviewed-on: https://codeberg.org/zen/snac2/pulls/9
| -rw-r--r-- | Makefile | 16 | ||||
| -rw-r--r-- | Makefile.NetBSD | 16 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 46 | ||||
| -rw-r--r-- | TODO.md | 4 | ||||
| -rw-r--r-- | activitypub.c | 39 | ||||
| -rw-r--r-- | data.c | 70 | ||||
| -rw-r--r-- | doc/snac.1 | 99 | ||||
| -rw-r--r-- | doc/snac.8 | 10 | ||||
| -rw-r--r-- | doc/style.css | 1 | ||||
| -rwxr-xr-x | examples/auto_follower_webhook.py | 55 | ||||
| -rw-r--r-- | format.c | 32 | ||||
| -rw-r--r-- | html.c | 224 | ||||
| -rw-r--r-- | httpd.c | 120 | ||||
| -rw-r--r-- | main.c | 166 | ||||
| -rw-r--r-- | mastoapi.c | 147 | ||||
| -rw-r--r-- | po/ru.po | 8 | ||||
| -rw-r--r-- | po/uk.po | 768 | ||||
| -rw-r--r-- | rss.c | 274 | ||||
| -rw-r--r-- | sandbox.c | 14 | ||||
| -rw-r--r-- | snac.h | 18 | ||||
| -rw-r--r-- | utils.c | 76 | ||||
| -rw-r--r-- | webfinger.c | 4 | ||||
| -rw-r--r-- | xs.h | 15 | ||||
| -rw-r--r-- | xs_fcgi.h | 4 | ||||
| -rw-r--r-- | xs_httpd.h | 12 | ||||
| -rw-r--r-- | xs_json.h | 163 | ||||
| -rw-r--r-- | xs_version.h | 2 | ||||
| -rw-r--r-- | xs_webmention.h | 42 |
28 files changed, 2079 insertions, 366 deletions
| @@ -5,7 +5,7 @@ CFLAGS?=-g -Wall -Wextra -pedantic | |||
| 5 | all: snac | 5 | all: snac |
| 6 | 6 | ||
| 7 | snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ | 7 | snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ |
| 8 | activitypub.o html.o utils.o format.o upgrade.o mastoapi.o | 8 | activitypub.o html.o utils.o format.o upgrade.o mastoapi.o rss.o |
| 9 | $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@ | 9 | $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@ |
| 10 | 10 | ||
| 11 | test: tests/smtp | 11 | test: tests/smtp |
| @@ -48,25 +48,27 @@ update-po: | |||
| 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_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 | snac.h http_codes.h | 51 | xs_webmention.h snac.h http_codes.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 \ |
| 53 | xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ | 53 | xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ |
| 54 | xs_po.h snac.h http_codes.h | 54 | xs_po.h snac.h http_codes.h |
| 55 | format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ | 55 | format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ |
| 56 | xs_time.h xs_match.h snac.h http_codes.h | 56 | xs_time.h xs_match.h xs_unicode.h snac.h http_codes.h |
| 57 | html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ | 57 | html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ |
| 58 | xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ | 58 | xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ |
| 59 | xs_random.h snac.h http_codes.h | 59 | xs_random.h snac.h http_codes.h |
| 60 | http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ | 60 | http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ |
| 61 | snac.h http_codes.h | 61 | snac.h http_codes.h |
| 62 | httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ | 62 | httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ |
| 63 | xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ | 63 | xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h \ |
| 64 | http_codes.h | 64 | xs_webmention.h snac.h http_codes.h |
| 65 | main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ | 65 | main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ |
| 66 | snac.h http_codes.h | 66 | xs_random.h snac.h http_codes.h |
| 67 | mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ | 67 | 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 | snac.h http_codes.h | 69 | xs_unicode.h snac.h http_codes.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 snac.h http_codes.h | ||
| 70 | sandbox.o: sandbox.c xs.h snac.h http_codes.h | 72 | sandbox.o: sandbox.c xs.h snac.h http_codes.h |
| 71 | 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 \ |
| 72 | 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 \ |
diff --git a/Makefile.NetBSD b/Makefile.NetBSD index ecf8205..ac5b052 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD | |||
| @@ -6,7 +6,7 @@ LDFLAGS=-lrt | |||
| 6 | all: snac | 6 | all: snac |
| 7 | 7 | ||
| 8 | snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ | 8 | snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ |
| 9 | activitypub.o html.o utils.o format.o upgrade.o mastoapi.o | 9 | activitypub.o html.o utils.o format.o upgrade.o mastoapi.o rss.o |
| 10 | $(CC) $(CFLAGS) -L/usr/pkg/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -Wl,-rpath,/usr/lib -Wl,-rpath,/usr/pkg/lib -o $@ | 10 | $(CC) $(CFLAGS) -L/usr/pkg/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -Wl,-rpath,/usr/lib -Wl,-rpath,/usr/pkg/lib -o $@ |
| 11 | 11 | ||
| 12 | 12 | ||
| @@ -37,25 +37,27 @@ uninstall: | |||
| 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_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 | snac.h http_codes.h | 40 | xs_webmention.h snac.h http_codes.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 \ |
| 42 | xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ | 42 | xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ |
| 43 | xs_po.h snac.h http_codes.h | 43 | xs_po.h snac.h http_codes.h |
| 44 | format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ | 44 | format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ |
| 45 | xs_time.h xs_match.h snac.h http_codes.h | 45 | xs_time.h xs_match.h xs_unicode.h snac.h http_codes.h |
| 46 | html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ | 46 | html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ |
| 47 | xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ | 47 | xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ |
| 48 | xs_random.h snac.h http_codes.h | 48 | xs_random.h snac.h http_codes.h |
| 49 | http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ | 49 | http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ |
| 50 | snac.h http_codes.h | 50 | snac.h http_codes.h |
| 51 | httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ | 51 | httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ |
| 52 | xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ | 52 | xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h \ |
| 53 | http_codes.h | 53 | xs_webmention.h snac.h http_codes.h |
| 54 | main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ | 54 | main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ |
| 55 | snac.h http_codes.h | 55 | xs_random.h snac.h http_codes.h |
| 56 | mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ | 56 | 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 | snac.h http_codes.h | 58 | xs_unicode.h snac.h http_codes.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 snac.h http_codes.h | ||
| 59 | sandbox.o: sandbox.c xs.h snac.h http_codes.h | 61 | sandbox.o: sandbox.c xs.h snac.h http_codes.h |
| 60 | 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 \ |
| 61 | 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 \ |
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 484e425..b932cc2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md | |||
| @@ -2,11 +2,55 @@ | |||
| 2 | 2 | ||
| 3 | ## UNRELEASED | 3 | ## UNRELEASED |
| 4 | 4 | ||
| 5 | Included a small tweak to avoid being confused by implementations that return valid webfinger queries for non-account URLs (like i.e. the Wordpress ActivityPub plugin in some configurations). | ||
| 6 | |||
| 7 | Added Ukrainian translation (contributed by wincentbalin). | ||
| 8 | |||
| 9 | ## 2.80 | ||
| 10 | |||
| 11 | Mastodon API: fixed a regression (introduced in previous version in the "boosts disappear in Tusky" fix) that interrupted timelines. | ||
| 12 | |||
| 13 | ## 2.79 | ||
| 14 | |||
| 15 | Added a server-wide Webmention hook; this way, if somebody out there (that supports Webmention) links to a user or post in this instance, a notification is sent (this is the complementary of what was implemented in version 2.76). | ||
| 16 | |||
| 17 | Fixed regression while sending email via pipe on OpenBSD. | ||
| 18 | |||
| 19 | Fixed Markdown parsing when the URL has parenthesis. | ||
| 20 | |||
| 21 | Always show the 'pending follow confirmations' section if there are any (even if the toggle is off). | ||
| 22 | |||
| 23 | If a metadata value is an account handler, it's also tried to be validated (rel="me" links). | ||
| 24 | |||
| 25 | Another search by URL tweak (this time for Pixelfed links). | ||
| 26 | |||
| 27 | Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky, added followed hashtags maintenance, other minor changes. | ||
| 28 | |||
| 29 | Renamed command-line actions `create_list` to `list_create` and `delete_list` to `list_remove`. | ||
| 30 | |||
| 31 | The default favicon URL can be changed from the server configuration. | ||
| 32 | |||
| 33 | New command-line option `export_posts`, to export all posts by a user in a JSON format compatible with the one generated by Mastodon. | ||
| 34 | |||
| 35 | The command-line options to send notes also allow an optional `-r` argument, to set the URL of a Fediverse post this note is a reply to. | ||
| 36 | |||
| 37 | ## 2.78 | ||
| 38 | |||
| 39 | Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). | ||
| 40 | |||
| 41 | Users can now configure a webhook to receive an HTTP POST for every notification. This can be useful for implementing bots that react to activities, like autorepliers, chatbots or interactive textual games (see `snac(1)` for more information). | ||
| 42 | |||
| 5 | The number of pending follow confirmations is shown next to the "people" link. | 43 | The number of pending follow confirmations is shown next to the "people" link. |
| 6 | 44 | ||
| 7 | Faster performance metrics (contributed by dandelions). | 45 | Faster performance metrics (contributed by dandelions). |
| 8 | 46 | ||
| 9 | Mastodon API: added follow confirmation endpoints. | 47 | Improved lowercasing in hashtags (contributed by postscriptum). |
| 48 | |||
| 49 | A search-by-url tweak for implementations that return 200 for invalid webfinger queries (e.g. piefed). | ||
| 50 | |||
| 51 | Mastodon API: added follow confirmation endpoints, fixed collisions in attachment file names. | ||
| 52 | |||
| 53 | Fixed potential crashes in attachment uploads. | ||
| 10 | 54 | ||
| 11 | ## 2.77 "Ugly Links Everywhere" | 55 | ## 2.77 "Ugly Links Everywhere" |
| 12 | 56 | ||
| @@ -14,6 +14,8 @@ Important: deleting a follower should do more that just delete the object, see h | |||
| 14 | 14 | ||
| 15 | ## Wishlist | 15 | ## Wishlist |
| 16 | 16 | ||
| 17 | The local purge should generate `Delete` activities for local posts. | ||
| 18 | |||
| 17 | Add account reporting. | 19 | Add account reporting. |
| 18 | 20 | ||
| 19 | Add a list option to hide member posts from the main timeline, see https://codeberg.org/grunfink/snac2/issues/383 | 21 | Add a list option to hide member posts from the main timeline, see https://codeberg.org/grunfink/snac2/issues/383 |
| @@ -22,6 +24,8 @@ The instance timeline should also show boosts from users. | |||
| 22 | 24 | ||
| 23 | Mastoapi: implement /v1/conversations. | 25 | Mastoapi: implement /v1/conversations. |
| 24 | 26 | ||
| 27 | Track "BadgeFed - ActivityPub Badges" https://github.com/tryvocalcat/badgefed | ||
| 28 | |||
| 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). | 29 | 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 | 30 | ||
| 27 | Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md | 31 | Implement "FEP-3b86: Activity Intents" https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md |
diff --git a/activitypub.c b/activitypub.c index 120b4a1..100db67 100644 --- a/activitypub.c +++ b/activitypub.c | |||
| @@ -903,7 +903,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) | |||
| 903 | if (*v == '#') { | 903 | if (*v == '#') { |
| 904 | /* hashtag */ | 904 | /* hashtag */ |
| 905 | xs *d = xs_dict_new(); | 905 | xs *d = xs_dict_new(); |
| 906 | xs *n = xs_tolower_i(xs_dup(v)); | 906 | xs *n = xs_utf8_to_lower(v); |
| 907 | xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1); | 907 | xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1); |
| 908 | xs *l = xs_fmt("<a href=\"%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", h, v); | 908 | xs *l = xs_fmt("<a href=\"%s\" class=\"mention hashtag\" rel=\"tag\">%s</a>", h, v); |
| 909 | 909 | ||
| @@ -2766,6 +2766,39 @@ void process_user_queue_item(snac *user, xs_dict *q_item) | |||
| 2766 | } | 2766 | } |
| 2767 | } | 2767 | } |
| 2768 | else | 2768 | else |
| 2769 | if (strcmp(type, "notify_webhook") == 0) { | ||
| 2770 | const char *webhook = xs_dict_get(user->config, "notify_webhook"); | ||
| 2771 | |||
| 2772 | if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/ | ||
| 2773 | const xs_dict *msg = xs_dict_get(q_item, "message"); | ||
| 2774 | int retries = xs_number_get(xs_dict_get(q_item, "retries")); | ||
| 2775 | |||
| 2776 | xs *hdrs = xs_dict_new(); | ||
| 2777 | |||
| 2778 | hdrs = xs_dict_set(hdrs, "content-type", "application/json"); | ||
| 2779 | hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); | ||
| 2780 | |||
| 2781 | xs *body = xs_json_dumps(msg, 4); | ||
| 2782 | |||
| 2783 | int status; | ||
| 2784 | xs *rsp = xs_http_request("POST", webhook, hdrs, body, strlen(body), &status, NULL, NULL, 0); | ||
| 2785 | |||
| 2786 | snac_debug(user, 0, xs_fmt("webhook post %s %d", webhook, status)); | ||
| 2787 | |||
| 2788 | if (!valid_status(status)) { | ||
| 2789 | retries++; | ||
| 2790 | |||
| 2791 | if (retries > queue_retry_max) | ||
| 2792 | snac_debug(user, 0, xs_fmt("webhook post giving up %s", webhook)); | ||
| 2793 | else { | ||
| 2794 | snac_debug(user, 0, xs_fmt("webhook post requeue %s %d", webhook, retries)); | ||
| 2795 | |||
| 2796 | enqueue_notify_webhook(user, msg, retries); | ||
| 2797 | } | ||
| 2798 | } | ||
| 2799 | } | ||
| 2800 | } | ||
| 2801 | else | ||
| 2769 | snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); | 2802 | snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); |
| 2770 | } | 2803 | } |
| 2771 | 2804 | ||
| @@ -3050,6 +3083,10 @@ void process_queue_item(xs_dict *q_item) | |||
| 3050 | } | 3083 | } |
| 3051 | } | 3084 | } |
| 3052 | else | 3085 | else |
| 3086 | if (strcmp(type, "rss_hashtag_poll") == 0) { | ||
| 3087 | rss_poll_hashtags(); | ||
| 3088 | } | ||
| 3089 | else | ||
| 3053 | srv_log(xs_fmt("unexpected q_item type '%s'", type)); | 3090 | srv_log(xs_fmt("unexpected q_item type '%s'", type)); |
| 3054 | } | 3091 | } |
| 3055 | 3092 | ||
| @@ -333,6 +333,7 @@ int user_open_by_md5(snac *snac, const char *md5) | |||
| 333 | return 0; | 333 | return 0; |
| 334 | } | 334 | } |
| 335 | 335 | ||
| 336 | |||
| 336 | int user_persist(snac *snac, int publish) | 337 | int user_persist(snac *snac, int publish) |
| 337 | /* store user */ | 338 | /* store user */ |
| 338 | { | 339 | { |
| @@ -348,7 +349,7 @@ int user_persist(snac *snac, int publish) | |||
| 348 | 349 | ||
| 349 | if (old != NULL) { | 350 | if (old != NULL) { |
| 350 | int nw = 0; | 351 | int nw = 0; |
| 351 | const char *fields[] = { "header", "avatar", "name", "bio", | 352 | const char *fields[] = { "header", "avatar", "name", "bio", "alias", "alias_raw", |
| 352 | "metadata", "latitude", "longitude", NULL }; | 353 | "metadata", "latitude", "longitude", NULL }; |
| 353 | 354 | ||
| 354 | for (int n = 0; fields[n]; n++) { | 355 | for (int n = 0; fields[n]; n++) { |
| @@ -1391,7 +1392,7 @@ xs_str *timeline_fn_by_md5(snac *snac, const char *md5) | |||
| 1391 | } | 1392 | } |
| 1392 | 1393 | ||
| 1393 | 1394 | ||
| 1394 | int timeline_here(snac *snac, const char *md5) | 1395 | int timeline_here_by_md5(snac *snac, const char *md5) |
| 1395 | /* checks if an object is in the user cache */ | 1396 | /* checks if an object is in the user cache */ |
| 1396 | { | 1397 | { |
| 1397 | xs *fn = timeline_fn_by_md5(snac, md5); | 1398 | xs *fn = timeline_fn_by_md5(snac, md5); |
| @@ -1400,6 +1401,14 @@ int timeline_here(snac *snac, const char *md5) | |||
| 1400 | } | 1401 | } |
| 1401 | 1402 | ||
| 1402 | 1403 | ||
| 1404 | int timeline_here(snac *user, const char *id) | ||
| 1405 | { | ||
| 1406 | xs *md5 = xs_md5_hex(id, strlen(id)); | ||
| 1407 | |||
| 1408 | return timeline_here_by_md5(user, md5); | ||
| 1409 | } | ||
| 1410 | |||
| 1411 | |||
| 1403 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg) | 1412 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg) |
| 1404 | /* gets a message from the timeline */ | 1413 | /* gets a message from the timeline */ |
| 1405 | { | 1414 | { |
| @@ -1515,7 +1524,7 @@ xs_list *timeline_top_level(snac *snac, const xs_list *list) | |||
| 1515 | break; | 1524 | break; |
| 1516 | 1525 | ||
| 1517 | /* well, there is a parent... but is it here? */ | 1526 | /* well, there is a parent... but is it here? */ |
| 1518 | if (!timeline_here(snac, line2)) | 1527 | if (!timeline_here_by_md5(snac, line2)) |
| 1519 | break; | 1528 | break; |
| 1520 | 1529 | ||
| 1521 | /* it's here! try again with its own parent */ | 1530 | /* it's here! try again with its own parent */ |
| @@ -2217,7 +2226,7 @@ void tag_index(const char *id, const xs_dict *obj) | |||
| 2217 | if (*name == '\0') | 2226 | if (*name == '\0') |
| 2218 | continue; | 2227 | continue; |
| 2219 | 2228 | ||
| 2220 | name = xs_tolower_i((xs_str *)name); | 2229 | name = xs_utf8_to_lower((xs_str *)name); |
| 2221 | 2230 | ||
| 2222 | xs *md5_tag = xs_md5_hex(name, strlen(name)); | 2231 | xs *md5_tag = xs_md5_hex(name, strlen(name)); |
| 2223 | xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]); | 2232 | xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]); |
| @@ -2235,7 +2244,7 @@ void tag_index(const char *id, const xs_dict *obj) | |||
| 2235 | fclose(f); | 2244 | fclose(f); |
| 2236 | } | 2245 | } |
| 2237 | 2246 | ||
| 2238 | srv_debug(0, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag)); | 2247 | srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag)); |
| 2239 | } | 2248 | } |
| 2240 | } | 2249 | } |
| 2241 | } | 2250 | } |
| @@ -2247,7 +2256,7 @@ xs_str *tag_fn(const char *tag) | |||
| 2247 | if (*tag == '#') | 2256 | if (*tag == '#') |
| 2248 | tag++; | 2257 | tag++; |
| 2249 | 2258 | ||
| 2250 | xs *lw_tag = xs_tolower_i(xs_dup(tag)); | 2259 | xs *lw_tag = xs_utf8_to_lower(tag); |
| 2251 | xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag)); | 2260 | xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag)); |
| 2252 | 2261 | ||
| 2253 | return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5); | 2262 | return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5); |
| @@ -2832,9 +2841,9 @@ int content_match(const char *file, const xs_dict *msg) | |||
| 2832 | srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn)); | 2841 | srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn)); |
| 2833 | 2842 | ||
| 2834 | /* massage content (strip HTML tags, etc.) */ | 2843 | /* massage content (strip HTML tags, etc.) */ |
| 2835 | xs *c = xs_regex_replace(v, "<[^>]+>", " "); | 2844 | xs *c1 = xs_regex_replace(v, "<[^>]+>", " "); |
| 2836 | c = xs_regex_replace_i(c, " {2,}", " "); | 2845 | c1 = xs_regex_replace_i(c1, " {2,}", " "); |
| 2837 | c = xs_tolower_i(c); | 2846 | xs *c = xs_utf8_to_lower(c1); |
| 2838 | 2847 | ||
| 2839 | while (!r && !feof(f)) { | 2848 | while (!r && !feof(f)) { |
| 2840 | xs *rx = xs_strip_i(xs_readline(f)); | 2849 | xs *rx = xs_strip_i(xs_readline(f)); |
| @@ -3158,6 +3167,9 @@ void notify_add(snac *snac, const char *type, const char *utype, | |||
| 3158 | 3167 | ||
| 3159 | pthread_mutex_unlock(&data_mutex); | 3168 | pthread_mutex_unlock(&data_mutex); |
| 3160 | } | 3169 | } |
| 3170 | |||
| 3171 | if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook"))) | ||
| 3172 | enqueue_notify_webhook(snac, noti, 0); | ||
| 3161 | } | 3173 | } |
| 3162 | 3174 | ||
| 3163 | 3175 | ||
| @@ -3513,6 +3525,46 @@ void enqueue_webmention(const xs_dict *msg) | |||
| 3513 | } | 3525 | } |
| 3514 | 3526 | ||
| 3515 | 3527 | ||
| 3528 | void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) | ||
| 3529 | /* enqueues a notification webhook */ | ||
| 3530 | { | ||
| 3531 | const char *webhook = xs_dict_get(user->config, "notify_webhook"); | ||
| 3532 | |||
| 3533 | if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/ | ||
| 3534 | xs *msg = xs_dup(noti); | ||
| 3535 | |||
| 3536 | /* add more data */ | ||
| 3537 | msg = xs_dict_set(msg, "target", user->actor); | ||
| 3538 | msg = xs_dict_set(msg, "uid", user->uid); | ||
| 3539 | msg = xs_dict_set(msg, "basedir", srv_basedir); | ||
| 3540 | msg = xs_dict_set(msg, "baseurl", srv_baseurl); | ||
| 3541 | |||
| 3542 | xs *actor_obj = NULL; | ||
| 3543 | |||
| 3544 | if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj) | ||
| 3545 | msg = xs_dict_set(msg, "account", actor_obj); | ||
| 3546 | |||
| 3547 | /* if this post is a reply, also add the inReplyTo object */ | ||
| 3548 | const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo"); | ||
| 3549 | |||
| 3550 | if (xs_is_string(in_reply_to)) { | ||
| 3551 | xs *irt_obj = NULL; | ||
| 3552 | |||
| 3553 | if (valid_status(object_get(in_reply_to, &irt_obj))) | ||
| 3554 | msg = xs_dict_set(msg, "reply", irt_obj); | ||
| 3555 | } | ||
| 3556 | |||
| 3557 | xs *qmsg = _new_qmsg("notify_webhook", msg, retries); | ||
| 3558 | const char *ntid = xs_dict_get(qmsg, "ntid"); | ||
| 3559 | xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid); | ||
| 3560 | |||
| 3561 | qmsg = _enqueue_put(fn, qmsg); | ||
| 3562 | |||
| 3563 | snac_debug(user, 1, xs_fmt("notify_webhook")); | ||
| 3564 | } | ||
| 3565 | } | ||
| 3566 | |||
| 3567 | |||
| 3516 | int was_question_voted(snac *user, const char *id) | 3568 | int was_question_voted(snac *user, const char *id) |
| 3517 | /* returns true if the user voted in this poll */ | 3569 | /* returns true if the user voted in this poll */ |
| 3518 | { | 3570 | { |
| @@ -24,9 +24,9 @@ For file and data formats, see | |||
| 24 | .Ss Web Interface | 24 | .Ss Web Interface |
| 25 | The web interface provided by | 25 | The web interface provided by |
| 26 | .Nm | 26 | .Nm |
| 27 | is split in two data streams: the public timeline and the | 27 | is split in three data streams: the public timeline, the private |
| 28 | private timeline. There are no other feeds like the server-scoped | 28 | timeline and the instance timeline. There are no other feeds like |
| 29 | or the federated firehoses provided by other similar ActivityPub | 29 | the federated firehoses provided by other similar ActivityPub |
| 30 | implementations like Mastodon or Pleroma. | 30 | implementations like Mastodon or Pleroma. |
| 31 | .Pp | 31 | .Pp |
| 32 | The public timeline, also called the local timeline, is what an | 32 | The public timeline, also called the local timeline, is what an |
| @@ -67,9 +67,23 @@ sent to those people you mention in the post body. | |||
| 67 | If you fill this optional text field with the URL of another one's | 67 | If you fill this optional text field with the URL of another one's |
| 68 | post, your text will be considered as a reply to it, not a | 68 | post, your text will be considered as a reply to it, not a |
| 69 | standalone one. | 69 | standalone one. |
| 70 | .It Draft | ||
| 71 | If you set this checkbox, your text will not be sent when you | ||
| 72 | push the Post button, but stored for later modification in | ||
| 73 | the "Drafts" section. | ||
| 74 | .It Scheduled post... | ||
| 75 | This dropdown menu allows setting a date and time for the | ||
| 76 | post publication. | ||
| 77 | .It Attachments... | ||
| 78 | This dropdown menu allows uploading media attachments (images, | ||
| 79 | audio, video, etc.) to your post. | ||
| 80 | .It Poll... | ||
| 81 | this dropdown menu gives access to the voting options, that | ||
| 82 | will make your post a poll. You can set the options to be | ||
| 83 | voted, if it's a multiple choice poll and the due date. | ||
| 70 | .El | 84 | .El |
| 71 | .Pp | 85 | .Pp |
| 72 | More options are hidden under a toggle control. They are the | 86 | More options are hidden under dropdown menus. They are the |
| 73 | following: | 87 | following: |
| 74 | .Bl -tag -offset indent | 88 | .Bl -tag -offset indent |
| 75 | .It Follow (by URL or user@host) | 89 | .It Follow (by URL or user@host) |
| @@ -85,7 +99,9 @@ liked. | |||
| 85 | This option opens the user setup dialog. | 99 | This option opens the user setup dialog. |
| 86 | .It Followed hashtags... | 100 | .It Followed hashtags... |
| 87 | Enter here the list of hashtags you want to follow, one | 101 | Enter here the list of hashtags you want to follow, one |
| 88 | per line, with or without the # symbol. | 102 | per line, with or without the # symbol. Since version 2.78, |
| 103 | URLs to RSS feeds of ActivityPub objects are also allowed | ||
| 104 | (like e.g. https://mastodon.social/tags/bloomscrolling). | ||
| 89 | .It Blocked hashtags... | 105 | .It Blocked hashtags... |
| 90 | Enter here the list of hashtags you want to block, one | 106 | Enter here the list of hashtags you want to block, one |
| 91 | per line, with or without the # symbol. | 107 | per line, with or without the # symbol. |
| @@ -125,6 +141,10 @@ standard ntfy.sh server), fill the two provided | |||
| 125 | fields (ntfy server/topic and, if protected, the token). | 141 | fields (ntfy server/topic and, if protected, the token). |
| 126 | You need to refer to the https://ntfy.sh web site for | 142 | You need to refer to the https://ntfy.sh web site for |
| 127 | more information on this process. | 143 | more information on this process. |
| 144 | .It Notify webhook | ||
| 145 | If this is set to an URL, an HTTP POST will be sent to it | ||
| 146 | whenever a new notification happens (see the 'Webhook for | ||
| 147 | notifications' section below for more information). | ||
| 128 | .It Maximum days to keep posts | 148 | .It Maximum days to keep posts |
| 129 | This numeric value specifies the number of days to pass before | 149 | This numeric value specifies the number of days to pass before |
| 130 | posts (yours and others') will be purged. This value overrides | 150 | posts (yours and others') will be purged. This value overrides |
| @@ -264,7 +284,7 @@ Requests an object and dumps it to stdout. This is a very low | |||
| 264 | level command that is not very useful to you. | 284 | level command that is not very useful to you. |
| 265 | .It Cm announce Ar basedir Ar uid Ar url | 285 | .It Cm announce Ar basedir Ar uid Ar url |
| 266 | Announces (boosts) a post via its URL. | 286 | Announces (boosts) a post via its URL. |
| 267 | .It Cm note Ar basedir Ar uid Ar text Op file file ... | 287 | .It Cm note Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo |
| 268 | Enqueues a Create + Note message to all followers. If the | 288 | Enqueues a Create + Note message to all followers. If the |
| 269 | .Ar text | 289 | .Ar text |
| 270 | argument is -e, the external editor defined by the EDITOR | 290 | argument is -e, the external editor defined by the EDITOR |
| @@ -272,10 +292,11 @@ environment variable will be invoked to prepare a message; if | |||
| 272 | it's - (a lonely hyphen), the post content will be read from stdin. | 292 | it's - (a lonely hyphen), the post content will be read from stdin. |
| 273 | The rest of command line arguments are treated as media files to be | 293 | The rest of command line arguments are treated as media files to be |
| 274 | attached to the post. The LANG environment variable (if defined) is used | 294 | attached to the post. The LANG environment variable (if defined) is used |
| 275 | as the post language. | 295 | as the post language. An optional URL to a Fediverse post, prefixed by -r, |
| 276 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... | 296 | can be specified for this note to be a reply to. |
| 297 | .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo | ||
| 277 | Like the previous one, but creates an "unlisted" (or "quiet public") post. | 298 | Like the previous one, but creates an "unlisted" (or "quiet public") post. |
| 278 | .It Cm note_mention Ar basedir Ar uid Ar text Op file file ... | 299 | .It Cm note_mention Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo |
| 279 | Like the previous one, but creates a post only for accounts mentioned | 300 | Like the previous one, but creates a post only for accounts mentioned |
| 280 | in the post body. | 301 | in the post body. |
| 281 | .It Cm block Ar basedir Ar instance_url | 302 | .It Cm block Ar basedir Ar instance_url |
| @@ -285,9 +306,9 @@ blocked without further inspection. | |||
| 285 | .It Cm unblock Ar basedir Ar instance_url | 306 | .It Cm unblock Ar basedir Ar instance_url |
| 286 | Unblocks a previously blocked instance. | 307 | Unblocks a previously blocked instance. |
| 287 | .It Cm verify_links Ar basedir Ar uid | 308 | .It Cm verify_links Ar basedir Ar uid |
| 288 | Verifies all links stored as metadata for the given user. This verification | 309 | Verifies all links or account handles stored as metadata for the given user. |
| 289 | is done by downloading the link content and searching for a link back to | 310 | This verification is done by downloading the link content and searching for |
| 290 | the | 311 | a link back to the |
| 291 | .Nm | 312 | .Nm |
| 292 | user url that also contains a rel="me" attribute. These links are specially | 313 | user url that also contains a rel="me" attribute. These links are specially |
| 293 | marked as verified in the user's public timeline and also via the Mastodon API. | 314 | marked as verified in the user's public timeline and also via the Mastodon API. |
| @@ -300,6 +321,13 @@ subdirectory inside the user directory: | |||
| 300 | .Pa blocked_accounts.csv , | 321 | .Pa blocked_accounts.csv , |
| 301 | .Pa lists.csv , and | 322 | .Pa lists.csv , and |
| 302 | .Pa following_accounts.csv . | 323 | .Pa following_accounts.csv . |
| 324 | .It Cm export_posts Ar basedir Ar uid | ||
| 325 | Exports all posts written by the user to the file | ||
| 326 | .Pa outbox.json | ||
| 327 | inside the | ||
| 328 | .Pa export/ | ||
| 329 | subdirectory inside the user directory. The format is compatible with the | ||
| 330 | one generated by the equivalent option in Mastodon. | ||
| 303 | .It Cm alias Ar basedir Ar uid Ar "@account@remotehost" | 331 | .It Cm alias Ar basedir Ar uid Ar "@account@remotehost" |
| 304 | Sets an account as an alias of this one. This is a necessary step to migrate | 332 | Sets an account as an alias of this one. This is a necessary step to migrate |
| 305 | an account to a remote Mastodon instance (see | 333 | an account to a remote Mastodon instance (see |
| @@ -354,10 +382,10 @@ subdirectory of a user's directory inside the server base directory. | |||
| 354 | Prints the name of the user created lists. | 382 | Prints the name of the user created lists. |
| 355 | .It Cm list_members Ar basedir Ar uid Ar name | 383 | .It Cm list_members Ar basedir Ar uid Ar name |
| 356 | Prints the list of actors in the named list. | 384 | Prints the list of actors in the named list. |
| 357 | .It Cm create_list Ar basedir Ar uid Ar name | 385 | .It Cm list_create Ar basedir Ar uid Ar name |
| 358 | Creates a new list. | 386 | Creates a new list. |
| 359 | .It Cm delete_list Ar basedir Ar uid Ar name | 387 | .It Cm list_remove Ar basedir Ar uid Ar name |
| 360 | Deletes an existing list. | 388 | Removes an existing list. |
| 361 | .It Cm list_add Ar basedir Ar uid Ar name Ar account | 389 | .It Cm list_add Ar basedir Ar uid Ar name Ar account |
| 362 | Adds an account (by its @name@host handle or actor URL) to a list. | 390 | Adds an account (by its @name@host handle or actor URL) to a list. |
| 363 | .It Cm list_del Ar basedir Ar uid Ar name Ar actor_url | 391 | .It Cm list_del Ar basedir Ar uid Ar name Ar actor_url |
| @@ -408,6 +436,43 @@ You can obtain an API Token by connecting to the following URL: | |||
| 408 | https://$SNAC_HOST/oauth/x-snac-get-token | 436 | https://$SNAC_HOST/oauth/x-snac-get-token |
| 409 | .Ed | 437 | .Ed |
| 410 | .Pp | 438 | .Pp |
| 439 | .Ss Webhook for notifications | ||
| 440 | Since version 2.78, users can set the URL to a webhook that will receive | ||
| 441 | an HTTP POST with every notification (in JSON format). This can be used to | ||
| 442 | implement some automation whenever a new activity happens, like autorepliers, | ||
| 443 | chatbots, interactive textual games or whatever. The | ||
| 444 | .Pa examples/ | ||
| 445 | subdirectory contains a tiny Python program that implements an auto-follower | ||
| 446 | for every new follow. The JSON notification object includes the following data: | ||
| 447 | .Bl -tag -offset indent | ||
| 448 | .It id | ||
| 449 | a unique notification identifier | ||
| 450 | .It actor | ||
| 451 | the origin actor id | ||
| 452 | .It target | ||
| 453 | the target actor id | ||
| 454 | .It date | ||
| 455 | the notification date | ||
| 456 | .It msg | ||
| 457 | the full ActivityPub action JSON object | ||
| 458 | .It objid | ||
| 459 | the object identifier (extracted from msg, may be null) | ||
| 460 | .It type | ||
| 461 | the action type (extracted from msg) | ||
| 462 | .It utype | ||
| 463 | the action subtype (extracted from msg, may be null) | ||
| 464 | .It uid | ||
| 465 | the user identifier (account name) | ||
| 466 | .It basedir | ||
| 467 | the server base directory | ||
| 468 | .It baseurl | ||
| 469 | the server base URL | ||
| 470 | .It account | ||
| 471 | the origin actor object | ||
| 472 | .It reply | ||
| 473 | the activity this post is a reply to (may not exist) | ||
| 474 | .El | ||
| 475 | .Pp | ||
| 411 | .Sh ENVIRONMENT | 476 | .Sh ENVIRONMENT |
| 412 | .Bl -tag -width Ds | 477 | .Bl -tag -width Ds |
| 413 | .It SNAC_BASEDIR | 478 | .It SNAC_BASEDIR |
| @@ -422,9 +487,9 @@ Overrides the debugging level from the server 'dbglevel' configuration | |||
| 422 | variable. Set it to an integer value. The higher, the deeper in meaningless | 487 | variable. Set it to an integer value. The higher, the deeper in meaningless |
| 423 | verbiage you'll find yourself into. | 488 | verbiage you'll find yourself into. |
| 424 | .It Ev EDITOR | 489 | .It Ev EDITOR |
| 425 | The user-preferred interactive text editor to prepare messages. | 490 | The user-preferred interactive text editor to prepare notes. |
| 426 | .It Ev LANG | 491 | .It Ev LANG |
| 427 | The language of the post when sending messages. | 492 | The language of the post when sending notes from the command line. |
| 428 | .El | 493 | .El |
| 429 | .Sh SEE ALSO | 494 | .Sh SEE ALSO |
| 430 | .Xr snac 5 , | 495 | .Xr snac 5 , |
| @@ -207,6 +207,8 @@ fields are set (see below), they are also shown. | |||
| 207 | The email address of the instance administrator (optional). | 207 | The email address of the instance administrator (optional). |
| 208 | .It Ic admin_account | 208 | .It Ic admin_account |
| 209 | The user name of the instance administrator (optional). | 209 | The user name of the instance administrator (optional). |
| 210 | .It Ic title | ||
| 211 | The name of the instance (optional). | ||
| 210 | .It Ic short_description | 212 | .It Ic short_description |
| 211 | A textual short description about the instance (optional). | 213 | A textual short description about the instance (optional). |
| 212 | .It Ic short_description_raw | 214 | .It Ic short_description_raw |
| @@ -277,6 +279,14 @@ the usual one, like in smtp://mail.example.com:587. | |||
| 277 | .It Ic smtp_user | 279 | .It Ic smtp_user |
| 278 | .It Ic smtp_password | 280 | .It Ic smtp_password |
| 279 | To be filled if the SMTP server defined by the previous directive needs credentials. | 281 | To be filled if the SMTP server defined by the previous directive needs credentials. |
| 282 | .It Ic rss_hashtag_poll_hours | ||
| 283 | The periodic number of hours hashtag RSS are polled (default: 4). It has a minimum | ||
| 284 | value of 1 to avoid hammering servers. | ||
| 285 | .It Ic disable_notify_webhook | ||
| 286 | Since version 2.78, users can set a webhook URL to receive notifications. Set this | ||
| 287 | to true if you don't want your users to have this privilege. | ||
| 288 | .It Ic favicon_url | ||
| 289 | The URL to a favicon. If it's not set, the default one is used instead. | ||
| 280 | .El | 290 | .El |
| 281 | .Pp | 291 | .Pp |
| 282 | You must restart the server to make effective these changes. | 292 | You must restart the server to make effective these changes. |
diff --git a/doc/style.css b/doc/style.css index 027fc43..5289332 100644 --- a/doc/style.css +++ b/doc/style.css | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; } | 1 | body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; } |
| 2 | pre { overflow-x: scroll; } | 2 | pre { overflow-x: scroll; } |
| 3 | blockquote { font-style: italic; } | ||
| 3 | .snac-embedded-video, img { max-width: 100% } | 4 | .snac-embedded-video, img { max-width: 100% } |
| 4 | .snac-origin { font-size: 85% } | 5 | .snac-origin { font-size: 85% } |
| 5 | .snac-score { float: right; font-size: 85% } | 6 | .snac-score { float: right; font-size: 85% } |
diff --git a/examples/auto_follower_webhook.py b/examples/auto_follower_webhook.py new file mode 100755 index 0000000..eb632a3 --- /dev/null +++ b/examples/auto_follower_webhook.py | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | |||
| 3 | # This is an example of a snac webhook that automatically follows all new followers. | ||
| 4 | |||
| 5 | # To use it, configure the user webhook to be http://localhost:12345, and run this program. | ||
| 6 | |||
| 7 | # copyright (C) 2025 grunfink et al. / MIT license | ||
| 8 | |||
| 9 | from http.server import BaseHTTPRequestHandler, HTTPServer | ||
| 10 | import time | ||
| 11 | import json | ||
| 12 | import os | ||
| 13 | |||
| 14 | host_name = "localhost" | ||
| 15 | server_port = 12345 | ||
| 16 | |||
| 17 | class SnacAutoResponderServer(BaseHTTPRequestHandler): | ||
| 18 | |||
| 19 | def do_POST(self): | ||
| 20 | self.send_response(200) | ||
| 21 | self.end_headers() | ||
| 22 | |||
| 23 | content_type = self.headers["content-type"] | ||
| 24 | content_length = int(self.headers["content-length"]) | ||
| 25 | payload = self.rfile.read(content_length).decode("utf-8") | ||
| 26 | |||
| 27 | if content_type == "application/json": | ||
| 28 | try: | ||
| 29 | noti = json.loads(payload) | ||
| 30 | |||
| 31 | type = noti["type"] | ||
| 32 | |||
| 33 | if type == "Follow": | ||
| 34 | actor = noti["actor"] | ||
| 35 | uid = noti["uid"] | ||
| 36 | basedir = noti["basedir"] | ||
| 37 | |||
| 38 | cmd = "snac follow %s %s %s" % (basedir, uid, actor) | ||
| 39 | |||
| 40 | os.system(cmd) | ||
| 41 | |||
| 42 | except: | ||
| 43 | print("Error parsing notification") | ||
| 44 | |||
| 45 | if __name__ == "__main__": | ||
| 46 | webServer = HTTPServer((host_name, server_port), SnacAutoResponderServer) | ||
| 47 | print("Webhook started http://%s:%s" % (host_name, server_port)) | ||
| 48 | |||
| 49 | try: | ||
| 50 | webServer.serve_forever() | ||
| 51 | except KeyboardInterrupt: | ||
| 52 | pass | ||
| 53 | |||
| 54 | webServer.server_close() | ||
| 55 | print("Webhook stopped.") | ||
| @@ -8,6 +8,7 @@ | |||
| 8 | #include "xs_json.h" | 8 | #include "xs_json.h" |
| 9 | #include "xs_time.h" | 9 | #include "xs_time.h" |
| 10 | #include "xs_match.h" | 10 | #include "xs_match.h" |
| 11 | #include "xs_unicode.h" | ||
| 11 | 12 | ||
| 12 | #include "snac.h" | 13 | #include "snac.h" |
| 13 | 14 | ||
| @@ -95,8 +96,8 @@ static xs_str *format_line(const char *line, xs_list **attach) | |||
| 95 | "~~[^~]+~~" "|" | 96 | "~~[^~]+~~" "|" |
| 96 | "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|" | 97 | "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|" |
| 97 | "__[^_]+__" "|" //anzu | 98 | "__[^_]+__" "|" //anzu |
| 98 | "!\\[[^]]+\\]\\([^\\)]+\\)" "|" | 99 | "!\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|" |
| 99 | "\\[[^]]+\\]\\([^\\)]+\\)" "|" | 100 | "\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|" |
| 100 | "[a-z]+:/" "/" NOSPACE "|" | 101 | "[a-z]+:/" "/" NOSPACE "|" |
| 101 | "(mailto|xmpp):[^@[:space:]]+@" NOSPACE | 102 | "(mailto|xmpp):[^@[:space:]]+@" NOSPACE |
| 102 | ")"); | 103 | ")"); |
| @@ -148,14 +149,15 @@ static xs_str *format_line(const char *line, xs_list **attach) | |||
| 148 | else | 149 | else |
| 149 | if (*v == '[') { | 150 | if (*v == '[') { |
| 150 | /* markdown-like links [label](url) */ | 151 | /* markdown-like links [label](url) */ |
| 151 | xs *w = xs_strip_chars_i( | 152 | xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@"); |
| 152 | xs_replace_i(xs_replace(v, "#", "#"), "@", "@"), | ||
| 153 | "; | 153 | xs *l = xs_split_n(w, "](", 1); |
| 155 | 154 | ||
| 156 | if (xs_list_len(l) == 2) { | 155 | if (xs_list_len(l) == 2) { |
| 157 | const char *name = xs_list_get(l, 0); | 156 | xs *name = xs_dup(xs_list_get(l, 0)); |
| 158 | const char *url = xs_list_get(l, 1); | 157 | xs *url = xs_dup(xs_list_get(l, 1)); |
| 158 | |||
| 159 | name = xs_crop_i(name, 1, 0); | ||
| 160 | url = xs_crop_i(url, 0, -1); | ||
| 159 | 161 | ||
| 160 | xs *link = xs_fmt("<a href=\"%s\">%s</a>", url, name); | 162 | xs *link = xs_fmt("<a href=\"%s\">%s</a>", url, name); |
| 161 | 163 | ||
| @@ -167,15 +169,17 @@ static xs_str *format_line(const char *line, xs_list **attach) | |||
| 167 | else | 169 | else |
| 168 | if (*v == '!') { | 170 | if (*v == '!') { |
| 169 | /* markdown-like images  */ | 171 | /* markdown-like images  */ |
| 170 | xs *w = xs_strip_chars_i( | 172 | xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@"); |
| 171 | xs_replace_i(xs_replace(v, "#", "#"), "@", "@"), | ||
| 172 | "; | 173 | xs *l = xs_split_n(w, "](", 1); |
| 174 | 174 | ||
| 175 | if (xs_list_len(l) == 2) { | 175 | if (xs_list_len(l) == 2) { |
| 176 | const char *alt_text = xs_list_get(l, 0); | 176 | xs *alt_text = xs_dup(xs_list_get(l, 0)); |
| 177 | const char *img_url = xs_list_get(l, 1); | 177 | xs *img_url = xs_dup(xs_list_get(l, 1)); |
| 178 | const char *mime = xs_mime_by_ext(img_url); | 178 | |
| 179 | alt_text = xs_crop_i(alt_text, 2, 0); | ||
| 180 | img_url = xs_crop_i(img_url, 0, -1); | ||
| 181 | |||
| 182 | const char *mime = xs_mime_by_ext(img_url); | ||
| 179 | 183 | ||
| 180 | if (attach != NULL && xs_startswith(mime, "image/")) { | 184 | if (attach != NULL && xs_startswith(mime, "image/")) { |
| 181 | const xs_dict *ad; | 185 | const xs_dict *ad; |
| @@ -443,7 +447,7 @@ xs_str *sanitize(const char *content) | |||
| 443 | if (n & 0x1) { | 447 | if (n & 0x1) { |
| 444 | xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1)); | 448 | xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1)); |
| 445 | xs *l1 = xs_split_n(s1, " ", 1); | 449 | xs *l1 = xs_split_n(s1, " ", 1); |
| 446 | xs *tag = xs_tolower_i(xs_dup(xs_list_get(l1, 0))); | 450 | xs *tag = xs_utf8_to_lower(xs_list_get(l1, 0)); |
| 447 | xs *s2 = NULL; | 451 | xs *s2 = NULL; |
| 448 | int i; | 452 | int i; |
| 449 | 453 | ||
| @@ -636,13 +636,18 @@ static xs_html *html_base_head(void) | |||
| 636 | xs_html_attr("content", USER_AGENT))); | 636 | xs_html_attr("content", USER_AGENT))); |
| 637 | 637 | ||
| 638 | /* add server CSS and favicon */ | 638 | /* add server CSS and favicon */ |
| 639 | xs *f; | 639 | xs *f = NULL; |
| 640 | f = xs_fmt("%s/favicon.ico", srv_baseurl); | 640 | const char *favicon = xs_dict_get(srv_config, "favicon_url"); |
| 641 | |||
| 642 | if (xs_is_string(favicon)) | ||
| 643 | f = xs_dup(favicon); | ||
| 644 | else | ||
| 645 | f = xs_fmt("%s/favicon.ico", srv_baseurl); | ||
| 646 | |||
| 641 | const xs_list *p = xs_dict_get(srv_config, "cssurls"); | 647 | const xs_list *p = xs_dict_get(srv_config, "cssurls"); |
| 642 | const char *v; | 648 | const char *v; |
| 643 | int c = 0; | ||
| 644 | 649 | ||
| 645 | while (xs_list_next(p, &v, &c)) { | 650 | xs_list_foreach(p, v) { |
| 646 | xs_html_add(head, | 651 | xs_html_add(head, |
| 647 | xs_html_sctag("link", | 652 | xs_html_sctag("link", |
| 648 | xs_html_attr("rel", "stylesheet"), | 653 | xs_html_attr("rel", "stylesheet"), |
| @@ -863,6 +868,14 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url) | |||
| 863 | xs_html_attr("type", "application/activity+json"), | 868 | xs_html_attr("type", "application/activity+json"), |
| 864 | xs_html_attr("href", url ? url : user->actor))); | 869 | xs_html_attr("href", url ? url : user->actor))); |
| 865 | 870 | ||
| 871 | /* webmention hook */ | ||
| 872 | xs *wbh = xs_fmt("%s/webmention-hook", srv_baseurl); | ||
| 873 | |||
| 874 | xs_html_add(head, | ||
| 875 | xs_html_sctag("link", | ||
| 876 | xs_html_attr("rel", "webmention"), | ||
| 877 | xs_html_attr("href", wbh))); | ||
| 878 | |||
| 866 | return head; | 879 | return head; |
| 867 | } | 880 | } |
| 868 | 881 | ||
| @@ -1077,10 +1090,17 @@ static xs_html *html_user_body(snac *user, int read_only) | |||
| 1077 | while (xs_dict_next(metadata, &k, &v, &c)) { | 1090 | while (xs_dict_next(metadata, &k, &v, &c)) { |
| 1078 | xs_html *value; | 1091 | xs_html *value; |
| 1079 | 1092 | ||
| 1080 | if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) { | 1093 | if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/") || *v == '@') { |
| 1081 | /* is this link validated? */ | 1094 | /* is this link validated? */ |
| 1082 | xs *verified_link = NULL; | 1095 | xs *verified_link = NULL; |
| 1083 | const xs_number *val_time = xs_dict_get(val_links, v); | 1096 | const xs_number *val_time = xs_dict_get(val_links, v); |
| 1097 | const char *url = NULL; | ||
| 1098 | |||
| 1099 | if (xs_is_string(val_time)) { | ||
| 1100 | /* resolve again, as it may be an account handle */ | ||
| 1101 | url = val_time; | ||
| 1102 | val_time = xs_dict_get(val_links, val_time); | ||
| 1103 | } | ||
| 1084 | 1104 | ||
| 1085 | if (xs_type(val_time) == XSTYPE_NUMBER) { | 1105 | if (xs_type(val_time) == XSTYPE_NUMBER) { |
| 1086 | time_t t = xs_number_get(val_time); | 1106 | time_t t = xs_number_get(val_time); |
| @@ -1098,13 +1118,13 @@ static xs_html *html_user_body(snac *user, int read_only) | |||
| 1098 | xs_html_tag("a", | 1118 | xs_html_tag("a", |
| 1099 | xs_html_attr("rel", "me"), | 1119 | xs_html_attr("rel", "me"), |
| 1100 | xs_html_attr("target", "_blank"), | 1120 | xs_html_attr("target", "_blank"), |
| 1101 | xs_html_attr("href", v), | 1121 | xs_html_attr("href", url ? url : v), |
| 1102 | xs_html_text(v))); | 1122 | xs_html_text(v))); |
| 1103 | } | 1123 | } |
| 1104 | else { | 1124 | else { |
| 1105 | value = xs_html_tag("a", | 1125 | value = xs_html_tag("a", |
| 1106 | xs_html_attr("rel", "me"), | 1126 | xs_html_attr("rel", "me"), |
| 1107 | xs_html_attr("href", v), | 1127 | xs_html_attr("href", url ? url : v), |
| 1108 | xs_html_text(v)); | 1128 | xs_html_text(v)); |
| 1109 | } | 1129 | } |
| 1110 | } | 1130 | } |
| @@ -1290,6 +1310,7 @@ xs_html *html_top_controls(snac *user) | |||
| 1290 | const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics"); | 1310 | const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics"); |
| 1291 | const char *latitude = xs_dict_get_def(user->config, "latitude", ""); | 1311 | const char *latitude = xs_dict_get_def(user->config, "latitude", ""); |
| 1292 | const char *longitude = xs_dict_get_def(user->config, "longitude", ""); | 1312 | const char *longitude = xs_dict_get_def(user->config, "longitude", ""); |
| 1313 | const char *webhook = xs_dict_get_def(user->config, "notify_webhook", ""); | ||
| 1293 | 1314 | ||
| 1294 | xs *metadata = NULL; | 1315 | xs *metadata = NULL; |
| 1295 | const xs_dict *md = xs_dict_get(user->config, "metadata"); | 1316 | const xs_dict *md = xs_dict_get(user->config, "metadata"); |
| @@ -1452,6 +1473,14 @@ xs_html *html_top_controls(snac *user) | |||
| 1452 | xs_html_attr("value", ntfy_token), | 1473 | xs_html_attr("value", ntfy_token), |
| 1453 | xs_html_attr("placeholder", L("ntfy token - if needed")))), | 1474 | xs_html_attr("placeholder", L("ntfy token - if needed")))), |
| 1454 | xs_html_tag("p", | 1475 | xs_html_tag("p", |
| 1476 | xs_html_text(L("Notify webhook:")), | ||
| 1477 | xs_html_sctag("br", NULL), | ||
| 1478 | xs_html_sctag("input", | ||
| 1479 | xs_html_attr("type", "url"), | ||
| 1480 | xs_html_attr("name", "notify_webhook"), | ||
| 1481 | xs_html_attr("value", webhook), | ||
| 1482 | xs_html_attr("placeholder", L("http://example.com/webhook")))), | ||
| 1483 | xs_html_tag("p", | ||
| 1455 | xs_html_text(L("Maximum days to keep posts (0: server settings):")), | 1484 | xs_html_text(L("Maximum days to keep posts (0: server settings):")), |
| 1456 | xs_html_sctag("br", NULL), | 1485 | xs_html_sctag("br", NULL), |
| 1457 | xs_html_sctag("input", | 1486 | xs_html_sctag("input", |
| @@ -1601,7 +1630,8 @@ xs_html *html_top_controls(snac *user) | |||
| 1601 | xs_html_attr("name", "followed_hashtags"), | 1630 | xs_html_attr("name", "followed_hashtags"), |
| 1602 | xs_html_attr("cols", "40"), | 1631 | xs_html_attr("cols", "40"), |
| 1603 | xs_html_attr("rows", "4"), | 1632 | xs_html_attr("rows", "4"), |
| 1604 | xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"), | 1633 | xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic\nhttps:/" |
| 1634 | "/mastodon.social/tags/dogs"), | ||
| 1605 | xs_html_text(followed_hashtags)), | 1635 | xs_html_text(followed_hashtags)), |
| 1606 | 1636 | ||
| 1607 | xs_html_tag("br", NULL), | 1637 | xs_html_tag("br", NULL), |
| @@ -1978,8 +2008,13 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 1978 | } | 2008 | } |
| 1979 | 2009 | ||
| 1980 | if ((user == NULL || strcmp(actor, user->actor) != 0) | 2010 | if ((user == NULL || strcmp(actor, user->actor) != 0) |
| 1981 | && !valid_status(actor_get(actor, NULL))) | 2011 | && !valid_status(actor_get(actor, NULL))) { |
| 2012 | |||
| 2013 | if (user) | ||
| 2014 | enqueue_actor_refresh(user, actor, 0); | ||
| 2015 | |||
| 1982 | return NULL; | 2016 | return NULL; |
| 2017 | } | ||
| 1983 | 2018 | ||
| 1984 | /** html_entry top tag **/ | 2019 | /** html_entry top tag **/ |
| 1985 | xs_html *entry_top = xs_html_tag("div", NULL); | 2020 | xs_html *entry_top = xs_html_tag("div", NULL); |
| @@ -2115,9 +2150,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2115 | const char *parent = get_in_reply_to(msg); | 2150 | const char *parent = get_in_reply_to(msg); |
| 2116 | 2151 | ||
| 2117 | if (!xs_is_null(parent) && *parent) { | 2152 | if (!xs_is_null(parent) && *parent) { |
| 2118 | xs *md5 = xs_md5_hex(parent, strlen(parent)); | 2153 | if (!timeline_here(user, parent)) { |
| 2119 | |||
| 2120 | if (!timeline_here(user, md5)) { | ||
| 2121 | xs_html_add(post_header, | 2154 | xs_html_add(post_header, |
| 2122 | xs_html_tag("div", | 2155 | xs_html_tag("div", |
| 2123 | xs_html_attr("class", "snac-origin"), | 2156 | xs_html_attr("class", "snac-origin"), |
| @@ -2419,6 +2452,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, | |||
| 2419 | const char *o_href = xs_dict_get(a, "href"); | 2452 | const char *o_href = xs_dict_get(a, "href"); |
| 2420 | const char *name = xs_dict_get(a, "name"); | 2453 | const char *name = xs_dict_get(a, "name"); |
| 2421 | 2454 | ||
| 2455 | if (!xs_is_string(type) || !xs_is_string(o_href)) | ||
| 2456 | continue; | ||
| 2457 | |||
| 2422 | /* if this URL is already in the post content, skip */ | 2458 | /* if this URL is already in the post content, skip */ |
| 2423 | if (content && xs_str_in(content, o_href) != -1) | 2459 | if (content && xs_str_in(content, o_href) != -1) |
| 2424 | continue; | 2460 | continue; |
| @@ -3284,12 +3320,12 @@ xs_str *html_people(snac *user) | |||
| 3284 | 3320 | ||
| 3285 | xs *wing = following_list(user); | 3321 | xs *wing = following_list(user); |
| 3286 | xs *wers = follower_list(user); | 3322 | xs *wers = follower_list(user); |
| 3323 | xs *pending = pending_list(user); | ||
| 3287 | 3324 | ||
| 3288 | xs_html *lists = xs_html_tag("div", | 3325 | xs_html *lists = xs_html_tag("div", |
| 3289 | xs_html_attr("class", "snac-posts")); | 3326 | xs_html_attr("class", "snac-posts")); |
| 3290 | 3327 | ||
| 3291 | if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) { | 3328 | if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { |
| 3292 | xs *pending = pending_list(user); | ||
| 3293 | xs_html_add(lists, | 3329 | xs_html_add(lists, |
| 3294 | html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); | 3330 | html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); |
| 3295 | } | 3331 | } |
| @@ -3384,13 +3420,15 @@ xs_str *html_notifications(snac *user, int skip, int show) | |||
| 3384 | 3420 | ||
| 3385 | const char *actor_id = xs_dict_get(noti, "actor"); | 3421 | const char *actor_id = xs_dict_get(noti, "actor"); |
| 3386 | xs *actor = NULL; | 3422 | xs *actor = NULL; |
| 3423 | xs *a_name = NULL; | ||
| 3387 | 3424 | ||
| 3388 | if (!valid_status(actor_get(actor_id, &actor))) | 3425 | if (valid_status(actor_get(actor_id, &actor))) |
| 3389 | continue; | 3426 | a_name = actor_name(actor, proxy); |
| 3427 | else | ||
| 3428 | a_name = xs_dup(actor_id); | ||
| 3390 | 3429 | ||
| 3391 | xs *a_name = actor_name(actor, proxy); | 3430 | xs *label_sanitized = sanitize(type); |
| 3392 | xs *label_sanatized = sanitize(type); | 3431 | const char *label = label_sanitized; |
| 3393 | const char *label = label_sanatized; | ||
| 3394 | 3432 | ||
| 3395 | if (strcmp(type, "Create") == 0) | 3433 | if (strcmp(type, "Create") == 0) |
| 3396 | label = L("Mention"); | 3434 | label = L("Mention"); |
| @@ -3462,10 +3500,11 @@ xs_str *html_notifications(snac *user, int skip, int show) | |||
| 3462 | html_label); | 3500 | html_label); |
| 3463 | 3501 | ||
| 3464 | if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { | 3502 | if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { |
| 3465 | xs_html_add(entry, | 3503 | if (actor) |
| 3466 | xs_html_tag("div", | 3504 | xs_html_add(entry, |
| 3467 | xs_html_attr("class", "snac-post"), | 3505 | xs_html_tag("div", |
| 3468 | html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL))); | 3506 | xs_html_attr("class", "snac-post"), |
| 3507 | html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL, NULL))); | ||
| 3469 | } | 3508 | } |
| 3470 | else | 3509 | else |
| 3471 | if (strcmp(type, "Move") == 0) { | 3510 | if (strcmp(type, "Move") == 0) { |
| @@ -3498,6 +3537,13 @@ xs_str *html_notifications(snac *user, int skip, int show) | |||
| 3498 | xs_html_text(L("Context")))), | 3537 | xs_html_text(L("Context")))), |
| 3499 | h); | 3538 | h); |
| 3500 | } | 3539 | } |
| 3540 | else | ||
| 3541 | xs_html_add(entry, | ||
| 3542 | xs_html_tag("p", | ||
| 3543 | xs_html_text(L("Location: ")), | ||
| 3544 | xs_html_tag("a", | ||
| 3545 | xs_html_attr("href", id), | ||
| 3546 | xs_html_text(id)))); | ||
| 3501 | } | 3547 | } |
| 3502 | 3548 | ||
| 3503 | if (strcmp(v, n_time) > 0) { | 3549 | if (strcmp(v, n_time) > 0) { |
| @@ -3748,7 +3794,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3748 | /* may by an actor; try a webfinger */ | 3794 | /* may by an actor; try a webfinger */ |
| 3749 | xs *actor_obj = NULL; | 3795 | xs *actor_obj = NULL; |
| 3750 | 3796 | ||
| 3751 | if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) { | 3797 | if (valid_status(webfinger_request(q, &actor_obj, &url_acct)) && xs_is_string(url_acct)) { |
| 3752 | /* it's an actor; do the dirty trick of changing q to the account name */ | 3798 | /* it's an actor; do the dirty trick of changing q to the account name */ |
| 3753 | q = url_acct; | 3799 | q = url_acct; |
| 3754 | } | 3800 | } |
| @@ -3773,12 +3819,18 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3773 | q = url_acct; | 3819 | q = url_acct; |
| 3774 | 3820 | ||
| 3775 | /* add the post to the timeline */ | 3821 | /* add the post to the timeline */ |
| 3776 | xs *md5 = xs_md5_hex(q, strlen(q)); | 3822 | if (!timeline_here(&snac, q)) |
| 3777 | |||
| 3778 | if (!timeline_here(&snac, md5)) | ||
| 3779 | timeline_add(&snac, q, object); | 3823 | timeline_add(&snac, q, object); |
| 3780 | } | 3824 | } |
| 3781 | } | 3825 | } |
| 3826 | else { | ||
| 3827 | /* retry webfinger, this time with the 'official' id */ | ||
| 3828 | const char *id = xs_dict_get(object, "id"); | ||
| 3829 | |||
| 3830 | if (xs_is_string(id) && valid_status(webfinger_request(id, &actor_obj, &url_acct)) && | ||
| 3831 | xs_is_string(url_acct)) | ||
| 3832 | q = url_acct; | ||
| 3833 | } | ||
| 3782 | } | 3834 | } |
| 3783 | } | 3835 | } |
| 3784 | 3836 | ||
| @@ -3917,7 +3969,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3917 | xs *l = xs_split(p_path, "/"); | 3969 | xs *l = xs_split(p_path, "/"); |
| 3918 | const char *md5 = xs_list_get(l, -1); | 3970 | const char *md5 = xs_list_get(l, -1); |
| 3919 | 3971 | ||
| 3920 | if (md5 && *md5 && timeline_here(&snac, md5)) { | 3972 | if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) { |
| 3921 | xs *list0 = xs_list_append(xs_list_new(), md5); | 3973 | xs *list0 = xs_list_append(xs_list_new(), md5); |
| 3922 | xs *list = timeline_top_level(&snac, list0); | 3974 | xs *list = timeline_top_level(&snac, list0); |
| 3923 | 3975 | ||
| @@ -4127,7 +4179,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 4127 | xs_dict_get(srv_config, "host")); | 4179 | xs_dict_get(srv_config, "host")); |
| 4128 | xs *rss_link = xs_fmt("%s.rss", snac.actor); | 4180 | xs *rss_link = xs_fmt("%s.rss", snac.actor); |
| 4129 | 4181 | ||
| 4130 | *body = timeline_to_rss(&snac, elems, rss_title, rss_link, bio); | 4182 | *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio); |
| 4131 | *b_size = strlen(*body); | 4183 | *b_size = strlen(*body); |
| 4132 | *ctype = "application/rss+xml; charset=utf-8"; | 4184 | *ctype = "application/rss+xml; charset=utf-8"; |
| 4133 | status = HTTP_STATUS_OK; | 4185 | status = HTTP_STATUS_OK; |
| @@ -4365,7 +4417,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 4365 | xs_rnd_buf(rnd, sizeof(rnd)); | 4417 | xs_rnd_buf(rnd, sizeof(rnd)); |
| 4366 | 4418 | ||
| 4367 | const char *ext = strrchr(fn, '.'); | 4419 | const char *ext = strrchr(fn, '.'); |
| 4368 | xs *hash = xs_md5_hex(rnd, strlen(rnd)); | 4420 | xs *hash = xs_md5_hex(rnd, sizeof(rnd)); |
| 4369 | xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); | 4421 | xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); |
| 4370 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); | 4422 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); |
| 4371 | int fo = xs_number_get(xs_list_get(attach_file, 1)); | 4423 | int fo = xs_number_get(xs_list_get(attach_file, 1)); |
| @@ -4825,6 +4877,8 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 4825 | snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); | 4877 | snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); |
| 4826 | snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); | 4878 | snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); |
| 4827 | 4879 | ||
| 4880 | snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", "")); | ||
| 4881 | |||
| 4828 | if ((v = xs_dict_get(p_vars, "metadata")) != NULL) | 4882 | if ((v = xs_dict_get(p_vars, "metadata")) != NULL) |
| 4829 | snac.config = xs_dict_set(snac.config, "metadata", v); | 4883 | snac.config = xs_dict_set(snac.config, "metadata", v); |
| 4830 | 4884 | ||
| @@ -4960,9 +5014,16 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 4960 | if (*s1 == '\0') | 5014 | if (*s1 == '\0') |
| 4961 | continue; | 5015 | continue; |
| 4962 | 5016 | ||
| 4963 | xs *s2 = xs_utf8_to_lower(s1); | 5017 | xs *s2 = NULL; |
| 4964 | if (*s2 != '#') | 5018 | |
| 4965 | s2 = xs_str_prepend_i(s2, "#"); | 5019 | if (xs_startswith(s1, "https:/")) |
| 5020 | s2 = xs_dup(s1); | ||
| 5021 | else { | ||
| 5022 | s2 = xs_utf8_to_lower(s1); | ||
| 5023 | |||
| 5024 | if (*s2 != '#') | ||
| 5025 | s2 = xs_str_prepend_i(s2, "#"); | ||
| 5026 | } | ||
| 4966 | 5027 | ||
| 4967 | new_hashtags = xs_list_append(new_hashtags, s2); | 5028 | new_hashtags = xs_list_append(new_hashtags, s2); |
| 4968 | } | 5029 | } |
| @@ -5024,100 +5085,3 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 5024 | 5085 | ||
| 5025 | return status; | 5086 | return status; |
| 5026 | } | 5087 | } |
| 5027 | |||
| 5028 | |||
| 5029 | xs_str *timeline_to_rss(snac *user, const xs_list *timeline, | ||
| 5030 | const char *title, const char *link, const char *desc) | ||
| 5031 | /* converts a timeline to rss */ | ||
| 5032 | { | ||
| 5033 | xs_html *rss = xs_html_tag("rss", | ||
| 5034 | xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"), | ||
| 5035 | xs_html_attr("version", "2.0"), | ||
| 5036 | xs_html_attr("xmlns:atom", "http:/" "/www.w3.org/2005/Atom")); | ||
| 5037 | |||
| 5038 | xs_html *channel = xs_html_tag("channel", | ||
| 5039 | xs_html_tag("title", | ||
| 5040 | xs_html_text(title)), | ||
| 5041 | xs_html_tag("language", | ||
| 5042 | xs_html_text("en")), | ||
| 5043 | xs_html_tag("link", | ||
| 5044 | xs_html_text(link)), | ||
| 5045 | xs_html_sctag("atom:link", | ||
| 5046 | xs_html_attr("href", link), | ||
| 5047 | xs_html_attr("rel", "self"), | ||
| 5048 | xs_html_attr("type", "application/rss+xml")), | ||
| 5049 | xs_html_tag("generator", | ||
| 5050 | xs_html_text(USER_AGENT)), | ||
| 5051 | xs_html_tag("description", | ||
| 5052 | xs_html_text(desc))); | ||
| 5053 | |||
| 5054 | xs_html_add(rss, channel); | ||
| 5055 | |||
| 5056 | int cnt = 0; | ||
| 5057 | const char *v; | ||
| 5058 | |||
| 5059 | xs_list_foreach(timeline, v) { | ||
| 5060 | xs *msg = NULL; | ||
| 5061 | |||
| 5062 | if (user) { | ||
| 5063 | if (!valid_status(timeline_get_by_md5(user, v, &msg))) | ||
| 5064 | continue; | ||
| 5065 | } | ||
| 5066 | else { | ||
| 5067 | if (!valid_status(object_get_by_md5(v, &msg))) | ||
| 5068 | continue; | ||
| 5069 | } | ||
| 5070 | |||
| 5071 | const char *id = xs_dict_get(msg, "id"); | ||
| 5072 | const char *content = xs_dict_get(msg, "content"); | ||
| 5073 | const char *published = xs_dict_get(msg, "published"); | ||
| 5074 | |||
| 5075 | if (user && !xs_startswith(id, user->actor)) | ||
| 5076 | continue; | ||
| 5077 | |||
| 5078 | if (!id || !content || !published) | ||
| 5079 | continue; | ||
| 5080 | |||
| 5081 | /* create a title with the first line of the content */ | ||
| 5082 | xs *title = xs_replace(content, "<br>", "\n"); | ||
| 5083 | title = xs_regex_replace_i(title, "<[^>]+>", " "); | ||
| 5084 | title = xs_regex_replace_i(title, "&[^;]+;", " "); | ||
| 5085 | int i; | ||
| 5086 | |||
| 5087 | for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); | ||
| 5088 | |||
| 5089 | if (title[i] != '\0') { | ||
| 5090 | title[i] = '\0'; | ||
| 5091 | title = xs_str_cat(title, "..."); | ||
| 5092 | } | ||
| 5093 | |||
| 5094 | title = xs_strip_i(title); | ||
| 5095 | |||
| 5096 | /* convert the date */ | ||
| 5097 | time_t t = xs_parse_iso_date(published, 0); | ||
| 5098 | xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000"); | ||
| 5099 | |||
| 5100 | /* if it's the first one, add it to the header */ | ||
| 5101 | if (cnt == 0) | ||
| 5102 | xs_html_add(channel, | ||
| 5103 | xs_html_tag("lastBuildDate", | ||
| 5104 | xs_html_text(rss_date))); | ||
| 5105 | |||
| 5106 | xs_html_add(channel, | ||
| 5107 | xs_html_tag("item", | ||
| 5108 | xs_html_tag("title", | ||
| 5109 | xs_html_text(title)), | ||
| 5110 | xs_html_tag("link", | ||
| 5111 | xs_html_text(id)), | ||
| 5112 | xs_html_tag("guid", | ||
| 5113 | xs_html_text(id)), | ||
| 5114 | xs_html_tag("pubDate", | ||
| 5115 | xs_html_text(rss_date)), | ||
| 5116 | xs_html_tag("description", | ||
| 5117 | xs_html_text(content)))); | ||
| 5118 | |||
| 5119 | cnt++; | ||
| 5120 | } | ||
| 5121 | |||
| 5122 | return xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); | ||
| 5123 | } | ||
| @@ -12,6 +12,7 @@ | |||
| 12 | #include "xs_openssl.h" | 12 | #include "xs_openssl.h" |
| 13 | #include "xs_fcgi.h" | 13 | #include "xs_fcgi.h" |
| 14 | #include "xs_html.h" | 14 | #include "xs_html.h" |
| 15 | #include "xs_webmention.h" | ||
| 15 | 16 | ||
| 16 | #include "snac.h" | 17 | #include "snac.h" |
| 17 | 18 | ||
| @@ -65,7 +66,9 @@ const char *nodeinfo_2_0_template = "" | |||
| 65 | "\"services\":{\"outbound\":[],\"inbound\":[]}," | 66 | "\"services\":{\"outbound\":[],\"inbound\":[]}," |
| 66 | "\"usage\":{\"users\":{\"total\":%d,\"activeMonth\":%d,\"activeHalfyear\":%d}," | 67 | "\"usage\":{\"users\":{\"total\":%d,\"activeMonth\":%d,\"activeHalfyear\":%d}," |
| 67 | "\"localPosts\":%d}," | 68 | "\"localPosts\":%d}," |
| 68 | "\"openRegistrations\":false,\"metadata\":{}}"; | 69 | "\"openRegistrations\":false,\"metadata\":{" |
| 70 | "\"nodeDescription\":\"%s\",\"nodeName\":\"%s\"" | ||
| 71 | "}}"; | ||
| 69 | 72 | ||
| 70 | xs_str *nodeinfo_2_0(void) | 73 | xs_str *nodeinfo_2_0(void) |
| 71 | /* builds a nodeinfo json object */ | 74 | /* builds a nodeinfo json object */ |
| @@ -98,7 +101,10 @@ xs_str *nodeinfo_2_0(void) | |||
| 98 | n_posts += index_len(pidxfn); | 101 | n_posts += index_len(pidxfn); |
| 99 | } | 102 | } |
| 100 | 103 | ||
| 101 | return xs_fmt(nodeinfo_2_0_template, n_utotal, n_umonth, n_uhyear, n_posts); | 104 | const char *name = xs_dict_get_def(srv_config, "title", ""); |
| 105 | const char *desc = xs_dict_get_def(srv_config, "short_description", ""); | ||
| 106 | |||
| 107 | return xs_fmt(nodeinfo_2_0_template, n_utotal, n_umonth, n_uhyear, n_posts, desc, name); | ||
| 102 | } | 108 | } |
| 103 | 109 | ||
| 104 | 110 | ||
| @@ -244,7 +250,7 @@ int server_get_handler(xs_dict *req, const char *q_path, | |||
| 244 | if (!xs_is_null(accept) && strcmp(accept, "application/rss+xml") == 0) { | 250 | if (!xs_is_null(accept) && strcmp(accept, "application/rss+xml") == 0) { |
| 245 | xs *link = xs_fmt("%s/?t=%s", srv_baseurl, t); | 251 | xs *link = xs_fmt("%s/?t=%s", srv_baseurl, t); |
| 246 | 252 | ||
| 247 | *body = timeline_to_rss(NULL, tl, link, link, link); | 253 | *body = rss_from_timeline(NULL, tl, link, link, link); |
| 248 | *ctype = "application/rss+xml; charset=utf-8"; | 254 | *ctype = "application/rss+xml; charset=utf-8"; |
| 249 | } | 255 | } |
| 250 | else { | 256 | else { |
| @@ -373,6 +379,68 @@ int server_get_handler(xs_dict *req, const char *q_path, | |||
| 373 | } | 379 | } |
| 374 | 380 | ||
| 375 | 381 | ||
| 382 | int server_post_handler(const xs_dict *req, const char *q_path, | ||
| 383 | char *payload, int p_size, | ||
| 384 | char **body, int *b_size, char **ctype) | ||
| 385 | { | ||
| 386 | int status = 0; | ||
| 387 | |||
| 388 | (void)payload; | ||
| 389 | (void)p_size; | ||
| 390 | (void)body; | ||
| 391 | (void)b_size; | ||
| 392 | (void)ctype; | ||
| 393 | |||
| 394 | if (strcmp(q_path, "/webmention-hook") == 0) { | ||
| 395 | status = HTTP_STATUS_BAD_REQUEST; | ||
| 396 | |||
| 397 | const xs_dict *p_vars = xs_dict_get(req, "p_vars"); | ||
| 398 | |||
| 399 | if (!xs_is_dict(p_vars)) | ||
| 400 | return status; | ||
| 401 | |||
| 402 | const char *source = xs_dict_get(p_vars, "source"); | ||
| 403 | const char *target = xs_dict_get(p_vars, "target"); | ||
| 404 | |||
| 405 | if (!xs_is_string(source) || !xs_is_string(target)) { | ||
| 406 | srv_debug(1, xs_fmt("webmention-hook bad source or target")); | ||
| 407 | return status; | ||
| 408 | } | ||
| 409 | |||
| 410 | if (!xs_startswith(target, srv_baseurl)) { | ||
| 411 | srv_debug(1, xs_fmt("webmention-hook unknown target %s", target)); | ||
| 412 | return status; | ||
| 413 | } | ||
| 414 | |||
| 415 | /* get the user */ | ||
| 416 | xs *s1 = xs_replace(target, srv_baseurl, ""); | ||
| 417 | |||
| 418 | xs *l1 = xs_split(s1, "/"); | ||
| 419 | const char *uid = xs_list_get(l1, 1); | ||
| 420 | snac user; | ||
| 421 | |||
| 422 | if (!xs_is_string(uid) || !user_open(&user, uid)) { | ||
| 423 | srv_debug(1, xs_fmt("webmention-hook cannot find user for %s", target)); | ||
| 424 | return status; | ||
| 425 | } | ||
| 426 | |||
| 427 | int r = xs_webmention_hook(source, target, USER_AGENT); | ||
| 428 | |||
| 429 | if (r > 0) { | ||
| 430 | notify_add(&user, "Webmention", NULL, source, target, xs_stock(XSTYPE_DICT)); | ||
| 431 | timeline_touch(&user); | ||
| 432 | } | ||
| 433 | |||
| 434 | srv_log(xs_fmt("webmention-hook source=%s target=%s %d", source, target, r)); | ||
| 435 | |||
| 436 | user_free(&user); | ||
| 437 | status = HTTP_STATUS_OK; | ||
| 438 | } | ||
| 439 | |||
| 440 | return status; | ||
| 441 | } | ||
| 442 | |||
| 443 | |||
| 376 | void httpd_connection(FILE *f) | 444 | void httpd_connection(FILE *f) |
| 377 | /* the connection processor */ | 445 | /* the connection processor */ |
| 378 | { | 446 | { |
| @@ -444,6 +512,10 @@ void httpd_connection(FILE *f) | |||
| 444 | else | 512 | else |
| 445 | if (strcmp(method, "POST") == 0) { | 513 | if (strcmp(method, "POST") == 0) { |
| 446 | 514 | ||
| 515 | if (status == 0) | ||
| 516 | status = server_post_handler(req, q_path, | ||
| 517 | payload, p_size, &body, &b_size, &ctype); | ||
| 518 | |||
| 447 | #ifndef NO_MASTODON_API | 519 | #ifndef NO_MASTODON_API |
| 448 | if (status == 0) | 520 | if (status == 0) |
| 449 | status = oauth_post_handler(req, q_path, | 521 | status = oauth_post_handler(req, q_path, |
| @@ -705,34 +777,36 @@ static pthread_cond_t sleep_cond; | |||
| 705 | static void *background_thread(void *arg) | 777 | static void *background_thread(void *arg) |
| 706 | /* background thread (queue management and other things) */ | 778 | /* background thread (queue management and other things) */ |
| 707 | { | 779 | { |
| 708 | time_t purge_time; | 780 | time_t t, purge_time, rss_time; |
| 709 | 781 | ||
| 710 | (void)arg; | 782 | (void)arg; |
| 711 | 783 | ||
| 784 | t = time(NULL); | ||
| 785 | |||
| 712 | /* first purge time */ | 786 | /* first purge time */ |
| 713 | purge_time = time(NULL) + 10 * 60; | 787 | purge_time = t + 10 * 60; |
| 788 | |||
| 789 | /* first RSS polling time */ | ||
| 790 | rss_time = t + 15 * 60; | ||
| 714 | 791 | ||
| 715 | srv_log(xs_fmt("background thread started")); | 792 | srv_log(xs_fmt("background thread started")); |
| 716 | 793 | ||
| 717 | while (p_state->srv_running) { | 794 | while (p_state->srv_running) { |
| 718 | time_t t; | ||
| 719 | int cnt = 0; | 795 | int cnt = 0; |
| 720 | 796 | ||
| 721 | p_state->th_state[0] = THST_QUEUE; | 797 | p_state->th_state[0] = THST_QUEUE; |
| 722 | 798 | ||
| 723 | { | 799 | { |
| 724 | xs *list = user_list(); | 800 | xs *list = user_list(); |
| 725 | char *p; | ||
| 726 | const char *uid; | 801 | const char *uid; |
| 727 | 802 | ||
| 728 | /* process queues for all users */ | 803 | /* process queues for all users */ |
| 729 | p = list; | 804 | xs_list_foreach(list, uid) { |
| 730 | while (xs_list_iter(&p, &uid)) { | 805 | snac user; |
| 731 | snac snac; | ||
| 732 | 806 | ||
| 733 | if (user_open(&snac, uid)) { | 807 | if (user_open(&user, uid)) { |
| 734 | cnt += process_user_queue(&snac); | 808 | cnt += process_user_queue(&user); |
| 735 | user_free(&snac); | 809 | user_free(&user); |
| 736 | } | 810 | } |
| 737 | } | 811 | } |
| 738 | } | 812 | } |
| @@ -740,8 +814,10 @@ static void *background_thread(void *arg) | |||
| 740 | /* global queue */ | 814 | /* global queue */ |
| 741 | cnt += process_queue(); | 815 | cnt += process_queue(); |
| 742 | 816 | ||
| 817 | t = time(NULL); | ||
| 818 | |||
| 743 | /* time to purge? */ | 819 | /* time to purge? */ |
| 744 | if ((t = time(NULL)) > purge_time) { | 820 | if (t > purge_time) { |
| 745 | /* next purge time is tomorrow */ | 821 | /* next purge time is tomorrow */ |
| 746 | purge_time = t + 24 * 60 * 60; | 822 | purge_time = t + 24 * 60 * 60; |
| 747 | 823 | ||
| @@ -750,6 +826,22 @@ static void *background_thread(void *arg) | |||
| 750 | job_post(q_item, 0); | 826 | job_post(q_item, 0); |
| 751 | } | 827 | } |
| 752 | 828 | ||
| 829 | /* time to poll the RSS? */ | ||
| 830 | if (t > rss_time) { | ||
| 831 | /* next RSS poll time */ | ||
| 832 | int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_hashtag_poll_hours", "4")); | ||
| 833 | |||
| 834 | /* don't hammer servers too much */ | ||
| 835 | if (hours < 1) | ||
| 836 | hours = 1; | ||
| 837 | |||
| 838 | rss_time = t + 60 * 60 * hours; | ||
| 839 | |||
| 840 | xs *q_item = xs_dict_new(); | ||
| 841 | q_item = xs_dict_append(q_item, "type", "rss_hashtag_poll"); | ||
| 842 | job_post(q_item, 0); | ||
| 843 | } | ||
| 844 | |||
| 753 | if (cnt == 0) { | 845 | if (cnt == 0) { |
| 754 | /* sleep 3 seconds */ | 846 | /* sleep 3 seconds */ |
| 755 | 847 | ||
| @@ -7,64 +7,90 @@ | |||
| 7 | #include "xs_time.h" | 7 | #include "xs_time.h" |
| 8 | #include "xs_openssl.h" | 8 | #include "xs_openssl.h" |
| 9 | #include "xs_match.h" | 9 | #include "xs_match.h" |
| 10 | #include "xs_random.h" | ||
| 10 | 11 | ||
| 11 | #include "snac.h" | 12 | #include "snac.h" |
| 12 | 13 | ||
| 13 | #include <sys/stat.h> | 14 | #include <sys/stat.h> |
| 14 | #include <sys/wait.h> | 15 | #include <sys/wait.h> |
| 15 | 16 | ||
| 16 | int usage(void) | 17 | int usage(const char *cmd) |
| 17 | { | 18 | { |
| 18 | printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); | 19 | printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); |
| 19 | printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n"); | 20 | printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n"); |
| 20 | printf("\n"); | 21 | printf("\n"); |
| 21 | printf("Commands:\n"); | 22 | |
| 22 | printf("\n"); | 23 | if (cmd == NULL) { |
| 23 | printf("init [{basedir}] Initializes the data storage\n"); | 24 | printf("Commands:\n"); |
| 24 | printf("upgrade {basedir} Upgrade to a new version\n"); | 25 | printf("\n"); |
| 25 | printf("adduser {basedir} [{uid}] Adds a new user\n"); | 26 | } |
| 26 | printf("deluser {basedir} {uid} Deletes a user\n"); | 27 | |
| 27 | printf("httpd {basedir} Starts the HTTPD daemon\n"); | 28 | const char *cmds = |
| 28 | printf("purge {basedir} Purges old data\n"); | 29 | "init [{basedir}] Initializes the data storage\n" |
| 29 | printf("state {basedir} Prints server state\n"); | 30 | "upgrade {basedir} Upgrade to a new version\n" |
| 30 | printf("webfinger {basedir} {account} Queries about an account (@user@host or actor url)\n"); | 31 | "adduser {basedir} [{uid}] Adds a new user\n" |
| 31 | printf("queue {basedir} {uid} Processes a user queue\n"); | 32 | "deluser {basedir} {uid} Deletes a user\n" |
| 32 | printf("follow {basedir} {uid} {actor} Follows an actor\n"); | 33 | "httpd {basedir} Starts the HTTPD daemon\n" |
| 33 | printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n"); | 34 | "purge {basedir} Purges old data\n" |
| 34 | printf("request {basedir} {uid} {url} Requests an object\n"); | 35 | "state {basedir} Prints server state\n" |
| 35 | printf("insert {basedir} {uid} {url} Requests an object and inserts it into the timeline\n"); | 36 | "webfinger {basedir} {account} Queries about an account (@user@host or actor url)\n" |
| 36 | printf("actor {basedir} [{uid}] {url} Requests an actor\n"); | 37 | "queue {basedir} {uid} Processes a user queue\n" |
| 37 | printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"); | 38 | "follow {basedir} {uid} {actor} Follows an actor\n" |
| 38 | printf("note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n"); | 39 | "unfollow {basedir} {uid} {actor} Unfollows an actor\n" |
| 39 | printf("note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n"); | 40 | "request {basedir} {uid} {url} Requests an object\n" |
| 40 | printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"); | 41 | "insert {basedir} {uid} {url} Requests an object and inserts it into the timeline\n" |
| 41 | printf("unboost {basedir} {uid} {url} Unboosts a post\n"); | 42 | "actor {basedir} [{uid}] {url} Requests an actor\n" |
| 42 | printf("resetpwd {basedir} {uid} Resets the password of a user\n"); | 43 | "note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n" |
| 43 | printf("ping {basedir} {uid} {actor} Pings an actor\n"); | 44 | "note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n" |
| 44 | printf("webfinger_s {basedir} {uid} {account} Queries about an account (@user@host or actor url)\n"); | 45 | "note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n" |
| 45 | printf("pin {basedir} {uid} {msg_url} Pins a message\n"); | 46 | "boost|announce {basedir} {uid} {url} Boosts (announces) a post\n" |
| 46 | printf("unpin {basedir} {uid} {msg_url} Unpins a message\n"); | 47 | "unboost {basedir} {uid} {url} Unboosts a post\n" |
| 47 | printf("bookmark {basedir} {uid} {msg_url} Bookmarks a message\n"); | 48 | "resetpwd {basedir} {uid} Resets the password of a user\n" |
| 48 | printf("unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n"); | 49 | "ping {basedir} {uid} {actor} Pings an actor\n" |
| 49 | printf("block {basedir} {instance_url} Blocks a full instance\n"); | 50 | "webfinger_s {basedir} {uid} {account} Queries about an account (@user@host or actor url)\n" |
| 50 | printf("unblock {basedir} {instance_url} Unblocks a full instance\n"); | 51 | "pin {basedir} {uid} {msg_url} Pins a message\n" |
| 51 | printf("limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n"); | 52 | "unpin {basedir} {uid} {msg_url} Unpins a message\n" |
| 52 | printf("unlimit {basedir} {uid} {actor} Unlimits an actor\n"); | 53 | "bookmark {basedir} {uid} {msg_url} Bookmarks a message\n" |
| 53 | printf("unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n"); | 54 | "unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n" |
| 54 | printf("verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n"); | 55 | "block {basedir} {instance_url} Blocks a full instance\n" |
| 55 | printf("search {basedir} {uid} {regex} Searches posts by content\n"); | 56 | "unblock {basedir} {instance_url} Unblocks a full instance\n" |
| 56 | printf("export_csv {basedir} {uid} Exports data as CSV files\n"); | 57 | "limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n" |
| 57 | printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n"); | 58 | "unlimit {basedir} {uid} {actor} Unlimits an actor\n" |
| 58 | printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n"); | 59 | "unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n" |
| 59 | printf("import_csv {basedir} {uid} Imports data from CSV files\n"); | 60 | "verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n" |
| 60 | printf("import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n"); | 61 | "search {basedir} {uid} {regex} Searches posts by content\n" |
| 61 | printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n"); | 62 | "export_csv {basedir} {uid} Exports followers, lists, MUTEd and bookmarks to CSV\n" |
| 62 | printf("lists {basedir} {uid} Returns the names of the lists created by the user\n"); | 63 | "export_posts {basedir} {iod} Exports all posts to outbox.json\n" |
| 63 | printf("list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n"); | 64 | "alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n" |
| 64 | printf("create_list {basedir} {uid} {name} Creates a new list\n"); | 65 | "migrate {basedir} {uid} Migrates to the account defined as the alias\n" |
| 65 | printf("delete_list {basedir} {uid} {name} Deletes an existing list\n"); | 66 | "import_csv {basedir} {uid} Imports data from CSV files\n" |
| 66 | printf("list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n"); | 67 | "import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n" |
| 67 | printf("list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"); | 68 | "import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n" |
| 69 | "lists {basedir} {uid} Returns the names of the lists created by the user\n" | ||
| 70 | "list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n" | ||
| 71 | "list_create {basedir} {uid} {name} Creates a new list\n" | ||
| 72 | "list_remove {basedir} {uid} {name} Removes an existing list\n" | ||
| 73 | "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" | ||
| 74 | "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"; | ||
| 75 | |||
| 76 | if (cmd == NULL) | ||
| 77 | printf("%s", cmds); | ||
| 78 | else { | ||
| 79 | /* only show help for the entered command */ | ||
| 80 | xs *l = xs_split(cmds, "\n"); | ||
| 81 | const char *v; | ||
| 82 | int cnt = 0; | ||
| 83 | |||
| 84 | xs_list_foreach(l, v) { | ||
| 85 | if (xs_str_in(v, cmd) != -1) { | ||
| 86 | printf("%s\n", v); | ||
| 87 | cnt++; | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | if (cnt == 0) | ||
| 92 | printf("%s", cmds); | ||
| 93 | } | ||
| 68 | 94 | ||
| 69 | return 1; | 95 | return 1; |
| 70 | } | 96 | } |
| @@ -94,7 +120,7 @@ int main(int argc, char *argv[]) | |||
| 94 | umask(0007); | 120 | umask(0007); |
| 95 | 121 | ||
| 96 | if ((cmd = GET_ARGV()) == NULL) | 122 | if ((cmd = GET_ARGV()) == NULL) |
| 97 | return usage(); | 123 | return usage(cmd); |
| 98 | 124 | ||
| 99 | if (strcmp(cmd, "init") == 0) { /** **/ | 125 | if (strcmp(cmd, "init") == 0) { /** **/ |
| 100 | /* initialize the data storage */ | 126 | /* initialize the data storage */ |
| @@ -106,7 +132,7 @@ int main(int argc, char *argv[]) | |||
| 106 | 132 | ||
| 107 | if ((basedir = getenv("SNAC_BASEDIR")) == NULL) { | 133 | if ((basedir = getenv("SNAC_BASEDIR")) == NULL) { |
| 108 | if ((basedir = GET_ARGV()) == NULL) | 134 | if ((basedir = GET_ARGV()) == NULL) |
| 109 | return usage(); | 135 | return usage(cmd); |
| 110 | } | 136 | } |
| 111 | 137 | ||
| 112 | if (strcmp(cmd, "upgrade") == 0) { /** **/ | 138 | if (strcmp(cmd, "upgrade") == 0) { /** **/ |
| @@ -143,6 +169,11 @@ int main(int argc, char *argv[]) | |||
| 143 | return 0; | 169 | return 0; |
| 144 | } | 170 | } |
| 145 | 171 | ||
| 172 | if (strcmp(cmd, "poll_hashtag_rss") == 0) { /** **/ | ||
| 173 | rss_poll_hashtags(); | ||
| 174 | return 0; | ||
| 175 | } | ||
| 176 | |||
| 146 | if (strcmp(cmd, "state") == 0) { /** **/ | 177 | if (strcmp(cmd, "state") == 0) { /** **/ |
| 147 | xs *shm_name = NULL; | 178 | xs *shm_name = NULL; |
| 148 | srv_state *p_state = srv_state_op(&shm_name, 1); | 179 | srv_state *p_state = srv_state_op(&shm_name, 1); |
| @@ -167,7 +198,7 @@ int main(int argc, char *argv[]) | |||
| 167 | } | 198 | } |
| 168 | 199 | ||
| 169 | if ((user = GET_ARGV()) == NULL) | 200 | if ((user = GET_ARGV()) == NULL) |
| 170 | return usage(); | 201 | return usage(cmd); |
| 171 | 202 | ||
| 172 | if (strcmp(cmd, "block") == 0) { /** **/ | 203 | if (strcmp(cmd, "block") == 0) { /** **/ |
| 173 | int ret = instance_block(user); | 204 | int ret = instance_block(user); |
| @@ -279,6 +310,11 @@ int main(int argc, char *argv[]) | |||
| 279 | return 0; | 310 | return 0; |
| 280 | } | 311 | } |
| 281 | 312 | ||
| 313 | if (strcmp(cmd, "export_posts") == 0) { /** **/ | ||
| 314 | export_posts(&snac); | ||
| 315 | return 0; | ||
| 316 | } | ||
| 317 | |||
| 282 | if (strcmp(cmd, "import_csv") == 0) { /** **/ | 318 | if (strcmp(cmd, "import_csv") == 0) { /** **/ |
| 283 | import_csv(&snac); | 319 | import_csv(&snac); |
| 284 | return 0; | 320 | return 0; |
| @@ -300,7 +336,7 @@ int main(int argc, char *argv[]) | |||
| 300 | } | 336 | } |
| 301 | 337 | ||
| 302 | if ((url = GET_ARGV()) == NULL) | 338 | if ((url = GET_ARGV()) == NULL) |
| 303 | return usage(); | 339 | return usage(cmd); |
| 304 | 340 | ||
| 305 | if (strcmp(cmd, "list_members") == 0) { /** **/ | 341 | if (strcmp(cmd, "list_members") == 0) { /** **/ |
| 306 | xs *lid = list_maint(&snac, url, 4); | 342 | xs *lid = list_maint(&snac, url, 4); |
| @@ -323,7 +359,7 @@ int main(int argc, char *argv[]) | |||
| 323 | return 0; | 359 | return 0; |
| 324 | } | 360 | } |
| 325 | 361 | ||
| 326 | if (strcmp(cmd, "create_list") == 0) { /** **/ | 362 | if (strcmp(cmd, "list_create") == 0) { /** **/ |
| 327 | xs *lid = list_maint(&snac, url, 4); | 363 | xs *lid = list_maint(&snac, url, 4); |
| 328 | 364 | ||
| 329 | if (lid == NULL) { | 365 | if (lid == NULL) { |
| @@ -336,7 +372,7 @@ int main(int argc, char *argv[]) | |||
| 336 | return 0; | 372 | return 0; |
| 337 | } | 373 | } |
| 338 | 374 | ||
| 339 | if (strcmp(cmd, "delete_list") == 0) { /** **/ | 375 | if (strcmp(cmd, "list_remove") == 0) { /** **/ |
| 340 | xs *lid = list_maint(&snac, url, 4); | 376 | xs *lid = list_maint(&snac, url, 4); |
| 341 | 377 | ||
| 342 | if (lid != NULL) { | 378 | if (lid != NULL) { |
| @@ -744,12 +780,24 @@ int main(int argc, char *argv[]) | |||
| 744 | xs *msg = NULL; | 780 | xs *msg = NULL; |
| 745 | xs *c_msg = NULL; | 781 | xs *c_msg = NULL; |
| 746 | xs *attl = xs_list_new(); | 782 | xs *attl = xs_list_new(); |
| 747 | char *fn = NULL; | 783 | const char *fn = NULL; |
| 784 | const char *in_reply_to = NULL; | ||
| 785 | const char **next = NULL; | ||
| 748 | 786 | ||
| 749 | /* iterate possible attachments */ | 787 | /* iterate possible attachments */ |
| 750 | while ((fn = GET_ARGV())) { | 788 | while ((fn = GET_ARGV())) { |
| 751 | FILE *f; | 789 | FILE *f; |
| 752 | 790 | ||
| 791 | if (next) { | ||
| 792 | *next = fn; | ||
| 793 | next = NULL; | ||
| 794 | } | ||
| 795 | else | ||
| 796 | if (strcmp(fn, "-r") == 0) { | ||
| 797 | /* next argument is an inReplyTo */ | ||
| 798 | next = &in_reply_to; | ||
| 799 | } | ||
| 800 | else | ||
| 753 | if ((f = fopen(fn, "rb")) != NULL) { | 801 | if ((f = fopen(fn, "rb")) != NULL) { |
| 754 | /* get the file size and content */ | 802 | /* get the file size and content */ |
| 755 | fseek(f, 0, SEEK_END); | 803 | fseek(f, 0, SEEK_END); |
| @@ -759,8 +807,10 @@ int main(int argc, char *argv[]) | |||
| 759 | fclose(f); | 807 | fclose(f); |
| 760 | 808 | ||
| 761 | char *ext = strrchr(fn, '.'); | 809 | char *ext = strrchr(fn, '.'); |
| 762 | xs *hash = xs_md5_hex(fn, strlen(fn)); | 810 | char rnd[32]; |
| 763 | xs *id = xs_fmt("%s%s", hash, ext); | 811 | xs_rnd_buf(rnd, sizeof(rnd)); |
| 812 | xs *hash = xs_md5_hex(rnd, sizeof(rnd)); | ||
| 813 | xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); | ||
| 764 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); | 814 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); |
| 765 | 815 | ||
| 766 | /* store */ | 816 | /* store */ |
| @@ -821,7 +871,7 @@ int main(int argc, char *argv[]) | |||
| 821 | if (strcmp(cmd, "note_unlisted") == 0) | 871 | if (strcmp(cmd, "note_unlisted") == 0) |
| 822 | scope = 2; | 872 | scope = 2; |
| 823 | 873 | ||
| 824 | msg = msg_note(&snac, content, NULL, NULL, attl, scope, getenv("LANG"), NULL); | 874 | msg = msg_note(&snac, content, NULL, in_reply_to, attl, scope, getenv("LANG"), NULL); |
| 825 | 875 | ||
| 826 | c_msg = msg_create(&snac, msg); | 876 | c_msg = msg_create(&snac, msg); |
| 827 | 877 | ||
| @@ -15,6 +15,7 @@ | |||
| 15 | #include "xs_url.h" | 15 | #include "xs_url.h" |
| 16 | #include "xs_mime.h" | 16 | #include "xs_mime.h" |
| 17 | #include "xs_match.h" | 17 | #include "xs_match.h" |
| 18 | #include "xs_unicode.h" | ||
| 18 | 19 | ||
| 19 | #include "snac.h" | 20 | #include "snac.h" |
| 20 | 21 | ||
| @@ -381,7 +382,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, | |||
| 381 | } | 382 | } |
| 382 | } | 383 | } |
| 383 | 384 | ||
| 384 | /* no code? | 385 | /* no code? |
| 385 | I'm not sure of the impacts of this right now, but Subway Tooter does not | 386 | I'm not sure of the impacts of this right now, but Subway Tooter does not |
| 386 | provide a code so one must be generated */ | 387 | provide a code so one must be generated */ |
| 387 | if (xs_is_null(code)){ | 388 | if (xs_is_null(code)){ |
| @@ -1133,9 +1134,14 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) | |||
| 1133 | bst = xs_dict_set(bst, "content", ""); | 1134 | bst = xs_dict_set(bst, "content", ""); |
| 1134 | bst = xs_dict_set(bst, "reblog", st); | 1135 | bst = xs_dict_set(bst, "reblog", st); |
| 1135 | 1136 | ||
| 1137 | xs *b_id = xs_toupper_i(xs_dup(xs_dict_get(st, "id"))); | ||
| 1138 | bst = xs_dict_set(bst, "id", b_id); | ||
| 1139 | |||
| 1136 | xs_free(st); | 1140 | xs_free(st); |
| 1137 | st = bst; | 1141 | st = bst; |
| 1138 | } | 1142 | } |
| 1143 | else | ||
| 1144 | xs_free(bst); | ||
| 1139 | } | 1145 | } |
| 1140 | 1146 | ||
| 1141 | return st; | 1147 | return st; |
| @@ -1338,9 +1344,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1338 | if ((f = fopen(index_fn, "r")) == NULL) | 1344 | if ((f = fopen(index_fn, "r")) == NULL) |
| 1339 | return out; | 1345 | return out; |
| 1340 | 1346 | ||
| 1341 | const char *max_id = xs_dict_get(args, "max_id"); | 1347 | const char *o_max_id = xs_dict_get(args, "max_id"); |
| 1342 | const char *since_id = xs_dict_get(args, "since_id"); | 1348 | const char *o_since_id = xs_dict_get(args, "since_id"); |
| 1343 | const char *min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */ | 1349 | const char *o_min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */ |
| 1344 | const char *limit_s = xs_dict_get(args, "limit"); | 1350 | const char *limit_s = xs_dict_get(args, "limit"); |
| 1345 | int (*iterator)(FILE *, char *); | 1351 | int (*iterator)(FILE *, char *); |
| 1346 | int initial_status = 0; | 1352 | int initial_status = 0; |
| @@ -1348,6 +1354,10 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1348 | int limit = 0; | 1354 | int limit = 0; |
| 1349 | int cnt = 0; | 1355 | int cnt = 0; |
| 1350 | 1356 | ||
| 1357 | xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL; | ||
| 1358 | xs *since_id = o_since_id ? xs_tolower_i(xs_dup(o_since_id)) : NULL; | ||
| 1359 | xs *min_id = o_min_id ? xs_tolower_i(xs_dup(o_min_id)) : NULL; | ||
| 1360 | |||
| 1351 | if (!xs_is_null(limit_s)) | 1361 | if (!xs_is_null(limit_s)) |
| 1352 | limit = atoi(limit_s); | 1362 | limit = atoi(limit_s); |
| 1353 | 1363 | ||
| @@ -1371,7 +1381,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1371 | /* only return entries older that max_id */ | 1381 | /* only return entries older that max_id */ |
| 1372 | if (max_id) { | 1382 | if (max_id) { |
| 1373 | if (strcmp(md5, MID_TO_MD5(max_id)) == 0) { | 1383 | if (strcmp(md5, MID_TO_MD5(max_id)) == 0) { |
| 1374 | max_id = NULL; | 1384 | max_id = xs_free(max_id); |
| 1375 | if (ascending) | 1385 | if (ascending) |
| 1376 | break; | 1386 | break; |
| 1377 | } | 1387 | } |
| @@ -1384,7 +1394,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1384 | if (strcmp(md5, MID_TO_MD5(since_id)) == 0) { | 1394 | if (strcmp(md5, MID_TO_MD5(since_id)) == 0) { |
| 1385 | if (!ascending) | 1395 | if (!ascending) |
| 1386 | break; | 1396 | break; |
| 1387 | since_id = NULL; | 1397 | since_id = xs_free(since_id); |
| 1388 | } | 1398 | } |
| 1389 | if (ascending) | 1399 | if (ascending) |
| 1390 | continue; | 1400 | continue; |
| @@ -1637,7 +1647,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1637 | const char *aq = xs_dict_get(args, "q"); | 1647 | const char *aq = xs_dict_get(args, "q"); |
| 1638 | 1648 | ||
| 1639 | if (!xs_is_null(aq)) { | 1649 | if (!xs_is_null(aq)) { |
| 1640 | xs *q = xs_tolower_i(xs_dup(aq)); | 1650 | xs *q = xs_utf8_to_lower(aq); |
| 1641 | out = xs_list_new(); | 1651 | out = xs_list_new(); |
| 1642 | xs *wing = following_list(&snac1); | 1652 | xs *wing = following_list(&snac1); |
| 1643 | xs *wers = follower_list(&snac1); | 1653 | xs *wers = follower_list(&snac1); |
| @@ -1780,7 +1790,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1780 | } | 1790 | } |
| 1781 | else | 1791 | else |
| 1782 | if (strcmp(opt, "statuses") == 0) { | 1792 | if (strcmp(opt, "statuses") == 0) { |
| 1783 | /* we don't serve statuses of others; return the empty list */ | 1793 | /* we don't serve statuses of others; return the empty list */ |
| 1784 | out = xs_list_new(); | 1794 | out = xs_list_new(); |
| 1785 | } | 1795 | } |
| 1786 | else | 1796 | else |
| @@ -1999,7 +2009,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 1999 | } | 2009 | } |
| 2000 | else | 2010 | else |
| 2001 | if (strcmp(cmd, "/v2/filters") == 0) { /** **/ | 2011 | if (strcmp(cmd, "/v2/filters") == 0) { /** **/ |
| 2002 | /* snac will never have filters | 2012 | /* snac will never have filters |
| 2003 | * but still, without a v2 endpoint a short delay is introduced | 2013 | * but still, without a v2 endpoint a short delay is introduced |
| 2004 | * in some apps */ | 2014 | * in some apps */ |
| 2005 | *body = xs_dup("[]"); | 2015 | *body = xs_dup("[]"); |
| @@ -2331,19 +2341,36 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2331 | status = HTTP_STATUS_OK; | 2341 | status = HTTP_STATUS_OK; |
| 2332 | } | 2342 | } |
| 2333 | else | 2343 | else |
| 2344 | if (strcmp(cmd, "/v1/instance/extended_description") == 0) { /** **/ | ||
| 2345 | xs *d = xs_dict_new(); | ||
| 2346 | xs *greeting = xs_fmt("%s/greeting.html", srv_basedir); | ||
| 2347 | time_t t = mtime(greeting); | ||
| 2348 | xs *updated_at = xs_str_iso_date(t); | ||
| 2349 | xs *content = xs_replace(snac_blurb, "%host%", xs_dict_get(srv_config, "host")); | ||
| 2350 | |||
| 2351 | d = xs_dict_set(d, "updated_at", updated_at); | ||
| 2352 | d = xs_dict_set(d, "content", content); | ||
| 2353 | |||
| 2354 | *body = xs_json_dumps(d, 4); | ||
| 2355 | *ctype = "application/json"; | ||
| 2356 | status = HTTP_STATUS_OK; | ||
| 2357 | } | ||
| 2358 | else | ||
| 2334 | if (xs_startswith(cmd, "/v1/statuses/")) { /** **/ | 2359 | if (xs_startswith(cmd, "/v1/statuses/")) { /** **/ |
| 2335 | /* information about a status */ | 2360 | /* information about a status */ |
| 2336 | if (logged_in) { | 2361 | if (logged_in) { |
| 2337 | xs *l = xs_split(cmd, "/"); | 2362 | xs *l = xs_split(cmd, "/"); |
| 2338 | const char *id = xs_list_get(l, 3); | 2363 | const char *oid = xs_list_get(l, 3); |
| 2339 | const char *op = xs_list_get(l, 4); | 2364 | const char *op = xs_list_get(l, 4); |
| 2340 | 2365 | ||
| 2341 | if (!xs_is_null(id)) { | 2366 | if (!xs_is_null(oid)) { |
| 2342 | xs *msg = NULL; | 2367 | xs *msg = NULL; |
| 2343 | xs *out = NULL; | 2368 | xs *out = NULL; |
| 2344 | 2369 | ||
| 2345 | /* skip the 'fake' part of the id */ | 2370 | /* skip the 'fake' part of the id */ |
| 2346 | id = MID_TO_MD5(id); | 2371 | oid = MID_TO_MD5(oid); |
| 2372 | |||
| 2373 | xs *id = xs_tolower_i(xs_dup(oid)); | ||
| 2347 | 2374 | ||
| 2348 | if (valid_status(object_get_by_md5(id, &msg))) { | 2375 | if (valid_status(object_get_by_md5(id, &msg))) { |
| 2349 | if (op == NULL) { | 2376 | if (op == NULL) { |
| @@ -2459,7 +2486,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2459 | if (logged_in) { | 2486 | if (logged_in) { |
| 2460 | const xs_list *timeline = xs_dict_get(args, "timeline[]"); | 2487 | const xs_list *timeline = xs_dict_get(args, "timeline[]"); |
| 2461 | xs_str *json = NULL; | 2488 | xs_str *json = NULL; |
| 2462 | if (!xs_is_null(timeline)) | 2489 | if (!xs_is_null(timeline)) |
| 2463 | json = xs_json_dumps(markers_get(&snac1, timeline), 4); | 2490 | json = xs_json_dumps(markers_get(&snac1, timeline), 4); |
| 2464 | 2491 | ||
| 2465 | if (!xs_is_null(json)) | 2492 | if (!xs_is_null(json)) |
| @@ -2475,6 +2502,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2475 | } | 2502 | } |
| 2476 | else | 2503 | else |
| 2477 | if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/ | 2504 | if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/ |
| 2505 | if (logged_in) { | ||
| 2506 | xs *r = xs_list_new(); | ||
| 2507 | const xs_list *followed_hashtags = xs_dict_get_def(snac1.config, | ||
| 2508 | "followed_hashtags", xs_stock(XSTYPE_LIST)); | ||
| 2509 | const char *hashtag; | ||
| 2510 | |||
| 2511 | xs_list_foreach(followed_hashtags, hashtag) { | ||
| 2512 | if (*hashtag == '#') { | ||
| 2513 | xs *d = xs_dict_new(); | ||
| 2514 | xs *s = xs_fmt("%s?t=%s", srv_baseurl, hashtag + 1); | ||
| 2515 | |||
| 2516 | d = xs_dict_set(d, "name", hashtag + 1); | ||
| 2517 | d = xs_dict_set(d, "url", s); | ||
| 2518 | d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST)); | ||
| 2519 | |||
| 2520 | r = xs_list_append(r, d); | ||
| 2521 | } | ||
| 2522 | } | ||
| 2523 | |||
| 2524 | *body = xs_json_dumps(r, 4); | ||
| 2525 | *ctype = "application/json"; | ||
| 2526 | status = HTTP_STATUS_OK; | ||
| 2527 | } | ||
| 2528 | else | ||
| 2529 | status = HTTP_STATUS_UNAUTHORIZED; | ||
| 2530 | } | ||
| 2531 | else | ||
| 2532 | if (strcmp(cmd, "/v1/blocks") == 0) { /** **/ | ||
| 2533 | *body = xs_dup("[]"); | ||
| 2534 | *ctype = "application/json"; | ||
| 2535 | status = HTTP_STATUS_OK; | ||
| 2536 | } | ||
| 2537 | else | ||
| 2538 | if (strcmp(cmd, "/v1/mutes") == 0) { /** **/ | ||
| 2478 | *body = xs_dup("[]"); | 2539 | *body = xs_dup("[]"); |
| 2479 | *ctype = "application/json"; | 2540 | *ctype = "application/json"; |
| 2480 | status = HTTP_STATUS_OK; | 2541 | status = HTTP_STATUS_OK; |
| @@ -2507,9 +2568,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, | |||
| 2507 | /* reply something only for offset 0; otherwise, | 2568 | /* reply something only for offset 0; otherwise, |
| 2508 | apps like Tusky keep asking again and again */ | 2569 | apps like Tusky keep asking again and again */ |
| 2509 | if (xs_startswith(q, "https://")) { | 2570 | if (xs_startswith(q, "https://")) { |
| 2510 | xs *md5 = xs_md5_hex(q, strlen(q)); | 2571 | if (!timeline_here(&snac1, q)) { |
| 2511 | |||
| 2512 | if (!timeline_here(&snac1, md5)) { | ||
| 2513 | xs *object = NULL; | 2572 | xs *object = NULL; |
| 2514 | int status; | 2573 | int status; |
| 2515 | 2574 | ||
| @@ -2979,8 +3038,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2979 | 3038 | ||
| 2980 | if (*fn != '\0') { | 3039 | if (*fn != '\0') { |
| 2981 | char *ext = strrchr(fn, '.'); | 3040 | char *ext = strrchr(fn, '.'); |
| 2982 | xs *hash = xs_md5_hex(fn, strlen(fn)); | 3041 | char rnd[32]; |
| 2983 | xs *id = xs_fmt("%s%s", hash, ext); | 3042 | xs_rnd_buf(rnd, sizeof(rnd)); |
| 3043 | xs *hash = xs_md5_hex(rnd, sizeof(rnd)); | ||
| 3044 | xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); | ||
| 2984 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); | 3045 | xs *url = xs_fmt("%s/s/%s", snac.actor, id); |
| 2985 | int fo = xs_number_get(xs_list_get(file, 1)); | 3046 | int fo = xs_number_get(xs_list_get(file, 1)); |
| 2986 | int fs = xs_number_get(xs_list_get(file, 2)); | 3047 | int fs = xs_number_get(xs_list_get(file, 2)); |
| @@ -3227,7 +3288,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 3227 | if (!xs_is_null(home)) | 3288 | if (!xs_is_null(home)) |
| 3228 | home_marker = xs_dict_get(home, "last_read_id"); | 3289 | home_marker = xs_dict_get(home, "last_read_id"); |
| 3229 | } | 3290 | } |
| 3230 | 3291 | ||
| 3231 | const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]"); | 3292 | const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]"); |
| 3232 | if (xs_is_null(notify_marker)) { | 3293 | if (xs_is_null(notify_marker)) { |
| 3233 | const xs_dict *notify = xs_dict_get(args, "notifications"); | 3294 | const xs_dict *notify = xs_dict_get(args, "notifications"); |
| @@ -3296,6 +3357,54 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 3296 | } | 3357 | } |
| 3297 | } | 3358 | } |
| 3298 | else | 3359 | else |
| 3360 | if (xs_startswith(cmd, "/v1/tags/")) { /** **/ | ||
| 3361 | if (logged_in) { | ||
| 3362 | xs *l = xs_split(cmd, "/"); | ||
| 3363 | const char *i_tag = xs_list_get(l, 3); | ||
| 3364 | const char *cmd = xs_list_get(l, 4); | ||
| 3365 | |||
| 3366 | status = HTTP_STATUS_UNPROCESSABLE_CONTENT; | ||
| 3367 | |||
| 3368 | if (xs_is_string(i_tag) && xs_is_string(cmd)) { | ||
| 3369 | int ok = 0; | ||
| 3370 | |||
| 3371 | xs *tag = xs_fmt("#%s", i_tag); | ||
| 3372 | xs *followed_hashtags = xs_dup(xs_dict_get_def(snac.config, | ||
| 3373 | "followed_hashtags", xs_stock(XSTYPE_LIST))); | ||
| 3374 | |||
| 3375 | if (strcmp(cmd, "follow") == 0) { | ||
| 3376 | followed_hashtags = xs_list_append(followed_hashtags, tag); | ||
| 3377 | ok = 1; | ||
| 3378 | } | ||
| 3379 | else | ||
| 3380 | if (strcmp(cmd, "unfollow") == 0) { | ||
| 3381 | int off = xs_list_in(followed_hashtags, tag); | ||
| 3382 | |||
| 3383 | if (off != -1) | ||
| 3384 | followed_hashtags = xs_list_del(followed_hashtags, off); | ||
| 3385 | |||
| 3386 | ok = 1; | ||
| 3387 | } | ||
| 3388 | |||
| 3389 | if (ok) { | ||
| 3390 | /* update */ | ||
| 3391 | xs_dict_set(snac.config, "followed_hashtags", followed_hashtags); | ||
| 3392 | user_persist(&snac, 0); | ||
| 3393 | |||
| 3394 | xs *d = xs_dict_new(); | ||
| 3395 | xs *s = xs_fmt("%s?t=%s", srv_baseurl, i_tag); | ||
| 3396 | d = xs_dict_set(d, "name", i_tag); | ||
| 3397 | d = xs_dict_set(d, "url", s); | ||
| 3398 | d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST)); | ||
| 3399 | |||
| 3400 | *body = xs_json_dumps(d, 4); | ||
| 3401 | *ctype = "application/json"; | ||
| 3402 | status = HTTP_STATUS_OK; | ||
| 3403 | } | ||
| 3404 | } | ||
| 3405 | } | ||
| 3406 | } | ||
| 3407 | else | ||
| 3299 | status = HTTP_STATUS_UNPROCESSABLE_CONTENT; | 3408 | status = HTTP_STATUS_UNPROCESSABLE_CONTENT; |
| 3300 | 3409 | ||
| 3301 | /* user cleanup */ | 3410 | /* user cleanup */ |
| @@ -126,7 +126,7 @@ msgstr "люди" | |||
| 126 | 126 | ||
| 127 | #: html.c:936 | 127 | #: html.c:936 |
| 128 | msgid "instance" | 128 | msgid "instance" |
| 129 | msgstr "ÑкземплÑÑ€" | 129 | msgstr "Ñервер" |
| 130 | 130 | ||
| 131 | #: html.c:945 | 131 | #: html.c:945 |
| 132 | msgid "" | 132 | msgid "" |
| @@ -679,12 +679,12 @@ msgstr "Ðичего не найдено Ð´Ð»Ñ '%s'" | |||
| 679 | 679 | ||
| 680 | #: html.c:3929 | 680 | #: html.c:3929 |
| 681 | msgid "Showing instance timeline" | 681 | msgid "Showing instance timeline" |
| 682 | msgstr "Показываем ленту инÑтанции" | 682 | msgstr "Показываем ленту Ñервера" |
| 683 | 683 | ||
| 684 | #: html.c:4012 | 684 | #: html.c:4012 |
| 685 | #, c-format | 685 | #, c-format |
| 686 | msgid "Showing timeline for list '%s'" | 686 | msgid "Showing timeline for list '%s'" |
| 687 | msgstr "Показываем ленты инÑтанции Ð´Ð»Ñ ÑпиÑка '%s'" | 687 | msgstr "Показываем ленту Ð´Ð»Ñ ÑпиÑка '%s'" |
| 688 | 688 | ||
| 689 | #: httpd.c:250 | 689 | #: httpd.c:250 |
| 690 | #, c-format | 690 | #, c-format |
| @@ -693,7 +693,7 @@ msgstr "Результаты поиÑка Ð´Ð»Ñ Ñ‚ÐµÐ³Ð° #%s" | |||
| 693 | 693 | ||
| 694 | #: httpd.c:259 | 694 | #: httpd.c:259 |
| 695 | msgid "Recent posts by users in this instance" | 695 | msgid "Recent posts by users in this instance" |
| 696 | msgstr "ПоÑледние ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð½Ð° Ñтой инÑтанции" | 696 | msgstr "ПоÑледние ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð½Ð° Ñтом Ñервере" |
| 697 | 697 | ||
| 698 | #: html.c:1603 | 698 | #: html.c:1603 |
| 699 | msgid "Blocked hashtags..." | 699 | msgid "Blocked hashtags..." |
diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..fd59678 --- /dev/null +++ b/po/uk.po | |||
| @@ -0,0 +1,768 @@ | |||
| 1 | # snac message translation file | ||
| 2 | # | ||
| 3 | msgid "" | ||
| 4 | msgstr "" | ||
| 5 | "Project-Id-Version: snac\n" | ||
| 6 | "Last-Translator: grunfink\n" | ||
| 7 | "Language: uk\n" | ||
| 8 | "Content-Type: text/plain; charset=UTF-8\n" | ||
| 9 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | ||
| 10 | "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" | ||
| 11 | "POT-Creation-Date: \n" | ||
| 12 | "PO-Revision-Date: \n" | ||
| 13 | "Language-Team: \n" | ||
| 14 | "MIME-Version: 1.0\n" | ||
| 15 | "Content-Transfer-Encoding: 8bit\n" | ||
| 16 | "X-Generator: Poedit 3.0\n" | ||
| 17 | |||
| 18 | #: html.c:384 | ||
| 19 | msgid "Sensitive content: " | ||
| 20 | msgstr "Делікатний вміÑÑ‚: " | ||
| 21 | |||
| 22 | #: html.c:392 | ||
| 23 | msgid "Sensitive content description" | ||
| 24 | msgstr "ÐžÐ¿Ð¸Ñ Ð´ÐµÐ»Ñ–ÐºÐ°Ñ‚Ð½Ð¾Ð³Ð¾ вміÑту" | ||
| 25 | |||
| 26 | #: html.c:405 | ||
| 27 | msgid "Only for mentioned people: " | ||
| 28 | msgstr "Тільки Ð´Ð»Ñ Ð·Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ… оÑіб: " | ||
| 29 | |||
| 30 | #: html.c:428 | ||
| 31 | msgid "Reply to (URL): " | ||
| 32 | msgstr "ВідповіÑти до (URL): " | ||
| 33 | |||
| 34 | #: html.c:437 | ||
| 35 | msgid "Don't send, but store as a draft" | ||
| 36 | msgstr "Ðе надÑилати, але зберігти Ñк чернетку" | ||
| 37 | |||
| 38 | #: html.c:438 | ||
| 39 | msgid "Draft:" | ||
| 40 | msgstr "Чернетка:" | ||
| 41 | |||
| 42 | #: html.c:494 | ||
| 43 | msgid "Attachments..." | ||
| 44 | msgstr "ВкладеннÑ..." | ||
| 45 | |||
| 46 | #: html.c:517 | ||
| 47 | msgid "File:" | ||
| 48 | msgstr "Файл:" | ||
| 49 | |||
| 50 | #: html.c:521 | ||
| 51 | msgid "Clear this field to delete the attachment" | ||
| 52 | msgstr "ОчиÑтіть це поле, щоб видалити вкладеннÑ" | ||
| 53 | |||
| 54 | #: html.c:530 html.c:555 | ||
| 55 | msgid "Attachment description" | ||
| 56 | msgstr "ÐžÐ¿Ð¸Ñ Ð²ÐºÐ»Ð°Ð´ÐµÐ½Ð½Ñ" | ||
| 57 | |||
| 58 | #: html.c:566 | ||
| 59 | msgid "Poll..." | ||
| 60 | msgstr "ОпитуваннÑ..." | ||
| 61 | |||
| 62 | #: html.c:568 | ||
| 63 | msgid "Poll options (one per line, up to 8):" | ||
| 64 | msgstr "Варіанти відповідей (по одному в Ñ€Ñдку, до 8):" | ||
| 65 | |||
| 66 | #: html.c:580 | ||
| 67 | msgid "One choice" | ||
| 68 | msgstr "Єдиний вибір" | ||
| 69 | |||
| 70 | #: html.c:583 | ||
| 71 | msgid "Multiple choices" | ||
| 72 | msgstr "Декілька варіантів вибору" | ||
| 73 | |||
| 74 | #: html.c:589 | ||
| 75 | msgid "End in 5 minutes" | ||
| 76 | msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· 5 хвилин" | ||
| 77 | |||
| 78 | #: html.c:593 | ||
| 79 | msgid "End in 1 hour" | ||
| 80 | msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· 1 годину" | ||
| 81 | |||
| 82 | #: html.c:596 | ||
| 83 | msgid "End in 1 day" | ||
| 84 | msgstr "Ð—Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· 1 день" | ||
| 85 | |||
| 86 | #: html.c:604 | ||
| 87 | msgid "Post" | ||
| 88 | msgstr "ÐадіÑлати" | ||
| 89 | |||
| 90 | #: html.c:701 html.c:708 | ||
| 91 | msgid "Site description" | ||
| 92 | msgstr "ÐžÐ¿Ð¸Ñ Ñайту" | ||
| 93 | |||
| 94 | #: html.c:719 | ||
| 95 | msgid "Admin email" | ||
| 96 | msgstr "Пошта админа" | ||
| 97 | |||
| 98 | #: html.c:732 | ||
| 99 | msgid "Admin account" | ||
| 100 | msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð°Ð´Ð¼Ñ–Ð½Ð°" | ||
| 101 | |||
| 102 | #: html.c:800 html.c:1136 | ||
| 103 | #, c-format | ||
| 104 | msgid "%d following, %d followers" | ||
| 105 | msgstr "%d підпиÑок, %d підпиÑників" | ||
| 106 | |||
| 107 | #: html.c:890 | ||
| 108 | msgid "RSS" | ||
| 109 | msgstr "RSS" | ||
| 110 | |||
| 111 | #: html.c:895 html.c:923 | ||
| 112 | msgid "private" | ||
| 113 | msgstr "оÑобиÑте" | ||
| 114 | |||
| 115 | #: html.c:919 | ||
| 116 | msgid "public" | ||
| 117 | msgstr "публічне" | ||
| 118 | |||
| 119 | #: html.c:927 | ||
| 120 | msgid "notifications" | ||
| 121 | msgstr "повідомленнÑ" | ||
| 122 | |||
| 123 | #: html.c:932 | ||
| 124 | msgid "people" | ||
| 125 | msgstr "люди" | ||
| 126 | |||
| 127 | #: html.c:936 | ||
| 128 | msgid "instance" | ||
| 129 | msgstr "Ñервер" | ||
| 130 | |||
| 131 | #: html.c:945 | ||
| 132 | msgid "" | ||
| 133 | "Search posts by URL or content (regular expression), @user@host accounts, or " | ||
| 134 | "#tag" | ||
| 135 | msgstr "" | ||
| 136 | "Шукати допиÑи за URL або вміÑтом (регулÑрний вираз), акаунтами @user@host " | ||
| 137 | "або #тегом" | ||
| 138 | |||
| 139 | #: html.c:946 | ||
| 140 | msgid "Content search" | ||
| 141 | msgstr "Пошук за вміÑтом" | ||
| 142 | |||
| 143 | #: html.c:1068 | ||
| 144 | msgid "verified link" | ||
| 145 | msgstr "перевірене поÑиланнÑ" | ||
| 146 | |||
| 147 | #: html.c:1125 html.c:2540 html.c:2553 html.c:2562 | ||
| 148 | msgid "Location: " | ||
| 149 | msgstr "МіÑце знаходженнÑ: " | ||
| 150 | |||
| 151 | #: html.c:1161 | ||
| 152 | msgid "New Post..." | ||
| 153 | msgstr "Ðовий допиÑ..." | ||
| 154 | |||
| 155 | #: html.c:1163 | ||
| 156 | msgid "What's on your mind?" | ||
| 157 | msgstr "Що у Ð²Ð°Ñ Ð½Ð° думці?" | ||
| 158 | |||
| 159 | #: html.c:1172 | ||
| 160 | msgid "Operations..." | ||
| 161 | msgstr "Дії..." | ||
| 162 | |||
| 163 | #: html.c:1187 html.c:1788 html.c:3193 html.c:4578 | ||
| 164 | msgid "Follow" | ||
| 165 | msgstr "ПідпиÑатиÑÑ" | ||
| 166 | |||
| 167 | #: html.c:1189 | ||
| 168 | msgid "(by URL or user@host)" | ||
| 169 | msgstr "(за URL або user@host)" | ||
| 170 | |||
| 171 | #: html.c:1204 html.c:1764 html.c:4527 | ||
| 172 | msgid "Boost" | ||
| 173 | msgstr "ПроÑувати" | ||
| 174 | |||
| 175 | #: html.c:1206 html.c:1223 | ||
| 176 | msgid "(by URL)" | ||
| 177 | msgstr "(за URL)" | ||
| 178 | |||
| 179 | #: html.c:1221 html.c:1743 html.c:4518 | ||
| 180 | msgid "Like" | ||
| 181 | msgstr "Вподобайка" | ||
| 182 | |||
| 183 | #: html.c:1347 | ||
| 184 | msgid "User Settings..." | ||
| 185 | msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача..." | ||
| 186 | |||
| 187 | #: html.c:1356 | ||
| 188 | msgid "Display name:" | ||
| 189 | msgstr "Видиме ім'Ñ:" | ||
| 190 | |||
| 191 | #: html.c:1362 | ||
| 192 | msgid "Your name" | ||
| 193 | msgstr "Ваше ім'Ñ" | ||
| 194 | |||
| 195 | #: html.c:1364 | ||
| 196 | msgid "Avatar: " | ||
| 197 | msgstr "Ðватар: " | ||
| 198 | |||
| 199 | #: html.c:1372 | ||
| 200 | msgid "Delete current avatar" | ||
| 201 | msgstr "Видалити поточний аватар" | ||
| 202 | |||
| 203 | #: html.c:1374 | ||
| 204 | msgid "Header image (banner): " | ||
| 205 | msgstr "Ð—Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ° (банер): " | ||
| 206 | |||
| 207 | #: html.c:1382 | ||
| 208 | msgid "Delete current header image" | ||
| 209 | msgstr "Видалити поточне Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ°" | ||
| 210 | |||
| 211 | #: html.c:1384 | ||
| 212 | msgid "Bio:" | ||
| 213 | msgstr "Про Ñебе:" | ||
| 214 | |||
| 215 | #: html.c:1390 | ||
| 216 | msgid "Write about yourself here..." | ||
| 217 | msgstr "Ðапишіть про Ñебе тут..." | ||
| 218 | |||
| 219 | #: html.c:1399 | ||
| 220 | msgid "Always show sensitive content" | ||
| 221 | msgstr "Завжди показувати делікатний вміÑÑ‚" | ||
| 222 | |||
| 223 | #: html.c:1401 | ||
| 224 | msgid "Email address for notifications:" | ||
| 225 | msgstr "Поштова адреÑа Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ:" | ||
| 226 | |||
| 227 | #: html.c:1409 | ||
| 228 | msgid "Telegram notifications (bot key and chat id):" | ||
| 229 | msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð² Telegram (ключ бота та id чату):" | ||
| 230 | |||
| 231 | #: html.c:1423 | ||
| 232 | msgid "ntfy notifications (ntfy server and token):" | ||
| 233 | msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð² ntfy (Ñервер та токен ntfy):" | ||
| 234 | |||
| 235 | #: html.c:1437 | ||
| 236 | msgid "Maximum days to keep posts (0: server settings):" | ||
| 237 | msgstr "МакÑимальний Ñ‡Ð°Ñ Ð´Ð»Ñ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ð´Ð¾Ð¿Ð¸Ñів (0: Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñервера):" | ||
| 238 | |||
| 239 | #: html.c:1451 | ||
| 240 | msgid "Drop direct messages from people you don't follow" | ||
| 241 | msgstr "ВідхилÑти оÑобиÑті Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ незнайомців" | ||
| 242 | |||
| 243 | #: html.c:1460 | ||
| 244 | msgid "This account is a bot" | ||
| 245 | msgstr "Цей акаунт є ботом" | ||
| 246 | |||
| 247 | #: html.c:1469 | ||
| 248 | msgid "Auto-boost all mentions to this account" | ||
| 249 | msgstr "Ðвтоматично проÑувати вÑÑ– згадки цього акаунта" | ||
| 250 | |||
| 251 | #: html.c:1478 | ||
| 252 | msgid "This account is private (posts are not shown through the web)" | ||
| 253 | msgstr "Це закритий акаунт (допиÑи не показуютьÑÑ Ð² мережі)" | ||
| 254 | |||
| 255 | #: html.c:1488 | ||
| 256 | msgid "Collapse top threads by default" | ||
| 257 | msgstr "Згорнути потоки за замовчуваннÑм" | ||
| 258 | |||
| 259 | #: html.c:1497 | ||
| 260 | msgid "Follow requests must be approved" | ||
| 261 | msgstr "Запити на підпиÑку мають бути Ñхвалені" | ||
| 262 | |||
| 263 | #: html.c:1506 | ||
| 264 | msgid "Publish follower and following metrics" | ||
| 265 | msgstr "Публікувати метрики підпиÑок та підпиÑників" | ||
| 266 | |||
| 267 | #: html.c:1508 | ||
| 268 | msgid "Current location:" | ||
| 269 | msgstr "Поточне міÑцезнаходженнÑ:" | ||
| 270 | |||
| 271 | #: html.c:1522 | ||
| 272 | msgid "Profile metadata (key=value pairs in each line):" | ||
| 273 | msgstr "Метадані профілю (пари ключ=Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² кожному Ñ€Ñдку):" | ||
| 274 | |||
| 275 | #: html.c:1533 | ||
| 276 | msgid "Web interface language:" | ||
| 277 | msgstr "Мова інтерфейÑу:" | ||
| 278 | |||
| 279 | #: html.c:1543 | ||
| 280 | msgid "New password:" | ||
| 281 | msgstr "Ðовий пароль:" | ||
| 282 | |||
| 283 | #: html.c:1550 | ||
| 284 | msgid "Repeat new password:" | ||
| 285 | msgstr "Повторити новий пароль:" | ||
| 286 | |||
| 287 | #: html.c:1560 | ||
| 288 | msgid "Update user info" | ||
| 289 | msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… кориÑтувача" | ||
| 290 | |||
| 291 | #: html.c:1571 | ||
| 292 | msgid "Followed hashtags..." | ||
| 293 | msgstr "ПідпиÑані хештеги..." | ||
| 294 | |||
| 295 | #: html.c:1573 html.c:1605 | ||
| 296 | msgid "One hashtag per line" | ||
| 297 | msgstr "Один хештег на Ñ€Ñдок" | ||
| 298 | |||
| 299 | #: html.c:1594 html.c:1626 | ||
| 300 | msgid "Update hashtags" | ||
| 301 | msgstr "Оновити хештеги" | ||
| 302 | |||
| 303 | #: html.c:1743 | ||
| 304 | msgid "Say you like this post" | ||
| 305 | msgstr "Позначте Ð´Ð¾Ð¿Ð¸Ñ Ñк вподобаний" | ||
| 306 | |||
| 307 | #: html.c:1748 html.c:4536 | ||
| 308 | msgid "Unlike" | ||
| 309 | msgstr "Більше не подобаєтьÑÑ" | ||
| 310 | |||
| 311 | #: html.c:1748 | ||
| 312 | msgid "Nah don't like it that much" | ||
| 313 | msgstr "Мені це не дуже подобаєтьÑÑ" | ||
| 314 | |||
| 315 | #: html.c:1754 html.c:4673 | ||
| 316 | msgid "Unpin" | ||
| 317 | msgstr "Відкріпити" | ||
| 318 | |||
| 319 | #: html.c:1754 | ||
| 320 | msgid "Unpin this post from your timeline" | ||
| 321 | msgstr "Відкріпити цей Ð´Ð¾Ð¿Ð¸Ñ Ð·Ñ– влаÑної Ñтрічки" | ||
| 322 | |||
| 323 | #: html.c:1757 html.c:4668 | ||
| 324 | msgid "Pin" | ||
| 325 | msgstr "Закріпити" | ||
| 326 | |||
| 327 | #: html.c:1757 | ||
| 328 | msgid "Pin this post to the top of your timeline" | ||
| 329 | msgstr "Закріпити цей Ð´Ð¾Ð¿Ð¸Ñ Ð´Ð¾ влаÑної Ñтрічки" | ||
| 330 | |||
| 331 | #: html.c:1764 | ||
| 332 | msgid "Announce this post to your followers" | ||
| 333 | msgstr "ПоділитиÑÑ Ñ†Ð¸Ð¼ допиÑом зі Ñвоїми підпиÑниками" | ||
| 334 | |||
| 335 | #: html.c:1769 html.c:4544 | ||
| 336 | msgid "Unboost" | ||
| 337 | msgstr "СкаÑувати проÑуваннÑ" | ||
| 338 | |||
| 339 | #: html.c:1769 | ||
| 340 | msgid "I regret I boosted this" | ||
| 341 | msgstr "Я шкодую, що проÑував це" | ||
| 342 | |||
| 343 | #: html.c:1775 html.c:4683 | ||
| 344 | msgid "Unbookmark" | ||
| 345 | msgstr "Видалити з закладок" | ||
| 346 | |||
| 347 | #: html.c:1775 | ||
| 348 | msgid "Delete this post from your bookmarks" | ||
| 349 | msgstr "Видалити цей Ð´Ð¾Ð¿Ð¸Ñ Ð· закладок" | ||
| 350 | |||
| 351 | #: html.c:1778 html.c:4678 | ||
| 352 | msgid "Bookmark" | ||
| 353 | msgstr "Додати в закладки" | ||
| 354 | |||
| 355 | #: html.c:1778 | ||
| 356 | msgid "Add this post to your bookmarks" | ||
| 357 | msgstr "Додайте цей Ð´Ð¾Ð¿Ð¸Ñ Ð² закладки" | ||
| 358 | |||
| 359 | #: html.c:1784 html.c:3179 html.c:3367 html.c:4591 | ||
| 360 | msgid "Unfollow" | ||
| 361 | msgstr "ВідпиÑатиÑÑ" | ||
| 362 | |||
| 363 | #: html.c:1784 html.c:3180 | ||
| 364 | msgid "Stop following this user's activity" | ||
| 365 | msgstr "ВідпиÑатиÑÑ Ð²Ñ–Ð´ цього кориÑтувача" | ||
| 366 | |||
| 367 | #: html.c:1788 html.c:3194 | ||
| 368 | msgid "Start following this user's activity" | ||
| 369 | msgstr "ПідпиÑатиÑÑ Ð´Ð¾ цього кориÑтувача" | ||
| 370 | |||
| 371 | #: html.c:1794 html.c:4621 | ||
| 372 | msgid "Unfollow Group" | ||
| 373 | msgstr "ВідпиÑатиÑÑ Ð²Ñ–Ð´ групи" | ||
| 374 | |||
| 375 | #: html.c:1795 | ||
| 376 | msgid "Stop following this group or channel" | ||
| 377 | msgstr "ВідпиÑатиÑÑ Ð²Ñ–Ð´ групи чи канала" | ||
| 378 | |||
| 379 | #: html.c:1799 html.c:4608 | ||
| 380 | msgid "Follow Group" | ||
| 381 | msgstr "ПідпиÑатиÑÑ Ð½Ð° групу" | ||
| 382 | |||
| 383 | #: html.c:1800 | ||
| 384 | msgid "Start following this group or channel" | ||
| 385 | msgstr "ПідпиÑатиÑÑ Ð½Ð° групу чи канал" | ||
| 386 | |||
| 387 | #: html.c:1805 html.c:3216 html.c:4552 | ||
| 388 | msgid "MUTE" | ||
| 389 | msgstr "Заглушити" | ||
| 390 | |||
| 391 | #: html.c:1806 | ||
| 392 | msgid "Block any activity from this user forever" | ||
| 393 | msgstr "Ðазавжди заблокувати активніÑть цього кориÑтувача" | ||
| 394 | |||
| 395 | #: html.c:1811 html.c:3198 html.c:4638 | ||
| 396 | msgid "Delete" | ||
| 397 | msgstr "Видалити" | ||
| 398 | |||
| 399 | #: html.c:1811 | ||
| 400 | msgid "Delete this post" | ||
| 401 | msgstr "Видалити цей допиÑ" | ||
| 402 | |||
| 403 | #: html.c:1814 html.c:4560 | ||
| 404 | msgid "Hide" | ||
| 405 | msgstr "Приховати" | ||
| 406 | |||
| 407 | #: html.c:1814 | ||
| 408 | msgid "Hide this post and its children" | ||
| 409 | msgstr "Приховати цей Ð´Ð¾Ð¿Ð¸Ñ Ñ€Ð°Ð·Ð¾Ð¼ з обговореннÑм" | ||
| 410 | |||
| 411 | #: html.c:1845 | ||
| 412 | msgid "Edit..." | ||
| 413 | msgstr "Редагувати..." | ||
| 414 | |||
| 415 | #: html.c:1865 | ||
| 416 | msgid "Reply..." | ||
| 417 | msgstr "ВідповіÑти..." | ||
| 418 | |||
| 419 | #: html.c:1916 | ||
| 420 | msgid "Truncated (too deep)" | ||
| 421 | msgstr "Обрізано (занадто багато)" | ||
| 422 | |||
| 423 | #: html.c:1925 | ||
| 424 | msgid "follows you" | ||
| 425 | msgstr "підпиÑан на ваÑ" | ||
| 426 | |||
| 427 | #: html.c:1988 | ||
| 428 | msgid "Pinned" | ||
| 429 | msgstr "Закріплено" | ||
| 430 | |||
| 431 | #: html.c:1996 | ||
| 432 | msgid "Bookmarked" | ||
| 433 | msgstr "Додано до закладок" | ||
| 434 | |||
| 435 | #: html.c:2004 | ||
| 436 | msgid "Poll" | ||
| 437 | msgstr "ОпитуваннÑ" | ||
| 438 | |||
| 439 | #: html.c:2011 | ||
| 440 | msgid "Voted" | ||
| 441 | msgstr "ПроголоÑовано" | ||
| 442 | |||
| 443 | #: html.c:2020 | ||
| 444 | msgid "Event" | ||
| 445 | msgstr "ПодіÑ" | ||
| 446 | |||
| 447 | #: html.c:2052 html.c:2081 | ||
| 448 | msgid "boosted" | ||
| 449 | msgstr "проÑунуто" | ||
| 450 | |||
| 451 | #: html.c:2097 | ||
| 452 | msgid "in reply to" | ||
| 453 | msgstr "у відповідь на" | ||
| 454 | |||
| 455 | #: html.c:2148 | ||
| 456 | msgid " [SENSITIVE CONTENT]" | ||
| 457 | msgstr " [ДЕЛІКÐТÐИЙ ВМІСТ]" | ||
| 458 | |||
| 459 | #: html.c:2325 | ||
| 460 | msgid "Vote" | ||
| 461 | msgstr "ГолоÑ" | ||
| 462 | |||
| 463 | #: html.c:2335 | ||
| 464 | msgid "Closed" | ||
| 465 | msgstr "Завершено" | ||
| 466 | |||
| 467 | #: html.c:2360 | ||
| 468 | msgid "Closes in" | ||
| 469 | msgstr "ЗавершуєтьÑÑ Ñ‡ÐµÑ€ÐµÐ·" | ||
| 470 | |||
| 471 | #: html.c:2441 | ||
| 472 | msgid "Video" | ||
| 473 | msgstr "Відео" | ||
| 474 | |||
| 475 | #: html.c:2456 | ||
| 476 | msgid "Audio" | ||
| 477 | msgstr "Ðудіо" | ||
| 478 | |||
| 479 | #: html.c:2484 | ||
| 480 | msgid "Attachment" | ||
| 481 | msgstr "ВкладеннÑ" | ||
| 482 | |||
| 483 | #: html.c:2498 | ||
| 484 | msgid "Alt..." | ||
| 485 | msgstr "ОпиÑ..." | ||
| 486 | |||
| 487 | #: html.c:2511 | ||
| 488 | msgid "Source channel or community" | ||
| 489 | msgstr "Вихідний канал або Ñпільнота" | ||
| 490 | |||
| 491 | #: html.c:2605 | ||
| 492 | msgid "Time: " | ||
| 493 | msgstr "ЧаÑ: " | ||
| 494 | |||
| 495 | #: html.c:2686 | ||
| 496 | msgid "Older..." | ||
| 497 | msgstr "Раніше..." | ||
| 498 | |||
| 499 | #: html.c:2788 | ||
| 500 | msgid "about this site" | ||
| 501 | msgstr "про цей Ñайт" | ||
| 502 | |||
| 503 | #: html.c:2790 | ||
| 504 | msgid "powered by " | ||
| 505 | msgstr "на базі " | ||
| 506 | |||
| 507 | #: html.c:2855 | ||
| 508 | msgid "Dismiss" | ||
| 509 | msgstr "Відхилити" | ||
| 510 | |||
| 511 | #: html.c:2872 | ||
| 512 | #, c-format | ||
| 513 | msgid "Timeline for list '%s'" | ||
| 514 | msgstr "Стрічки Ð´Ð»Ñ ÑпиÑку '%s'" | ||
| 515 | |||
| 516 | #: html.c:2891 html.c:3944 | ||
| 517 | msgid "Pinned posts" | ||
| 518 | msgstr "Закріплені допиÑи" | ||
| 519 | |||
| 520 | #: html.c:2903 html.c:3959 | ||
| 521 | msgid "Bookmarked posts" | ||
| 522 | msgstr "ДопиÑи у закладках" | ||
| 523 | |||
| 524 | #: html.c:2915 html.c:3974 | ||
| 525 | msgid "Post drafts" | ||
| 526 | msgstr "Чернетки допиÑів" | ||
| 527 | |||
| 528 | #: html.c:2986 | ||
| 529 | msgid "No more unseen posts" | ||
| 530 | msgstr "УÑе переглÑнуто" | ||
| 531 | |||
| 532 | #: html.c:2990 html.c:3090 | ||
| 533 | msgid "Back to top" | ||
| 534 | msgstr "ПовернутиÑÑ Ð´Ð¾ початку" | ||
| 535 | |||
| 536 | #: html.c:3043 | ||
| 537 | msgid "History" | ||
| 538 | msgstr "ІÑторіÑ" | ||
| 539 | |||
| 540 | #: html.c:3095 html.c:3515 | ||
| 541 | msgid "More..." | ||
| 542 | msgstr "Більше..." | ||
| 543 | |||
| 544 | #: html.c:3184 html.c:4574 | ||
| 545 | msgid "Unlimit" | ||
| 546 | msgstr "Без обмеженнÑ" | ||
| 547 | |||
| 548 | #: html.c:3185 | ||
| 549 | msgid "Allow announces (boosts) from this user" | ||
| 550 | msgstr "Дозволити проÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ñ–Ð´ цього кориÑтувача" | ||
| 551 | |||
| 552 | #: html.c:3188 html.c:4570 | ||
| 553 | msgid "Limit" | ||
| 554 | msgstr "Обмежити" | ||
| 555 | |||
| 556 | #: html.c:3189 | ||
| 557 | msgid "Block announces (boosts) from this user" | ||
| 558 | msgstr "Заборонити проÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ñ–Ð´ цього кориÑтувача" | ||
| 559 | |||
| 560 | #: html.c:3198 | ||
| 561 | msgid "Delete this user" | ||
| 562 | msgstr "Видалити кориÑтувача" | ||
| 563 | |||
| 564 | #: html.c:3203 html.c:4688 | ||
| 565 | msgid "Approve" | ||
| 566 | msgstr "Підтвердити" | ||
| 567 | |||
| 568 | #: html.c:3204 | ||
| 569 | msgid "Approve this follow request" | ||
| 570 | msgstr "Підтвердити цей запит на підпиÑку" | ||
| 571 | |||
| 572 | #: html.c:3207 html.c:4712 | ||
| 573 | msgid "Discard" | ||
| 574 | msgstr "Відхилити" | ||
| 575 | |||
| 576 | #: html.c:3207 | ||
| 577 | msgid "Discard this follow request" | ||
| 578 | msgstr "Відхилити цей запит на підпиÑку" | ||
| 579 | |||
| 580 | #: html.c:3212 html.c:4556 | ||
| 581 | msgid "Unmute" | ||
| 582 | msgstr "СкаÑувати глушіннÑ" | ||
| 583 | |||
| 584 | #: html.c:3213 | ||
| 585 | msgid "Stop blocking activities from this user" | ||
| 586 | msgstr "Припинити Ð³Ð»ÑƒÑˆÑ–Ð½Ð½Ñ Ð´Ñ–Ð¹ цього кориÑтувача" | ||
| 587 | |||
| 588 | #: html.c:3217 | ||
| 589 | msgid "Block any activity from this user" | ||
| 590 | msgstr "Заглушити вÑÑ– дії цього кориÑтувача" | ||
| 591 | |||
| 592 | #: html.c:3225 | ||
| 593 | msgid "Direct Message..." | ||
| 594 | msgstr "ОÑобиÑте повідомленнÑ..." | ||
| 595 | |||
| 596 | #: html.c:3260 | ||
| 597 | msgid "Pending follow confirmations" | ||
| 598 | msgstr "Запити на підпиÑку очікують на розглÑд" | ||
| 599 | |||
| 600 | #: html.c:3264 | ||
| 601 | msgid "People you follow" | ||
| 602 | msgstr "Ваші підпиÑки" | ||
| 603 | |||
| 604 | #: html.c:3265 | ||
| 605 | msgid "People that follow you" | ||
| 606 | msgstr "Ваші підпиÑники" | ||
| 607 | |||
| 608 | #: html.c:3304 | ||
| 609 | msgid "Clear all" | ||
| 610 | msgstr "ОчиÑтити вÑе" | ||
| 611 | |||
| 612 | #: html.c:3361 | ||
| 613 | msgid "Mention" | ||
| 614 | msgstr "Згадка" | ||
| 615 | |||
| 616 | #: html.c:3364 | ||
| 617 | msgid "Finished poll" | ||
| 618 | msgstr "Завершене опитуваннÑ" | ||
| 619 | |||
| 620 | #: html.c:3379 | ||
| 621 | msgid "Follow Request" | ||
| 622 | msgstr "Запит на підпиÑку" | ||
| 623 | |||
| 624 | #: html.c:3462 | ||
| 625 | msgid "Context" | ||
| 626 | msgstr "КонтекÑÑ‚" | ||
| 627 | |||
| 628 | #: html.c:3473 | ||
| 629 | msgid "New" | ||
| 630 | msgstr "Ðове" | ||
| 631 | |||
| 632 | #: html.c:3488 | ||
| 633 | msgid "Already seen" | ||
| 634 | msgstr "Вже переглÑнуто" | ||
| 635 | |||
| 636 | #: html.c:3503 | ||
| 637 | msgid "None" | ||
| 638 | msgstr "Ðема" | ||
| 639 | |||
| 640 | #: html.c:3769 | ||
| 641 | #, c-format | ||
| 642 | msgid "Search results for account %s" | ||
| 643 | msgstr "Результати пошуку Ð´Ð»Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу %s" | ||
| 644 | |||
| 645 | #: html.c:3776 | ||
| 646 | #, c-format | ||
| 647 | msgid "Account %s not found" | ||
| 648 | msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ %s не знайдений" | ||
| 649 | |||
| 650 | #: html.c:3807 | ||
| 651 | #, c-format | ||
| 652 | msgid "Search results for tag %s" | ||
| 653 | msgstr "Результати пошуку тега %s" | ||
| 654 | |||
| 655 | #: html.c:3807 | ||
| 656 | #, c-format | ||
| 657 | msgid "Nothing found for tag %s" | ||
| 658 | msgstr "Ðічого не знайдено за тегом %s" | ||
| 659 | |||
| 660 | #: html.c:3823 | ||
| 661 | #, c-format | ||
| 662 | msgid "Search results for '%s' (may be more)" | ||
| 663 | msgstr "Результати пошуку Ð´Ð»Ñ '%s' (можливо Ñ” більше)" | ||
| 664 | |||
| 665 | #: html.c:3826 | ||
| 666 | #, c-format | ||
| 667 | msgid "Search results for '%s'" | ||
| 668 | msgstr "Результати пошуку Ð´Ð»Ñ '%s'" | ||
| 669 | |||
| 670 | #: html.c:3829 | ||
| 671 | #, c-format | ||
| 672 | msgid "No more matches for '%s'" | ||
| 673 | msgstr "Ðемає більше збігів Ð´Ð»Ñ '%s'" | ||
| 674 | |||
| 675 | #: html.c:3831 | ||
| 676 | #, c-format | ||
| 677 | msgid "Nothing found for '%s'" | ||
| 678 | msgstr "Ðічого не знайдено Ð´Ð»Ñ '%s'" | ||
| 679 | |||
| 680 | #: html.c:3929 | ||
| 681 | msgid "Showing instance timeline" | ||
| 682 | msgstr "Показуємо Ñтрічку Ñервера" | ||
| 683 | |||
| 684 | #: html.c:4012 | ||
| 685 | #, c-format | ||
| 686 | msgid "Showing timeline for list '%s'" | ||
| 687 | msgstr "Показуємо Ñтрічку Ð´Ð»Ñ ÑпиÑку '%s'" | ||
| 688 | |||
| 689 | #: httpd.c:250 | ||
| 690 | #, c-format | ||
| 691 | msgid "Search results for tag #%s" | ||
| 692 | msgstr "Результати пошуку Ð´Ð»Ñ Ñ‚ÐµÐ³Ð° #%s" | ||
| 693 | |||
| 694 | #: httpd.c:259 | ||
| 695 | msgid "Recent posts by users in this instance" | ||
| 696 | msgstr "ОÑтанні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð½Ð° цьому Ñервері" | ||
| 697 | |||
| 698 | #: html.c:1603 | ||
| 699 | msgid "Blocked hashtags..." | ||
| 700 | msgstr "Заблоковані теги..." | ||
| 701 | |||
| 702 | #: html.c:432 | ||
| 703 | msgid "Optional URL to reply to" | ||
| 704 | msgstr "Ðеобов'Ñзковий URL Ð´Ð»Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ñ–" | ||
| 705 | |||
| 706 | #: html.c:575 | ||
| 707 | msgid "" | ||
| 708 | "Option 1...\n" | ||
| 709 | "Option 2...\n" | ||
| 710 | "Option 3...\n" | ||
| 711 | "..." | ||
| 712 | msgstr "" | ||
| 713 | "Варіант 1...\n" | ||
| 714 | "Варіант 2...\n" | ||
| 715 | "Варіант 3...\n" | ||
| 716 | "..." | ||
| 717 | |||
| 718 | #: html.c:1415 | ||
| 719 | msgid "Bot API key" | ||
| 720 | msgstr "Ключ API Ð´Ð»Ñ Ð±Ð¾Ñ‚Ð°" | ||
| 721 | |||
| 722 | #: html.c:1421 | ||
| 723 | msgid "Chat id" | ||
| 724 | msgstr "Id чату" | ||
| 725 | |||
| 726 | #: html.c:1429 | ||
| 727 | msgid "ntfy server - full URL (example: https://ntfy.sh/YourTopic)" | ||
| 728 | msgstr "повна URL Ñервера ntfy (наприклад https://ntfy.sh/YourTopic)" | ||
| 729 | |||
| 730 | #: html.c:1435 | ||
| 731 | msgid "ntfy token - if needed" | ||
| 732 | msgstr "токен ntfy - Ñкщо потрібно" | ||
| 733 | |||
| 734 | #: html.c:2892 | ||
| 735 | msgid "pinned" | ||
| 736 | msgstr "закріплено" | ||
| 737 | |||
| 738 | #: html.c:2904 | ||
| 739 | msgid "bookmarks" | ||
| 740 | msgstr "закладки" | ||
| 741 | |||
| 742 | #: html.c:2916 | ||
| 743 | msgid "drafts" | ||
| 744 | msgstr "чернетки" | ||
| 745 | |||
| 746 | #: html.c:464 | ||
| 747 | msgid "Scheduled post..." | ||
| 748 | msgstr "Запланувати..." | ||
| 749 | |||
| 750 | msgid "Post date and time:" | ||
| 751 | msgstr "Ð§Ð°Ñ Ð´Ð¾Ð¿Ð¸Ñу:" | ||
| 752 | |||
| 753 | #: html.c:2927 html.c:3989 | ||
| 754 | msgid "Scheduled posts" | ||
| 755 | msgstr "Заплановані допиÑи" | ||
| 756 | |||
| 757 | #: html.c:2928 | ||
| 758 | msgid "scheduled posts" | ||
| 759 | msgstr "заплановані допиÑи" | ||
| 760 | |||
| 761 | #: html.c:458 | ||
| 762 | #, c-format | ||
| 763 | msgid "Post date and time (timezone: %s):" | ||
| 764 | msgstr "Дата та Ñ‡Ð°Ñ Ð´Ð¾Ð¿Ð¸Ñу (чаÑовий поÑÑ: %s):" | ||
| 765 | |||
| 766 | #: html.c:1538 | ||
| 767 | msgid "Time zone:" | ||
| 768 | msgstr "ЧаÑовий поÑÑ:" | ||
| @@ -0,0 +1,274 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | ||
| 2 | /* copyright (c) 2025 grunfink et al. / MIT license */ | ||
| 3 | |||
| 4 | #include "xs.h" | ||
| 5 | #include "xs_html.h" | ||
| 6 | #include "xs_regex.h" | ||
| 7 | #include "xs_time.h" | ||
| 8 | #include "xs_match.h" | ||
| 9 | #include "xs_curl.h" | ||
| 10 | #include "xs_openssl.h" | ||
| 11 | #include "xs_json.h" | ||
| 12 | |||
| 13 | #include "snac.h" | ||
| 14 | |||
| 15 | xs_str *rss_from_timeline(snac *user, const xs_list *timeline, | ||
| 16 | const char *title, const char *link, const char *desc) | ||
| 17 | /* converts a timeline to rss */ | ||
| 18 | { | ||
| 19 | xs_html *rss = xs_html_tag("rss", | ||
| 20 | xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"), | ||
| 21 | xs_html_attr("version", "2.0"), | ||
| 22 | xs_html_attr("xmlns:atom", "http:/" "/www.w3.org/2005/Atom")); | ||
| 23 | |||
| 24 | xs_html *channel = xs_html_tag("channel", | ||
| 25 | xs_html_tag("title", | ||
| 26 | xs_html_text(title)), | ||
| 27 | xs_html_tag("language", | ||
| 28 | xs_html_text("en")), | ||
| 29 | xs_html_tag("link", | ||
| 30 | xs_html_text(link)), | ||
| 31 | xs_html_sctag("atom:link", | ||
| 32 | xs_html_attr("href", link), | ||
| 33 | xs_html_attr("rel", "self"), | ||
| 34 | xs_html_attr("type", "application/rss+xml")), | ||
| 35 | xs_html_tag("generator", | ||
| 36 | xs_html_text(USER_AGENT)), | ||
| 37 | xs_html_tag("description", | ||
| 38 | xs_html_text(desc))); | ||
| 39 | |||
| 40 | xs_html_add(rss, channel); | ||
| 41 | |||
| 42 | int cnt = 0; | ||
| 43 | const char *v; | ||
| 44 | |||
| 45 | xs_list_foreach(timeline, v) { | ||
| 46 | xs *msg = NULL; | ||
| 47 | |||
| 48 | if (user) { | ||
| 49 | if (!valid_status(timeline_get_by_md5(user, v, &msg))) | ||
| 50 | continue; | ||
| 51 | } | ||
| 52 | else { | ||
| 53 | if (!valid_status(object_get_by_md5(v, &msg))) | ||
| 54 | continue; | ||
| 55 | } | ||
| 56 | |||
| 57 | const char *id = xs_dict_get(msg, "id"); | ||
| 58 | const char *content = xs_dict_get(msg, "content"); | ||
| 59 | const char *published = xs_dict_get(msg, "published"); | ||
| 60 | |||
| 61 | if (user && !xs_startswith(id, user->actor)) | ||
| 62 | continue; | ||
| 63 | |||
| 64 | if (!id || !content || !published) | ||
| 65 | continue; | ||
| 66 | |||
| 67 | /* create a title with the first line of the content */ | ||
| 68 | xs *title = xs_replace(content, "<br>", "\n"); | ||
| 69 | title = xs_regex_replace_i(title, "<[^>]+>", " "); | ||
| 70 | title = xs_regex_replace_i(title, "&[^;]+;", " "); | ||
| 71 | int i; | ||
| 72 | |||
| 73 | for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); | ||
| 74 | |||
| 75 | if (title[i] != '\0') { | ||
| 76 | title[i] = '\0'; | ||
| 77 | title = xs_str_cat(title, "..."); | ||
| 78 | } | ||
| 79 | |||
| 80 | title = xs_strip_i(title); | ||
| 81 | |||
| 82 | /* convert the date */ | ||
| 83 | time_t t = xs_parse_iso_date(published, 0); | ||
| 84 | xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000"); | ||
| 85 | |||
| 86 | /* if it's the first one, add it to the header */ | ||
| 87 | if (cnt == 0) | ||
| 88 | xs_html_add(channel, | ||
| 89 | xs_html_tag("lastBuildDate", | ||
| 90 | xs_html_text(rss_date))); | ||
| 91 | |||
| 92 | xs_html_add(channel, | ||
| 93 | xs_html_tag("item", | ||
| 94 | xs_html_tag("title", | ||
| 95 | xs_html_text(title)), | ||
| 96 | xs_html_tag("link", | ||
| 97 | xs_html_text(id)), | ||
| 98 | xs_html_tag("guid", | ||
| 99 | xs_html_text(id)), | ||
| 100 | xs_html_tag("pubDate", | ||
| 101 | xs_html_text(rss_date)), | ||
| 102 | xs_html_tag("description", | ||
| 103 | xs_html_text(content)))); | ||
| 104 | |||
| 105 | cnt++; | ||
| 106 | } | ||
| 107 | |||
| 108 | return xs_html_render_s(rss, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); | ||
| 109 | } | ||
| 110 | |||
| 111 | |||
| 112 | void rss_to_timeline(snac *user, const char *url) | ||
| 113 | /* reads an RSS and inserts all ActivityPub posts into the user's timeline */ | ||
| 114 | { | ||
| 115 | if (!xs_startswith(url, "https:/") && !xs_startswith(url, "http:/")) | ||
| 116 | return; | ||
| 117 | |||
| 118 | xs *hdrs = xs_dict_new(); | ||
| 119 | hdrs = xs_dict_set(hdrs, "accept", "application/rss+xml"); | ||
| 120 | hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); | ||
| 121 | |||
| 122 | /* get the RSS metadata */ | ||
| 123 | xs *md5 = xs_md5_hex(url, strlen(url)); | ||
| 124 | xs *rss_md_fn = xs_fmt("%s/rss", user->basedir); | ||
| 125 | mkdirx(rss_md_fn); | ||
| 126 | rss_md_fn = xs_str_cat(rss_md_fn, "/", md5, ".json"); | ||
| 127 | |||
| 128 | xs *rss_md = NULL; | ||
| 129 | const char *etag = NULL; | ||
| 130 | |||
| 131 | FILE *f; | ||
| 132 | if ((f = fopen(rss_md_fn, "r")) != NULL) { | ||
| 133 | rss_md = xs_json_load(f); | ||
| 134 | fclose(f); | ||
| 135 | |||
| 136 | etag = xs_dict_get(rss_md, "etag"); | ||
| 137 | |||
| 138 | if (xs_is_string(etag)) | ||
| 139 | hdrs = xs_dict_set(hdrs, "if-none-match", etag); | ||
| 140 | } | ||
| 141 | |||
| 142 | if (rss_md == NULL) | ||
| 143 | rss_md = xs_dict_new(); | ||
| 144 | |||
| 145 | xs *payload = NULL; | ||
| 146 | int status; | ||
| 147 | int p_size; | ||
| 148 | |||
| 149 | xs *rsp = xs_http_request("GET", url, hdrs, NULL, 0, &status, &payload, &p_size, 0); | ||
| 150 | |||
| 151 | snac_log(user, xs_fmt("parsing RSS %s %d", url, status)); | ||
| 152 | |||
| 153 | if (!valid_status(status) || !xs_is_string(payload)) | ||
| 154 | return; | ||
| 155 | |||
| 156 | /* not an RSS? done */ | ||
| 157 | const char *ctype = xs_dict_get(rsp, "content-type"); | ||
| 158 | if (!xs_is_string(ctype) || xs_str_in(ctype, "application/rss+xml") == -1) | ||
| 159 | return; | ||
| 160 | |||
| 161 | /* yes, parsing is done with regexes (now I have two problems blah blah blah) */ | ||
| 162 | xs *links = xs_regex_select(payload, "<link>[^<]+</link>"); | ||
| 163 | const char *link; | ||
| 164 | |||
| 165 | xs_list_foreach(links, link) { | ||
| 166 | xs *l = xs_replace(link, "<link>", ""); | ||
| 167 | char *p = strchr(l, '<'); | ||
| 168 | |||
| 169 | if (p == NULL) | ||
| 170 | continue; | ||
| 171 | *p = '\0'; | ||
| 172 | |||
| 173 | /* skip this same URL */ | ||
| 174 | if (strcmp(l, url) == 0) | ||
| 175 | continue; | ||
| 176 | |||
| 177 | /* skip crap */ | ||
| 178 | if (!xs_startswith(l, "https:/") && !xs_startswith(l, "http:/")) | ||
| 179 | continue; | ||
| 180 | |||
| 181 | snac_debug(user, 1, xs_fmt("RSS link: %s", l)); | ||
| 182 | |||
| 183 | if (timeline_here(user, l)) { | ||
| 184 | snac_debug(user, 1, xs_fmt("RSS entry already in timeline %s", l)); | ||
| 185 | continue; | ||
| 186 | } | ||
| 187 | |||
| 188 | /* special trick for Mastodon: convert from the alternate format */ | ||
| 189 | if (strchr(l, '@') != NULL) { | ||
| 190 | xs *l2 = xs_split(l, "/"); | ||
| 191 | |||
| 192 | if (xs_list_len(l2) == 5) { | ||
| 193 | const char *uid = xs_list_get(l2, 3); | ||
| 194 | if (*uid == '@') { | ||
| 195 | xs *guessed_id = xs_fmt("https:/" "/%s/users/%s/statuses/%s", | ||
| 196 | xs_list_get(l2, 2), uid + 1, xs_list_get(l2, -1)); | ||
| 197 | |||
| 198 | if (timeline_here(user, guessed_id)) { | ||
| 199 | snac_debug(user, 1, xs_fmt("RSS entry already in timeline (alt) %s", guessed_id)); | ||
| 200 | continue; | ||
| 201 | } | ||
| 202 | } | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | xs *obj = NULL; | ||
| 207 | |||
| 208 | if (!valid_status(object_get(l, &obj))) { | ||
| 209 | /* object is not here: bring it */ | ||
| 210 | if (!valid_status(activitypub_request(user, l, &obj))) | ||
| 211 | continue; | ||
| 212 | } | ||
| 213 | |||
| 214 | if (xs_is_dict(obj)) { | ||
| 215 | const char *id = xs_dict_get(obj, "id"); | ||
| 216 | const char *type = xs_dict_get(obj, "type"); | ||
| 217 | const char *attr_to = get_atto(obj); | ||
| 218 | |||
| 219 | if (!xs_is_string(id) || !xs_is_string(type) || !xs_is_string(attr_to)) | ||
| 220 | continue; | ||
| 221 | |||
| 222 | if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) | ||
| 223 | continue; | ||
| 224 | |||
| 225 | if (timeline_here(user, id)) { | ||
| 226 | snac_debug(user, 1, xs_fmt("RSS entry already in timeline (id) %s", id)); | ||
| 227 | continue; | ||
| 228 | } | ||
| 229 | |||
| 230 | enqueue_actor_refresh(user, attr_to, 0); | ||
| 231 | |||
| 232 | timeline_add(user, id, obj); | ||
| 233 | |||
| 234 | snac_log(user, xs_fmt("new '%s' (RSS) %s %s", type, attr_to, id)); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | /* update the RSS metadata */ | ||
| 239 | etag = xs_dict_get(rsp, "etag"); | ||
| 240 | |||
| 241 | if (xs_is_string(etag)) { | ||
| 242 | rss_md = xs_dict_set(rss_md, "etag", etag); | ||
| 243 | rss_md = xs_dict_set(rss_md, "url", url); | ||
| 244 | if ((f = fopen(rss_md_fn, "w")) != NULL) { | ||
| 245 | xs_json_dump(rss_md, 4, f); | ||
| 246 | fclose(f); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | |||
| 252 | void rss_poll_hashtags(void) | ||
| 253 | /* parses all RSS from all users */ | ||
| 254 | { | ||
| 255 | xs *list = user_list(); | ||
| 256 | const char *uid; | ||
| 257 | |||
| 258 | xs_list_foreach(list, uid) { | ||
| 259 | snac user; | ||
| 260 | |||
| 261 | if (user_open(&user, uid)) { | ||
| 262 | const xs_list *rss = xs_dict_get(user.config, "followed_hashtags"); | ||
| 263 | |||
| 264 | if (xs_is_list(rss)) { | ||
| 265 | const char *url; | ||
| 266 | |||
| 267 | xs_list_foreach(rss, url) | ||
| 268 | rss_to_timeline(&user, url); | ||
| 269 | } | ||
| 270 | |||
| 271 | user_free(&user); | ||
| 272 | } | ||
| 273 | } | ||
| 274 | } | ||
| @@ -13,6 +13,14 @@ void sbox_enter(const char *basedir) | |||
| 13 | return; | 13 | return; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | int smail; | ||
| 17 | const char *url = xs_dict_get(srv_config, "smtp_url"); | ||
| 18 | |||
| 19 | if (xs_is_string(url) && *url) | ||
| 20 | smail = 0; | ||
| 21 | else | ||
| 22 | smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications")); | ||
| 23 | |||
| 16 | srv_debug(1, xs_fmt("Calling unveil()")); | 24 | srv_debug(1, xs_fmt("Calling unveil()")); |
| 17 | unveil(basedir, "rwc"); | 25 | unveil(basedir, "rwc"); |
| 18 | unveil("/tmp", "rwc"); | 26 | unveil("/tmp", "rwc"); |
| @@ -25,6 +33,9 @@ void sbox_enter(const char *basedir) | |||
| 25 | if (*address == '/') | 33 | if (*address == '/') |
| 26 | unveil(address, "rwc"); | 34 | unveil(address, "rwc"); |
| 27 | 35 | ||
| 36 | if (smail) | ||
| 37 | unveil("/usr/sbin/sendmail", "x"); | ||
| 38 | |||
| 28 | unveil(NULL, NULL); | 39 | unveil(NULL, NULL); |
| 29 | 40 | ||
| 30 | srv_debug(1, xs_fmt("Calling pledge()")); | 41 | srv_debug(1, xs_fmt("Calling pledge()")); |
| @@ -34,6 +45,9 @@ void sbox_enter(const char *basedir) | |||
| 34 | if (*address == '/') | 45 | if (*address == '/') |
| 35 | p = xs_str_cat(p, " unix"); | 46 | p = xs_str_cat(p, " unix"); |
| 36 | 47 | ||
| 48 | if (smail) | ||
| 49 | p = xs_str_cat(p, " exec"); | ||
| 50 | |||
| 37 | pledge(p, NULL); | 51 | pledge(p, NULL); |
| 38 | } | 52 | } |
| 39 | 53 | ||
| @@ -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.77" | 4 | #define VERSION "2.81-dev" |
| 5 | 5 | ||
| 6 | #define USER_AGENT "snac/" VERSION | 6 | #define USER_AGENT "snac/" VERSION |
| 7 | 7 | ||
| @@ -164,7 +164,8 @@ int pending_count(snac *user); | |||
| 164 | 164 | ||
| 165 | double timeline_mtime(snac *snac); | 165 | double timeline_mtime(snac *snac); |
| 166 | int timeline_touch(snac *snac); | 166 | int timeline_touch(snac *snac); |
| 167 | int timeline_here(snac *snac, const char *md5); | 167 | int timeline_here_by_md5(snac *snac, const char *md5); |
| 168 | int timeline_here(snac *snac, const char *id); | ||
| 168 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); | 169 | int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg); |
| 169 | int timeline_del(snac *snac, const char *id); | 170 | int timeline_del(snac *snac, const char *id); |
| 170 | xs_str *user_index_fn(snac *user, const char *idx_name); | 171 | xs_str *user_index_fn(snac *user, const char *idx_name); |
| @@ -293,6 +294,8 @@ void enqueue_object_request(snac *user, const char *id, int forward_secs); | |||
| 293 | void enqueue_verify_links(snac *user); | 294 | void enqueue_verify_links(snac *user); |
| 294 | void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs); | 295 | void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs); |
| 295 | void enqueue_webmention(const xs_dict *msg); | 296 | void enqueue_webmention(const xs_dict *msg); |
| 297 | void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries); | ||
| 298 | |||
| 296 | int was_question_voted(snac *user, const char *id); | 299 | int was_question_voted(snac *user, const char *id); |
| 297 | 300 | ||
| 298 | xs_list *user_queue(snac *snac); | 301 | xs_list *user_queue(snac *snac); |
| @@ -322,7 +325,7 @@ void httpd(void); | |||
| 322 | int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str **user); | 325 | int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str **user); |
| 323 | int webfinger_request(const char *qs, xs_str **actor, xs_str **user); | 326 | int webfinger_request(const char *qs, xs_str **actor, xs_str **user); |
| 324 | int webfinger_request_fake(const char *qs, xs_str **actor, xs_str **user); | 327 | int webfinger_request_fake(const char *qs, xs_str **actor, xs_str **user); |
| 325 | int webfinger_get_handler(xs_dict *req, const char *q_path, | 328 | int webfinger_get_handler(const xs_dict *req, const char *q_path, |
| 326 | xs_val **body, int *b_size, char **ctype); | 329 | xs_val **body, int *b_size, char **ctype); |
| 327 | 330 | ||
| 328 | const char *default_avatar_base64(void); | 331 | const char *default_avatar_base64(void); |
| @@ -394,8 +397,6 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 394 | int html_post_handler(const xs_dict *req, const char *q_path, | 397 | int html_post_handler(const xs_dict *req, const char *q_path, |
| 395 | char *payload, int p_size, | 398 | char *payload, int p_size, |
| 396 | char **body, int *b_size, char **ctype); | 399 | char **body, int *b_size, char **ctype); |
| 397 | xs_str *timeline_to_rss(snac *user, const xs_list *timeline, | ||
| 398 | const char *title, const char *link, const char *desc); | ||
| 399 | 400 | ||
| 400 | int write_default_css(void); | 401 | int write_default_css(void); |
| 401 | int snac_init(const char *_basedir); | 402 | int snac_init(const char *_basedir); |
| @@ -433,6 +434,8 @@ void mastoapi_purge(void); | |||
| 433 | void verify_links(snac *user); | 434 | void verify_links(snac *user); |
| 434 | 435 | ||
| 435 | void export_csv(snac *user); | 436 | void export_csv(snac *user); |
| 437 | void export_posts(snac *user); | ||
| 438 | |||
| 436 | int migrate_account(snac *user); | 439 | int migrate_account(snac *user); |
| 437 | 440 | ||
| 438 | void import_blocked_accounts_csv(snac *user, const char *fn); | 441 | void import_blocked_accounts_csv(snac *user, const char *fn); |
| @@ -461,3 +464,8 @@ int badlogin_check(const char *user, const char *addr); | |||
| 461 | void badlogin_inc(const char *user, const char *addr); | 464 | void badlogin_inc(const char *user, const char *addr); |
| 462 | 465 | ||
| 463 | const char *lang_str(const char *str, const snac *user); | 466 | const char *lang_str(const char *str, const snac *user); |
| 467 | |||
| 468 | xs_str *rss_from_timeline(snac *user, const xs_list *timeline, | ||
| 469 | const char *title, const char *link, const char *desc); | ||
| 470 | void rss_to_timeline(snac *user, const char *url); | ||
| 471 | void rss_poll_hashtags(void); | ||
| @@ -45,6 +45,7 @@ static const char *default_srv_config = "{" | |||
| 45 | static const char *default_css = | 45 | static const char *default_css = |
| 46 | "body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; }\n" | 46 | "body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; }\n" |
| 47 | "pre { overflow-x: scroll; }\n" | 47 | "pre { overflow-x: scroll; }\n" |
| 48 | "blockquote { font-style: italic; }\n" | ||
| 48 | ".snac-embedded-video, img { max-width: 100% }\n" | 49 | ".snac-embedded-video, img { max-width: 100% }\n" |
| 49 | ".snac-origin { font-size: 85% }\n" | 50 | ".snac-origin { font-size: 85% }\n" |
| 50 | ".snac-score { float: right; font-size: 85% }\n" | 51 | ".snac-score { float: right; font-size: 85% }\n" |
| @@ -488,6 +489,26 @@ void verify_links(snac *user) | |||
| 488 | 489 | ||
| 489 | int c = 0; | 490 | int c = 0; |
| 490 | while (metadata && xs_dict_next(metadata, &k, &v, &c)) { | 491 | while (metadata && xs_dict_next(metadata, &k, &v, &c)) { |
| 492 | xs *wfinger = NULL; | ||
| 493 | const char *ov = NULL; | ||
| 494 | |||
| 495 | /* is it an account handle? */ | ||
| 496 | if (*v == '@' && strchr(v + 1, '@')) { | ||
| 497 | /* resolve it via webfinger */ | ||
| 498 | if (valid_status(webfinger_request(v, &wfinger, NULL)) && xs_is_string(wfinger)) { | ||
| 499 | ov = v; | ||
| 500 | v = wfinger; | ||
| 501 | |||
| 502 | /* store the alias */ | ||
| 503 | if (user->links == NULL) | ||
| 504 | user->links = xs_dict_new(); | ||
| 505 | |||
| 506 | user->links = xs_dict_set(user->links, ov, v); | ||
| 507 | |||
| 508 | changed++; | ||
| 509 | } | ||
| 510 | } | ||
| 511 | |||
| 491 | /* not an https link? skip */ | 512 | /* not an https link? skip */ |
| 492 | if (!xs_startswith(v, "https:/" "/")) | 513 | if (!xs_startswith(v, "https:/" "/")) |
| 493 | continue; | 514 | continue; |
| @@ -705,6 +726,61 @@ void export_csv(snac *user) | |||
| 705 | } | 726 | } |
| 706 | 727 | ||
| 707 | 728 | ||
| 729 | void export_posts(snac *user) | ||
| 730 | /* exports all posts to an OrderedCollection */ | ||
| 731 | { | ||
| 732 | xs *ifn = xs_fmt("%s/public.idx", user->basedir); | ||
| 733 | xs *index = index_list(ifn, XS_ALL); | ||
| 734 | xs *ofn = xs_fmt("%s/export/outbox.json", user->basedir); | ||
| 735 | FILE *f; | ||
| 736 | |||
| 737 | if ((f = fopen(ofn, "w")) == NULL) { | ||
| 738 | snac_log(user, xs_fmt("Cannot create file %s", ofn)); | ||
| 739 | return; | ||
| 740 | } | ||
| 741 | |||
| 742 | int cnt = 0; | ||
| 743 | |||
| 744 | /* raw output */ | ||
| 745 | fprintf(f, "{\"@context\": \"https:/" "/www.w3.org/ns/activitystreams\","); | ||
| 746 | fprintf(f, "\"id\": \"outbox.json\","); | ||
| 747 | fprintf(f, "\"type\": \"OrderedCollection\","); | ||
| 748 | fprintf(f, "\"orderedItems\": ["); | ||
| 749 | |||
| 750 | const char *md5; | ||
| 751 | |||
| 752 | snac_log(user, xs_fmt("Creating %s...", ofn)); | ||
| 753 | |||
| 754 | xs_list_foreach(index, md5) { | ||
| 755 | xs *obj = NULL; | ||
| 756 | |||
| 757 | if (!valid_status(object_get_by_md5(md5, &obj))) | ||
| 758 | continue; | ||
| 759 | |||
| 760 | const char *type = xs_dict_get(obj, "type"); | ||
| 761 | |||
| 762 | if (!xs_is_string(type) || strcmp(type, "Note")) | ||
| 763 | continue; | ||
| 764 | |||
| 765 | const char *atto = get_atto(obj); | ||
| 766 | |||
| 767 | if (!xs_is_string(atto) || strcmp(atto, user->actor)) | ||
| 768 | continue; | ||
| 769 | |||
| 770 | if (cnt) | ||
| 771 | fprintf(f, ","); | ||
| 772 | |||
| 773 | xs *c_msg = msg_create(user, obj); | ||
| 774 | xs_json_dump(c_msg, 0, f); | ||
| 775 | cnt++; | ||
| 776 | } | ||
| 777 | |||
| 778 | fprintf(f, "], \"totalItems\": %d}", cnt); | ||
| 779 | |||
| 780 | fclose(f); | ||
| 781 | } | ||
| 782 | |||
| 783 | |||
| 708 | void import_blocked_accounts_csv(snac *user, const char *ifn) | 784 | void import_blocked_accounts_csv(snac *user, const char *ifn) |
| 709 | /* imports a Mastodon CSV file of blocked accounts */ | 785 | /* imports a Mastodon CSV file of blocked accounts */ |
| 710 | { | 786 | { |
diff --git a/webfinger.c b/webfinger.c index 5db9a97..46b7edb 100644 --- a/webfinger.c +++ b/webfinger.c | |||
| @@ -93,7 +93,7 @@ int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str | |||
| 93 | if (user != NULL) { | 93 | if (user != NULL) { |
| 94 | const char *subject = xs_dict_get(obj, "subject"); | 94 | const char *subject = xs_dict_get(obj, "subject"); |
| 95 | 95 | ||
| 96 | if (subject) | 96 | if (subject && xs_startswith(subject, "acct:")) |
| 97 | *user = xs_replace_n(subject, "acct:", "", 1); | 97 | *user = xs_replace_n(subject, "acct:", "", 1); |
| 98 | } | 98 | } |
| 99 | 99 | ||
| @@ -152,7 +152,7 @@ int webfinger_request_fake(const char *qs, xs_str **actor, xs_str **user) | |||
| 152 | } | 152 | } |
| 153 | 153 | ||
| 154 | 154 | ||
| 155 | int webfinger_get_handler(xs_dict *req, const char *q_path, | 155 | int webfinger_get_handler(const xs_dict *req, const char *q_path, |
| 156 | xs_val **body, int *b_size, char **ctype) | 156 | xs_val **body, int *b_size, char **ctype) |
| 157 | /* serves webfinger queries */ | 157 | /* serves webfinger queries */ |
| 158 | { | 158 | { |
| @@ -90,6 +90,7 @@ xs_str *xs_rstrip_chars_i(xs_str *str, const char *chars); | |||
| 90 | xs_str *xs_strip_chars_i(xs_str *str, const char *chars); | 90 | xs_str *xs_strip_chars_i(xs_str *str, const char *chars); |
| 91 | #define xs_strip_i(str) xs_strip_chars_i(str, " \r\n\t\v\f") | 91 | #define xs_strip_i(str) xs_strip_chars_i(str, " \r\n\t\v\f") |
| 92 | xs_str *xs_tolower_i(xs_str *str); | 92 | xs_str *xs_tolower_i(xs_str *str); |
| 93 | xs_str *xs_toupper_i(xs_str *str); | ||
| 93 | 94 | ||
| 94 | xs_list *xs_list_new(void); | 95 | xs_list *xs_list_new(void); |
| 95 | xs_list *xs_list_append_m(xs_list *list, const char *mem, int dsz); | 96 | xs_list *xs_list_append_m(xs_list *list, const char *mem, int dsz); |
| @@ -692,6 +693,20 @@ xs_str *xs_tolower_i(xs_str *str) | |||
| 692 | } | 693 | } |
| 693 | 694 | ||
| 694 | 695 | ||
| 696 | xs_str *xs_toupper_i(xs_str *str) | ||
| 697 | /* convert to lowercase */ | ||
| 698 | { | ||
| 699 | XS_ASSERT_TYPE(str, XSTYPE_STRING); | ||
| 700 | |||
| 701 | int n; | ||
| 702 | |||
| 703 | for (n = 0; str[n]; n++) | ||
| 704 | str[n] = toupper(str[n]); | ||
| 705 | |||
| 706 | return str; | ||
| 707 | } | ||
| 708 | |||
| 709 | |||
| 695 | /** lists **/ | 710 | /** lists **/ |
| 696 | 711 | ||
| 697 | xs_list *xs_list_new(void) | 712 | xs_list *xs_list_new(void) |
| @@ -13,7 +13,7 @@ | |||
| 13 | #define _XS_FCGI_H | 13 | #define _XS_FCGI_H |
| 14 | 14 | ||
| 15 | xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *id); | 15 | xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *id); |
| 16 | void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int id); | 16 | void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int id); |
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | #ifdef XS_IMPLEMENTATION | 19 | #ifdef XS_IMPLEMENTATION |
| @@ -290,7 +290,7 @@ end: | |||
| 290 | } | 290 | } |
| 291 | 291 | ||
| 292 | 292 | ||
| 293 | void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int fcgi_id) | 293 | void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int fcgi_id) |
| 294 | /* writes an FCGI response */ | 294 | /* writes an FCGI response */ |
| 295 | { | 295 | { |
| 296 | struct fcgi_record_header hdr = {0}; | 296 | struct fcgi_record_header hdr = {0}; |
| @@ -5,7 +5,8 @@ | |||
| 5 | #define _XS_HTTPD_H | 5 | #define _XS_HTTPD_H |
| 6 | 6 | ||
| 7 | xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size); | 7 | xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size); |
| 8 | void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size); | 8 | void xs_httpd_response(FILE *f, int status, const char *status_text, |
| 9 | const xs_dict *headers, const xs_val *body, int b_size); | ||
| 9 | 10 | ||
| 10 | 11 | ||
| 11 | #ifdef XS_IMPLEMENTATION | 12 | #ifdef XS_IMPLEMENTATION |
| @@ -109,16 +110,15 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size) | |||
| 109 | } | 110 | } |
| 110 | 111 | ||
| 111 | 112 | ||
| 112 | void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size) | 113 | void xs_httpd_response(FILE *f, int status, const char *status_text, |
| 114 | const xs_dict *headers, const xs_val *body, int b_size) | ||
| 113 | /* sends an httpd response */ | 115 | /* sends an httpd response */ |
| 114 | { | 116 | { |
| 115 | xs *proto; | 117 | fprintf(f, "HTTP/1.1 %d %s\r\n", status, status_text ? status_text : ""); |
| 118 | |||
| 116 | const xs_str *k; | 119 | const xs_str *k; |
| 117 | const xs_val *v; | 120 | const xs_val *v; |
| 118 | 121 | ||
| 119 | proto = xs_fmt("HTTP/1.1 %d %s", status, status_text); | ||
| 120 | fprintf(f, "%s\r\n", proto); | ||
| 121 | |||
| 122 | xs_dict_foreach(headers, k, v) { | 122 | xs_dict_foreach(headers, k, v) { |
| 123 | fprintf(f, "%s: %s\r\n", k, v); | 123 | fprintf(f, "%s: %s\r\n", k, v); |
| 124 | } | 124 | } |
| @@ -8,6 +8,7 @@ | |||
| 8 | #define MAX_JSON_DEPTH 32 | 8 | #define MAX_JSON_DEPTH 32 |
| 9 | #endif | 9 | #endif |
| 10 | 10 | ||
| 11 | void xs_json_dump_value(const xs_val *data, int level, int indent, FILE *f); | ||
| 11 | int xs_json_dump(const xs_val *data, int indent, FILE *f); | 12 | int xs_json_dump(const xs_val *data, int indent, FILE *f); |
| 12 | xs_str *xs_json_dumps(const xs_val *data, int indent); | 13 | xs_str *xs_json_dumps(const xs_val *data, int indent); |
| 13 | 14 | ||
| @@ -19,8 +20,8 @@ xs_val *xs_json_loads_full(const xs_str *json, int maxdepth); | |||
| 19 | xstype xs_json_load_type(FILE *f); | 20 | xstype xs_json_load_type(FILE *f); |
| 20 | int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); | 21 | int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); |
| 21 | int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); | 22 | int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); |
| 22 | xs_list *xs_json_load_array(FILE *f, int maxdepth); | 23 | int xs_json_load_array(FILE *f, int maxdepth, xs_list **l); |
| 23 | xs_dict *xs_json_load_object(FILE *f, int maxdepth); | 24 | int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d); |
| 24 | 25 | ||
| 25 | 26 | ||
| 26 | #ifdef XS_IMPLEMENTATION | 27 | #ifdef XS_IMPLEMENTATION |
| @@ -77,7 +78,7 @@ static void _xs_json_indent(int level, int indent, FILE *f) | |||
| 77 | } | 78 | } |
| 78 | 79 | ||
| 79 | 80 | ||
| 80 | static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) | 81 | void xs_json_dump_value(const xs_val *data, int level, int indent, FILE *f) |
| 81 | /* dumps partial data as JSON */ | 82 | /* dumps partial data as JSON */ |
| 82 | { | 83 | { |
| 83 | int c = 0; | 84 | int c = 0; |
| @@ -108,7 +109,7 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) | |||
| 108 | fputc(',', f); | 109 | fputc(',', f); |
| 109 | 110 | ||
| 110 | _xs_json_indent(level + 1, indent, f); | 111 | _xs_json_indent(level + 1, indent, f); |
| 111 | _xs_json_dump(v, level + 1, indent, f); | 112 | xs_json_dump_value(v, level + 1, indent, f); |
| 112 | 113 | ||
| 113 | c++; | 114 | c++; |
| 114 | } | 115 | } |
| @@ -135,7 +136,7 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) | |||
| 135 | if (indent) | 136 | if (indent) |
| 136 | fputc(' ', f); | 137 | fputc(' ', f); |
| 137 | 138 | ||
| 138 | _xs_json_dump(v, level + 1, indent, f); | 139 | xs_json_dump_value(v, level + 1, indent, f); |
| 139 | 140 | ||
| 140 | c++; | 141 | c++; |
| 141 | } | 142 | } |
| @@ -154,6 +155,20 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) | |||
| 154 | } | 155 | } |
| 155 | 156 | ||
| 156 | 157 | ||
| 158 | int xs_json_dump(const xs_val *data, int indent, FILE *f) | ||
| 159 | /* dumps data into a file as JSON */ | ||
| 160 | { | ||
| 161 | xstype t = xs_type(data); | ||
| 162 | |||
| 163 | if (t == XSTYPE_LIST || t == XSTYPE_DICT) { | ||
| 164 | xs_json_dump_value(data, 0, indent, f); | ||
| 165 | return 1; | ||
| 166 | } | ||
| 167 | |||
| 168 | return 0; | ||
| 169 | } | ||
| 170 | |||
| 171 | |||
| 157 | xs_str *xs_json_dumps(const xs_val *data, int indent) | 172 | xs_str *xs_json_dumps(const xs_val *data, int indent) |
| 158 | /* dumps data as a JSON string */ | 173 | /* dumps data as a JSON string */ |
| 159 | { | 174 | { |
| @@ -173,20 +188,6 @@ xs_str *xs_json_dumps(const xs_val *data, int indent) | |||
| 173 | } | 188 | } |
| 174 | 189 | ||
| 175 | 190 | ||
| 176 | int xs_json_dump(const xs_val *data, int indent, FILE *f) | ||
| 177 | /* dumps data into a file as JSON */ | ||
| 178 | { | ||
| 179 | xstype t = xs_type(data); | ||
| 180 | |||
| 181 | if (t == XSTYPE_LIST || t == XSTYPE_DICT) { | ||
| 182 | _xs_json_dump(data, 0, indent, f); | ||
| 183 | return 1; | ||
| 184 | } | ||
| 185 | |||
| 186 | return 0; | ||
| 187 | } | ||
| 188 | |||
| 189 | |||
| 190 | /** JSON loads **/ | 191 | /** JSON loads **/ |
| 191 | 192 | ||
| 192 | typedef enum { | 193 | typedef enum { |
| @@ -370,6 +371,8 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) | |||
| 370 | else | 371 | else |
| 371 | return -1; | 372 | return -1; |
| 372 | } | 373 | } |
| 374 | else | ||
| 375 | *pt = xs_type(*value); | ||
| 373 | 376 | ||
| 374 | *c = *c + 1; | 377 | *c = *c + 1; |
| 375 | 378 | ||
| @@ -377,43 +380,51 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) | |||
| 377 | } | 380 | } |
| 378 | 381 | ||
| 379 | 382 | ||
| 380 | xs_list *xs_json_load_array(FILE *f, int maxdepth) | 383 | int xs_json_load_array(FILE *f, int maxdepth, xs_list **l) |
| 381 | /* loads a full JSON array (after the initial OBRACK) */ | 384 | /* loads a full JSON array (after the initial OBRACK) */ |
| 385 | /* l can be NULL for the content to be dropped */ | ||
| 382 | { | 386 | { |
| 383 | xstype t; | 387 | xstype t; |
| 384 | xs_list *l = xs_list_new(); | 388 | int r = 0; |
| 385 | int c = 0; | 389 | int c = 0; |
| 386 | 390 | ||
| 387 | for (;;) { | 391 | for (;;) { |
| 388 | xs *v = NULL; | 392 | xs *v = NULL; |
| 389 | int r = xs_json_load_array_iter(f, &v, &t, &c); | 393 | r = xs_json_load_array_iter(f, &v, &t, &c); |
| 390 | |||
| 391 | if (r == -1) | ||
| 392 | l = xs_free(l); | ||
| 393 | 394 | ||
| 394 | if (r == 1) { | 395 | if (r == 1) { |
| 395 | /* partial load? */ | 396 | /* partial load? */ |
| 396 | if (v == NULL && maxdepth != 0) { | 397 | if (v == NULL && maxdepth != 0) { |
| 397 | if (t == XSTYPE_LIST) | 398 | if (t == XSTYPE_LIST) { |
| 398 | v = xs_json_load_array(f, maxdepth - 1); | 399 | if (l) |
| 400 | v = xs_list_new(); | ||
| 401 | |||
| 402 | r = xs_json_load_array(f, maxdepth - 1, &v); | ||
| 403 | } | ||
| 399 | else | 404 | else |
| 400 | if (t == XSTYPE_DICT) | 405 | if (t == XSTYPE_DICT) { |
| 401 | v = xs_json_load_object(f, maxdepth - 1); | 406 | if (l) |
| 407 | v = xs_dict_new(); | ||
| 408 | |||
| 409 | r = xs_json_load_object(f, maxdepth - 1, &v); | ||
| 410 | } | ||
| 402 | } | 411 | } |
| 403 | 412 | ||
| 404 | /* still null? fail */ | 413 | /* error? */ |
| 405 | if (v == NULL) { | 414 | if (r < 0) |
| 406 | l = xs_free(l); | ||
| 407 | break; | 415 | break; |
| 408 | } | ||
| 409 | 416 | ||
| 410 | l = xs_list_append(l, v); | 417 | if (l) |
| 418 | *l = xs_list_append(*l, v); | ||
| 411 | } | 419 | } |
| 412 | else | 420 | else |
| 413 | break; | 421 | break; |
| 414 | } | 422 | } |
| 415 | 423 | ||
| 416 | return l; | 424 | if (r < 0 && l) |
| 425 | *l = xs_free(*l); | ||
| 426 | |||
| 427 | return r; | ||
| 417 | } | 428 | } |
| 418 | 429 | ||
| 419 | 430 | ||
| @@ -458,6 +469,8 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, | |||
| 458 | else | 469 | else |
| 459 | return -1; | 470 | return -1; |
| 460 | } | 471 | } |
| 472 | else | ||
| 473 | *pt = xs_type(*value); | ||
| 461 | 474 | ||
| 462 | *c = *c + 1; | 475 | *c = *c + 1; |
| 463 | 476 | ||
| @@ -465,59 +478,52 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, | |||
| 465 | } | 478 | } |
| 466 | 479 | ||
| 467 | 480 | ||
| 468 | xs_dict *xs_json_load_object(FILE *f, int maxdepth) | 481 | int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d) |
| 469 | /* loads a full JSON object (after the initial OCURLY) */ | 482 | /* loads a full JSON object (after the initial OCURLY) */ |
| 483 | /* d can be NULL for the content to be dropped */ | ||
| 470 | { | 484 | { |
| 471 | xstype t; | 485 | xstype t; |
| 472 | xs_dict *d = xs_dict_new(); | 486 | int r = 0; |
| 473 | int c = 0; | 487 | int c = 0; |
| 474 | 488 | ||
| 475 | for (;;) { | 489 | for (;;) { |
| 476 | xs *k = NULL; | 490 | xs *k = NULL; |
| 477 | xs *v = NULL; | 491 | xs *v = NULL; |
| 478 | int r = xs_json_load_object_iter(f, &k, &v, &t, &c); | 492 | r = xs_json_load_object_iter(f, &k, &v, &t, &c); |
| 479 | |||
| 480 | if (r == -1) | ||
| 481 | d = xs_free(d); | ||
| 482 | 493 | ||
| 483 | if (r == 1) { | 494 | if (r == 1) { |
| 484 | /* partial load? */ | 495 | /* partial load? */ |
| 485 | if (v == NULL && maxdepth != 0) { | 496 | if (v == NULL && maxdepth != 0) { |
| 486 | if (t == XSTYPE_LIST) | 497 | if (t == XSTYPE_LIST) { |
| 487 | v = xs_json_load_array(f, maxdepth - 1); | 498 | if (d) |
| 499 | v = xs_list_new(); | ||
| 500 | |||
| 501 | r = xs_json_load_array(f, maxdepth - 1, &v); | ||
| 502 | } | ||
| 488 | else | 503 | else |
| 489 | if (t == XSTYPE_DICT) | 504 | if (t == XSTYPE_DICT) { |
| 490 | v = xs_json_load_object(f, maxdepth - 1); | 505 | if (d) |
| 506 | v = xs_dict_new(); | ||
| 507 | |||
| 508 | r = xs_json_load_object(f, maxdepth - 1, &v); | ||
| 509 | } | ||
| 491 | } | 510 | } |
| 492 | 511 | ||
| 493 | /* still null? fail */ | 512 | /* error? */ |
| 494 | if (v == NULL) { | 513 | if (r < 0) |
| 495 | d = xs_free(d); | ||
| 496 | break; | 514 | break; |
| 497 | } | ||
| 498 | 515 | ||
| 499 | d = xs_dict_append(d, k, v); | 516 | if (d) |
| 517 | *d = xs_dict_append(*d, k, v); | ||
| 500 | } | 518 | } |
| 501 | else | 519 | else |
| 502 | break; | 520 | break; |
| 503 | } | 521 | } |
| 504 | 522 | ||
| 505 | return d; | 523 | if (r < 0 && d) |
| 506 | } | 524 | *d = xs_free(*d); |
| 507 | |||
| 508 | |||
| 509 | xs_val *xs_json_loads_full(const xs_str *json, int maxdepth) | ||
| 510 | /* loads a string in JSON format and converts to a multiple data */ | ||
| 511 | { | ||
| 512 | FILE *f; | ||
| 513 | xs_val *v = NULL; | ||
| 514 | |||
| 515 | if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { | ||
| 516 | v = xs_json_load_full(f, maxdepth); | ||
| 517 | fclose(f); | ||
| 518 | } | ||
| 519 | 525 | ||
| 520 | return v; | 526 | return r; |
| 521 | } | 527 | } |
| 522 | 528 | ||
| 523 | 529 | ||
| @@ -545,11 +551,30 @@ xs_val *xs_json_load_full(FILE *f, int maxdepth) | |||
| 545 | xs_val *v = NULL; | 551 | xs_val *v = NULL; |
| 546 | xstype t = xs_json_load_type(f); | 552 | xstype t = xs_json_load_type(f); |
| 547 | 553 | ||
| 548 | if (t == XSTYPE_LIST) | 554 | if (t == XSTYPE_LIST) { |
| 549 | v = xs_json_load_array(f, maxdepth); | 555 | v = xs_list_new(); |
| 556 | xs_json_load_array(f, maxdepth, &v); | ||
| 557 | } | ||
| 550 | else | 558 | else |
| 551 | if (t == XSTYPE_DICT) | 559 | if (t == XSTYPE_DICT) { |
| 552 | v = xs_json_load_object(f, maxdepth); | 560 | v = xs_dict_new(); |
| 561 | xs_json_load_object(f, maxdepth, &v); | ||
| 562 | } | ||
| 563 | |||
| 564 | return v; | ||
| 565 | } | ||
| 566 | |||
| 567 | |||
| 568 | xs_val *xs_json_loads_full(const xs_str *json, int maxdepth) | ||
| 569 | /* loads a string in JSON format and converts to a multiple data */ | ||
| 570 | { | ||
| 571 | FILE *f; | ||
| 572 | xs_val *v = NULL; | ||
| 573 | |||
| 574 | if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { | ||
| 575 | v = xs_json_load_full(f, maxdepth); | ||
| 576 | fclose(f); | ||
| 577 | } | ||
| 553 | 578 | ||
| 554 | return v; | 579 | return v; |
| 555 | } | 580 | } |
diff --git a/xs_version.h b/xs_version.h index c7789a7..466535b 100644 --- a/xs_version.h +++ b/xs_version.h | |||
| @@ -1 +1 @@ | |||
| /* 871d420cef893b6efe32869407294baf084ce3ab 2025-05-04T11:01:01+02:00 */ | /* 401d229ffbec89b4a5cf97793926b7afb84a4f26 2025-07-08T15:44:54+02:00 */ | ||
diff --git a/xs_webmention.h b/xs_webmention.h index 8415629..e177573 100644 --- a/xs_webmention.h +++ b/xs_webmention.h | |||
| @@ -5,6 +5,7 @@ | |||
| 5 | #define _XS_WEBMENTION_H | 5 | #define _XS_WEBMENTION_H |
| 6 | 6 | ||
| 7 | int xs_webmention_send(const char *source, const char *target, const char *user_agent); | 7 | int xs_webmention_send(const char *source, const char *target, const char *user_agent); |
| 8 | int xs_webmention_hook(const char *source, const char *target, const char *user_agent); | ||
| 8 | 9 | ||
| 9 | 10 | ||
| 10 | #ifdef XS_IMPLEMENTATION | 11 | #ifdef XS_IMPLEMENTATION |
| @@ -118,6 +119,47 @@ int xs_webmention_send(const char *source, const char *target, const char *user_ | |||
| 118 | } | 119 | } |
| 119 | 120 | ||
| 120 | 121 | ||
| 122 | int xs_webmention_hook(const char *source, const char *target, const char *user_agent) | ||
| 123 | /* a Webmention has been received for a target that is ours; check if the source | ||
| 124 | really contains a link to our target */ | ||
| 125 | { | ||
| 126 | int status = 0; | ||
| 127 | |||
| 128 | xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention"); | ||
| 129 | xs *headers = xs_dict_new(); | ||
| 130 | headers = xs_dict_set(headers, "accept", "text/html"); | ||
| 131 | headers = xs_dict_set(headers, "user-agent", ua); | ||
| 132 | |||
| 133 | xs *g_req = NULL; | ||
| 134 | xs *payload = NULL; | ||
| 135 | int p_size = 0; | ||
| 136 | |||
| 137 | g_req = xs_http_request("GET", source, headers, NULL, 0, &status, &payload, &p_size, 0); | ||
| 138 | |||
| 139 | if (status < 200 || status > 299) | ||
| 140 | return -1; | ||
| 141 | |||
| 142 | if (!xs_is_string(payload)) | ||
| 143 | return -2; | ||
| 144 | |||
| 145 | /* note: a "rogue" webmention can include a link to our target in commented-out HTML code */ | ||
| 146 | |||
| 147 | xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>"); | ||
| 148 | const char *link; | ||
| 149 | |||
| 150 | status = 0; | ||
| 151 | xs_list_foreach(links, link) { | ||
| 152 | /* if the link contains our target, it's valid */ | ||
| 153 | if (xs_str_in(link, target) != -1) { | ||
| 154 | status = 1; | ||
| 155 | break; | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | return status; | ||
| 160 | } | ||
| 161 | |||
| 162 | |||
| 121 | #endif /* XS_IMPLEMENTATION */ | 163 | #endif /* XS_IMPLEMENTATION */ |
| 122 | 164 | ||
| 123 | #endif /* _XS_WEBMENTION_H */ | 165 | #endif /* _XS_WEBMENTION_H */ |