diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | RELEASE_NOTES.md | 22 | ||||
| -rw-r--r-- | TODO.md | 6 | ||||
| -rw-r--r-- | activitypub.c | 82 | ||||
| -rw-r--r-- | data.c | 17 | ||||
| -rw-r--r-- | doc/snac.1 | 19 | ||||
| -rw-r--r-- | doc/snac.5 | 6 | ||||
| -rw-r--r-- | doc/snac.8 | 13 | ||||
| -rw-r--r-- | format.c | 13 | ||||
| -rw-r--r-- | html.c | 103 | ||||
| -rw-r--r-- | httpd.c | 61 | ||||
| -rw-r--r-- | main.c | 2 | ||||
| -rw-r--r-- | mastoapi.c | 20 | ||||
| -rw-r--r-- | snac.h | 7 | ||||
| -rw-r--r-- | utils.c | 55 | ||||
| -rw-r--r-- | xs_socket.h | 35 |
16 files changed, 392 insertions, 70 deletions
| @@ -100,6 +100,7 @@ This will: | |||
| 100 | - [How to install & run your own ActivityPub server on FreeBSD using snac, nginx, lets'encrypt (by gyptazy)](https://gyptazy.com/blog/install-snac2-on-freebsd-an-activitypub-instance-for-the-fediverse/). | 100 | - [How to install & run your own ActivityPub server on FreeBSD using snac, nginx, lets'encrypt (by gyptazy)](https://gyptazy.com/blog/install-snac2-on-freebsd-an-activitypub-instance-for-the-fediverse/). |
| 101 | - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). | 101 | - [How to install snac on OpenBSD without relayd (by @antics@mastodon.nu)](https://chai.guru/pub/openbsd/snac.html). |
| 102 | - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). | 102 | - [Setting up Snac in OpenBSD (by Yonle)](https://wiki.ircnow.org/index.php?n=Openbsd.Snac). |
| 103 | - [How to run your own social network with snac (by Giacomo Tesio)](https://encrypted.tesio.it/2024/12/18/how-to-run-your-own-social-network.html). Includes information on how to run snac as a CGI. | ||
| 103 | 104 | ||
| 104 | ## Incredibly awesome CSS themes for snac | 105 | ## Incredibly awesome CSS themes for snac |
| 105 | 106 | ||
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 073985b..5a1315f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md | |||
| @@ -1,5 +1,27 @@ | |||
| 1 | # Release Notes | 1 | # Release Notes |
| 2 | 2 | ||
| 3 | ## UNRELEASED | ||
| 4 | |||
| 5 | Fixed regression in link verification code (contributed by nowster). | ||
| 6 | |||
| 7 | Added ipv6 support for the https frontend connection (contributed by hb9hnt). | ||
| 8 | |||
| 9 | New "Like by URL" operation (contributed by dheadshot). | ||
| 10 | |||
| 11 | Some search fixes regarding repeated matches. | ||
| 12 | |||
| 13 | The `export_csv` cmdline operation now exports the CSV files inside a user's `export/` subdirectory instead of the current directory. | ||
| 14 | |||
| 15 | All CSV files to be imported must now be stored inside a user's `import/` subdirectory instead of the current directory. | ||
| 16 | |||
| 17 | Mastodon API: more timeline paging tunings (contributed by nowster). | ||
| 18 | |||
| 19 | The command-line operation `note` new reads the `LANG` environment variable to set the post's language. | ||
| 20 | |||
| 21 | Fixed support for `Audio` objects. | ||
| 22 | |||
| 23 | Made xmpp and mailto URLs clickable. | ||
| 24 | |||
| 3 | ## 2.67 | 25 | ## 2.67 |
| 4 | 26 | ||
| 5 | The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.). | 27 | The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.). |
| @@ -8,8 +8,6 @@ Editing / Updating a post does not index newly added hashtags. | |||
| 8 | 8 | ||
| 9 | Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042 | 9 | Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042 |
| 10 | 10 | ||
| 11 | Unfollowing lemmy groups gets rejected with an http status of 400. | ||
| 12 | |||
| 13 | Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. | 11 | Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. |
| 14 | 12 | ||
| 15 | Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 | 13 | Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 |
| @@ -363,3 +361,7 @@ Add a pidfile (2.64, 2024-11-17T10:21:29+0100). | |||
| 363 | Implement Proxying for Media Links to Enhance User Privacy (see https://codeberg.org/grunfink/snac2/issues/219 for more information) (2024-11-18T20:36:39+0100). | 361 | Implement Proxying for Media Links to Enhance User Privacy (see https://codeberg.org/grunfink/snac2/issues/219 for more information) (2024-11-18T20:36:39+0100). |
| 364 | 362 | ||
| 365 | Consider showing only posts by the account owner (not full trees) (see https://codeberg.org/grunfink/snac2/issues/217 for more information) (2024-11-18T20:36:39+0100). | 363 | Consider showing only posts by the account owner (not full trees) (see https://codeberg.org/grunfink/snac2/issues/217 for more information) (2024-11-18T20:36:39+0100). |
| 364 | |||
| 365 | Unfollowing lemmy groups gets rejected with an http status of 400 (it seems to work now; 2024-12-28T16:50:16+0100). | ||
| 366 | |||
| 367 | CSV import/export does not work with OpenBSD security on; document it or fix it (2025-01-04T19:35:09+0100). | ||
diff --git a/activitypub.c b/activitypub.c index 34cc32f..ea4d8ea 100644 --- a/activitypub.c +++ b/activitypub.c | |||
| @@ -10,6 +10,7 @@ | |||
| 10 | #include "xs_time.h" | 10 | #include "xs_time.h" |
| 11 | #include "xs_set.h" | 11 | #include "xs_set.h" |
| 12 | #include "xs_match.h" | 12 | #include "xs_match.h" |
| 13 | #include "xs_unicode.h" | ||
| 13 | 14 | ||
| 14 | #include "snac.h" | 15 | #include "snac.h" |
| 15 | 16 | ||
| @@ -178,6 +179,11 @@ const char *get_atto(const xs_dict *msg) | |||
| 178 | } | 179 | } |
| 179 | } | 180 | } |
| 180 | } | 181 | } |
| 182 | else | ||
| 183 | if (xs_type(actor) == XSTYPE_DICT) { | ||
| 184 | /* bandwagon.fm returns this */ | ||
| 185 | actor = xs_dict_get(actor, "id"); | ||
| 186 | } | ||
| 181 | 187 | ||
| 182 | return actor; | 188 | return actor; |
| 183 | } | 189 | } |
| @@ -701,6 +707,32 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg) | |||
| 701 | } | 707 | } |
| 702 | } | 708 | } |
| 703 | 709 | ||
| 710 | /* does this message contain a tag we are following? */ | ||
| 711 | const xs_list *fw_tags = xs_dict_get(snac->config, "followed_hashtags"); | ||
| 712 | if (pub_msg && xs_type(fw_tags) == XSTYPE_LIST) { | ||
| 713 | const xs_list *tags_in_msg = xs_dict_get(msg, "tag"); | ||
| 714 | if (xs_type(tags_in_msg) == XSTYPE_LIST) { | ||
| 715 | const xs_dict *te; | ||
| 716 | |||
| 717 | /* iterate the tags in the message */ | ||
| 718 | xs_list_foreach(tags_in_msg, te) { | ||
| 719 | if (xs_type(te) == XSTYPE_DICT) { | ||
| 720 | const char *type = xs_dict_get(te, "type"); | ||
| 721 | const char *name = xs_dict_get(te, "name"); | ||
| 722 | |||
| 723 | if (xs_type(type) == XSTYPE_STRING && xs_type(name) == XSTYPE_STRING) { | ||
| 724 | if (strcmp(type, "Hashtag") == 0) { | ||
| 725 | xs *lc_name = xs_utf8_to_lower(name); | ||
| 726 | |||
| 727 | if (xs_list_in(fw_tags, lc_name) != -1) | ||
| 728 | return 7; | ||
| 729 | } | ||
| 730 | } | ||
| 731 | } | ||
| 732 | } | ||
| 733 | } | ||
| 734 | } | ||
| 735 | |||
| 704 | return 0; | 736 | return 0; |
| 705 | } | 737 | } |
| 706 | 738 | ||
| @@ -1390,7 +1422,8 @@ xs_dict *msg_follow(snac *snac, const char *q) | |||
| 1390 | 1422 | ||
| 1391 | 1423 | ||
| 1392 | xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, | 1424 | xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, |
| 1393 | const xs_str *in_reply_to, const xs_list *attach, int priv) | 1425 | const xs_str *in_reply_to, const xs_list *attach, |
| 1426 | int priv, const char *lang_str) | ||
| 1394 | /* creates a 'Note' message */ | 1427 | /* creates a 'Note' message */ |
| 1395 | { | 1428 | { |
| 1396 | xs *ntid = tid(0); | 1429 | xs *ntid = tid(0); |
| @@ -1552,6 +1585,20 @@ xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, | |||
| 1552 | if (xs_list_len(atls)) | 1585 | if (xs_list_len(atls)) |
| 1553 | msg = xs_dict_append(msg, "attachment", atls); | 1586 | msg = xs_dict_append(msg, "attachment", atls); |
| 1554 | 1587 | ||
| 1588 | /* set language content map */ | ||
| 1589 | if (xs_type(lang_str) == XSTYPE_STRING) { | ||
| 1590 | /* split at the first _ */ | ||
| 1591 | xs *l0 = xs_split(lang_str, "_"); | ||
| 1592 | const char *lang = xs_list_get(l0, 0); | ||
| 1593 | |||
| 1594 | if (xs_type(lang) == XSTYPE_STRING && strlen(lang) == 2) { | ||
| 1595 | /* a valid ISO language id */ | ||
| 1596 | xs *cmap = xs_dict_new(); | ||
| 1597 | cmap = xs_dict_set(cmap, lang, xs_dict_get(msg, "content")); | ||
| 1598 | msg = xs_dict_set(msg, "contentMap", cmap); | ||
| 1599 | } | ||
| 1600 | } | ||
| 1601 | |||
| 1555 | return msg; | 1602 | return msg; |
| 1556 | } | 1603 | } |
| 1557 | 1604 | ||
| @@ -1593,7 +1640,7 @@ xs_dict *msg_question(snac *user, const char *content, xs_list *attach, | |||
| 1593 | const xs_list *opts, int multiple, int end_secs) | 1640 | const xs_list *opts, int multiple, int end_secs) |
| 1594 | /* creates a Question message */ | 1641 | /* creates a Question message */ |
| 1595 | { | 1642 | { |
| 1596 | xs_dict *msg = msg_note(user, content, NULL, NULL, attach, 0); | 1643 | xs_dict *msg = msg_note(user, content, NULL, NULL, attach, 0, NULL); |
| 1597 | int max = 8; | 1644 | int max = 8; |
| 1598 | xs_set seen; | 1645 | xs_set seen; |
| 1599 | 1646 | ||
| @@ -2386,22 +2433,28 @@ void process_user_queue_item(snac *snac, xs_dict *q_item) | |||
| 2386 | xs *rcpts = recipient_list(snac, msg, 1); | 2433 | xs *rcpts = recipient_list(snac, msg, 1); |
| 2387 | xs_set inboxes; | 2434 | xs_set inboxes; |
| 2388 | const xs_str *actor; | 2435 | const xs_str *actor; |
| 2389 | int c; | ||
| 2390 | 2436 | ||
| 2391 | xs_set_init(&inboxes); | 2437 | xs_set_init(&inboxes); |
| 2392 | 2438 | ||
| 2439 | /* add this shared inbox first */ | ||
| 2440 | xs *this_shared_inbox = xs_fmt("%s/shared-inbox", srv_baseurl); | ||
| 2441 | xs_set_add(&inboxes, this_shared_inbox); | ||
| 2442 | enqueue_output(snac, msg, this_shared_inbox, 0, 0); | ||
| 2443 | |||
| 2393 | /* iterate the recipients */ | 2444 | /* iterate the recipients */ |
| 2394 | c = 0; | 2445 | xs_list_foreach(rcpts, actor) { |
| 2395 | while (xs_list_next(rcpts, &actor, &c)) { | 2446 | /* local users were served by this_shared_inbox */ |
| 2396 | xs *inbox = get_actor_inbox(actor, 1); | 2447 | if (!xs_startswith(actor, srv_baseurl)) { |
| 2397 | 2448 | xs *inbox = get_actor_inbox(actor, 1); | |
| 2398 | if (inbox != NULL) { | 2449 | |
| 2399 | /* add to the set and, if it's not there, send message */ | 2450 | if (inbox != NULL) { |
| 2400 | if (xs_set_add(&inboxes, inbox) == 1) | 2451 | /* add to the set and, if it's not there, send message */ |
| 2401 | enqueue_output(snac, msg, inbox, 0, 0); | 2452 | if (xs_set_add(&inboxes, inbox) == 1) |
| 2453 | enqueue_output(snac, msg, inbox, 0, 0); | ||
| 2454 | } | ||
| 2455 | else | ||
| 2456 | snac_log(snac, xs_fmt("cannot find inbox for %s", actor)); | ||
| 2402 | } | 2457 | } |
| 2403 | else | ||
| 2404 | snac_log(snac, xs_fmt("cannot find inbox for %s", actor)); | ||
| 2405 | } | 2458 | } |
| 2406 | 2459 | ||
| 2407 | /* if it's a public note or question, send to the collected inboxes */ | 2460 | /* if it's a public note or question, send to the collected inboxes */ |
| @@ -2410,8 +2463,7 @@ void process_user_queue_item(snac *snac, xs_dict *q_item) | |||
| 2410 | xs *shibx = inbox_list(); | 2463 | xs *shibx = inbox_list(); |
| 2411 | const xs_str *inbox; | 2464 | const xs_str *inbox; |
| 2412 | 2465 | ||
| 2413 | c = 0; | 2466 | xs_list_foreach(shibx, inbox) { |
| 2414 | while (xs_list_next(shibx, &inbox, &c)) { | ||
| 2415 | if (xs_set_add(&inboxes, inbox) == 1) | 2467 | if (xs_set_add(&inboxes, inbox) == 1) |
| 2416 | enqueue_output(snac, msg, inbox, 0, 0); | 2468 | enqueue_output(snac, msg, inbox, 0, 0); |
| 2417 | } | 2469 | } |
| @@ -136,6 +136,18 @@ int srv_open(const char *basedir, int auto_upgrade) | |||
| 136 | srv_proxy_token_seed = xs_hex_enc(rnd, sizeof(rnd)); | 136 | srv_proxy_token_seed = xs_hex_enc(rnd, sizeof(rnd)); |
| 137 | } | 137 | } |
| 138 | 138 | ||
| 139 | /* ensure user directories include important subdirectories */ | ||
| 140 | xs *users = user_list(); | ||
| 141 | const char *uid; | ||
| 142 | |||
| 143 | xs_list_foreach(users, uid) { | ||
| 144 | xs *impdir = xs_fmt("%s/user/%s/import", srv_basedir, uid); | ||
| 145 | xs *expdir = xs_fmt("%s/user/%s/export", srv_basedir, uid); | ||
| 146 | |||
| 147 | mkdirx(impdir); | ||
| 148 | mkdirx(expdir); | ||
| 149 | } | ||
| 150 | |||
| 139 | return ret; | 151 | return ret; |
| 140 | } | 152 | } |
| 141 | 153 | ||
| @@ -2705,6 +2717,11 @@ xs_list *content_search(snac *user, const char *regex, | |||
| 2705 | if (id == NULL || is_hidden(user, id)) | 2717 | if (id == NULL || is_hidden(user, id)) |
| 2706 | continue; | 2718 | continue; |
| 2707 | 2719 | ||
| 2720 | /* recalculate the md5 id to be sure it's not repeated | ||
| 2721 | (it may have been searched by the "url" field instead of "id") */ | ||
| 2722 | xs *new_md5 = xs_md5_hex(id, strlen(id)); | ||
| 2723 | md5 = new_md5; | ||
| 2724 | |||
| 2708 | /* test for the post URL */ | 2725 | /* test for the post URL */ |
| 2709 | if (strcmp(id, regex) == 0) { | 2726 | if (strcmp(id, regex) == 0) { |
| 2710 | if (xs_set_add(&seen, md5) == 1) | 2727 | if (xs_set_add(&seen, md5) == 1) |
| @@ -270,7 +270,9 @@ user url that also contains a rel="me" attribute. These links are specially | |||
| 270 | marked as verified in the user's public timeline and also via the Mastodon API. | 270 | marked as verified in the user's public timeline and also via the Mastodon API. |
| 271 | .It Cm export_csv Ar basedir Ar uid | 271 | .It Cm export_csv Ar basedir Ar uid |
| 272 | Exports some account data as Mastodon-compatible CSV files. After executing | 272 | Exports some account data as Mastodon-compatible CSV files. After executing |
| 273 | this command, the following files will be written to the current directory: | 273 | this command, the following files will be written to the |
| 274 | .Pa export/ | ||
| 275 | subdirectory inside the user directory: | ||
| 274 | .Pa bookmarks.csv , | 276 | .Pa bookmarks.csv , |
| 275 | .Pa blocked_accounts.csv , | 277 | .Pa blocked_accounts.csv , |
| 276 | .Pa lists.csv , and | 278 | .Pa lists.csv , and |
| @@ -286,7 +288,9 @@ Starts a migration from this account to the one set as an alias (see | |||
| 286 | section 'Migrating from snac to Mastodon'). | 288 | section 'Migrating from snac to Mastodon'). |
| 287 | .It Cm import_csv Ar basedir Ar uid | 289 | .It Cm import_csv Ar basedir Ar uid |
| 288 | Imports CSV data files from a Mastodon export. This command expects the | 290 | Imports CSV data files from a Mastodon export. This command expects the |
| 289 | following files to be in the current directory: | 291 | following files to be inside the |
| 292 | .Pa import/ | ||
| 293 | subdirectory of a user's directory inside the server base directory: | ||
| 290 | .Pa bookmarks.csv , | 294 | .Pa bookmarks.csv , |
| 291 | .Pa blocked_accounts.csv , | 295 | .Pa blocked_accounts.csv , |
| 292 | .Pa lists.csv , and | 296 | .Pa lists.csv , and |
| @@ -314,10 +318,15 @@ for a job to be assigned), input or output (processing I/O packets) | |||
| 314 | or stopped (not running, only to be seen while starting or stopping | 318 | or stopped (not running, only to be seen while starting or stopping |
| 315 | the server). | 319 | the server). |
| 316 | .It Cm import_list Ar basedir Ar uid Ar file | 320 | .It Cm import_list Ar basedir Ar uid Ar file |
| 317 | Imports a Mastodon list in CSV format. This option can be used to | 321 | Imports a Mastodon list in CSV format. The file must be stored inside the |
| 318 | import "Mastodon Follow Packs". | 322 | .Pa import/ |
| 323 | subdirectory of a user's directory inside the server base directory. | ||
| 324 | This option can be used to import "Mastodon Follow Packs". | ||
| 319 | .It Cm import_block_list Ar basedir Ar uid Ar file | 325 | .It Cm import_block_list Ar basedir Ar uid Ar file |
| 320 | Imports a Mastodon list of accounts to be blocked in CSV format. | 326 | Imports a Mastodon list of accounts to be blocked in CSV format. The |
| 327 | file must be stored inside the | ||
| 328 | .Pa import/ | ||
| 329 | subdirectory of a user's directory inside the server base directory. | ||
| 321 | .El | 330 | .El |
| 322 | .Ss Migrating an account to/from Mastodon | 331 | .Ss Migrating an account to/from Mastodon |
| 323 | See | 332 | See |
| @@ -209,6 +209,12 @@ web interface. | |||
| 209 | .It Pa history/ | 209 | .It Pa history/ |
| 210 | This directory contains generated HTML files. They may be snapshots of the | 210 | This directory contains generated HTML files. They may be snapshots of the |
| 211 | local timeline in previous months or other cached data. | 211 | local timeline in previous months or other cached data. |
| 212 | .It Pa export/ | ||
| 213 | This directory will contain exported data in Mastodon-compatible CSV format | ||
| 214 | after executing the 'export_csv' command-line operation. | ||
| 215 | .It Pa import/ | ||
| 216 | Mastodon-compatible CSV files must be copied into this directory to use | ||
| 217 | any of the importing functions. | ||
| 212 | .It Pa server.pid | 218 | .It Pa server.pid |
| 213 | This file stores the server PID in a single text line. | 219 | This file stores the server PID in a single text line. |
| 214 | .El | 220 | .El |
| @@ -421,7 +421,9 @@ server, first export your data to CSV by running: | |||
| 421 | snac export_csv $SNAC_BASEDIR origin | 421 | snac export_csv $SNAC_BASEDIR origin |
| 422 | .Ed | 422 | .Ed |
| 423 | .Pp | 423 | .Pp |
| 424 | You'll find the following CSV files in the current directory: | 424 | You'll find the following CSV files in the |
| 425 | .Pa export/ | ||
| 426 | subdirectory inside the user directory: | ||
| 425 | .Pa bookmarks.csv , | 427 | .Pa bookmarks.csv , |
| 426 | .Pa blocked_accounts.csv , | 428 | .Pa blocked_accounts.csv , |
| 427 | .Pa lists.csv , and | 429 | .Pa lists.csv , and |
| @@ -480,6 +482,7 @@ Also, please take note that the | |||
| 480 | account you migrated from is not disabled nor changed in any way, so can still | 482 | account you migrated from is not disabled nor changed in any way, so can still |
| 481 | use it as it no migration was done. This behaviour may or may not match what other | 483 | use it as it no migration was done. This behaviour may or may not match what other |
| 482 | ActivityPub implementations do. | 484 | ActivityPub implementations do. |
| 485 | .Pp | ||
| 483 | .Ss Migrating from Mastodon to snac | 486 | .Ss Migrating from Mastodon to snac |
| 484 | Since version 2.61, you can migrate accounts on other ActivityPub instances to your | 487 | Since version 2.61, you can migrate accounts on other ActivityPub instances to your |
| 485 | .Nm | 488 | .Nm |
| @@ -508,7 +511,9 @@ directory: | |||
| 508 | .Pa lists.csv , and | 511 | .Pa lists.csv , and |
| 509 | .Pa following_accounts.csv . | 512 | .Pa following_accounts.csv . |
| 510 | .Pp | 513 | .Pp |
| 511 | 2. From the directory where those files are stored, run | 514 | 2. Copy all those files to the |
| 515 | .Pa import/ | ||
| 516 | subdirectory of the user's directory inside the server base directory, and run | ||
| 512 | .Bd -literal -offset indent | 517 | .Bd -literal -offset indent |
| 513 | snac import_csv $SNAC_BASEDIR destination | 518 | snac import_csv $SNAC_BASEDIR destination |
| 514 | .Ed | 519 | .Ed |
| @@ -518,7 +523,9 @@ of all the ActivityPub servers involved (webfinger, accounts, posts, etc.). Some | |||
| 518 | may be transient and retried later. Also, if | 523 | may be transient and retried later. Also, if |
| 519 | .Nm | 524 | .Nm |
| 520 | complains that it can't find any of these files, please check that they are really | 525 | complains that it can't find any of these files, please check that they are really |
| 521 | stored in the current directory and that their names match exactly. Some of them may be | 526 | stored in the |
| 527 | .Pa import/ | ||
| 528 | subdirectory and that their names match exactly. Some of them may be | ||
| 522 | empty (for example, if you didn't create any list) and that's fine. | 529 | empty (for example, if you didn't create any list) and that's fine. |
| 523 | .Pp | 530 | .Pp |
| 524 | 3. Again on your | 531 | 3. Again on your |
| @@ -7,6 +7,7 @@ | |||
| 7 | #include "xs_html.h" | 7 | #include "xs_html.h" |
| 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 | 11 | ||
| 11 | #include "snac.h" | 12 | #include "snac.h" |
| 12 | 13 | ||
| @@ -93,7 +94,8 @@ static xs_str *format_line(const char *line, xs_list **attach) | |||
| 93 | "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|" | 94 | "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|" |
| 94 | "!\\[[^]]+\\]\\([^\\)]+\\)" "|" | 95 | "!\\[[^]]+\\]\\([^\\)]+\\)" "|" |
| 95 | "\\[[^]]+\\]\\([^\\)]+\\)" "|" | 96 | "\\[[^]]+\\]\\([^\\)]+\\)" "|" |
| 96 | "[a-z]+:/" "/[^[:space:]]+" | 97 | "[a-z]+:/" "/[^[:space:]]+" "|" |
| 98 | "(mailto|xmpp):[^@[:space:]]+@[^[:space:]]+" | ||
| 97 | ")"); | 99 | ")"); |
| 98 | int n = 0; | 100 | int n = 0; |
| 99 | 101 | ||
| @@ -230,6 +232,15 @@ static xs_str *format_line(const char *line, xs_list **attach) | |||
| 230 | } | 232 | } |
| 231 | } | 233 | } |
| 232 | else | 234 | else |
| 235 | if (xs_match(v, "mailto*|xmpp*")) { | ||
| 236 | xs *u = xs_replace_i(xs_replace(v, "#", "#"), "@", "@"); | ||
| 237 | |||
| 238 | xs *v2 = xs_strip_chars_i(xs_dup(u), ".,)"); | ||
| 239 | |||
| 240 | xs *s1 = xs_fmt("<a href=\"%s\" target=\"_blank\">%s</a>", v2, u); | ||
| 241 | s = xs_str_cat(s, s1); | ||
| 242 | } | ||
| 243 | else | ||
| 233 | s = xs_str_cat(s, v); | 244 | s = xs_str_cat(s, v); |
| 234 | } | 245 | } |
| 235 | else | 246 | else |
| @@ -934,7 +934,7 @@ static xs_html *html_user_body(snac *user, int read_only) | |||
| 934 | } | 934 | } |
| 935 | } | 935 | } |
| 936 | else | 936 | else |
| 937 | if (xs_startswith(v, "gemini:/")) { | 937 | if (xs_startswith(v, "gemini:/") || xs_startswith(v, "xmpp:")) { |
| 938 | value = xs_html_tag("a", | 938 | value = xs_html_tag("a", |
| 939 | xs_html_attr("rel", "me"), | 939 | xs_html_attr("rel", "me"), |
| 940 | xs_html_attr("href", v), | 940 | xs_html_attr("href", v), |
| @@ -1034,6 +1034,23 @@ xs_html *html_top_controls(snac *snac) | |||
| 1034 | xs_html_attr("value", L("Boost"))), | 1034 | xs_html_attr("value", L("Boost"))), |
| 1035 | xs_html_text(" "), | 1035 | xs_html_text(" "), |
| 1036 | xs_html_text(L("(by URL)"))), | 1036 | xs_html_text(L("(by URL)"))), |
| 1037 | xs_html_tag("p", NULL), | ||
| 1038 | xs_html_tag("form", | ||
| 1039 | xs_html_attr("autocomplete", "off"), | ||
| 1040 | xs_html_attr("method", "post"), | ||
| 1041 | xs_html_attr("action", ops_action), | ||
| 1042 | xs_html_sctag("input", | ||
| 1043 | xs_html_attr("type", "text"), | ||
| 1044 | xs_html_attr("name", "id"), | ||
| 1045 | xs_html_attr("required", "required"), | ||
| 1046 | xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")), | ||
| 1047 | xs_html_text(" "), | ||
| 1048 | xs_html_sctag("input", | ||
| 1049 | xs_html_attr("type", "submit"), | ||
| 1050 | xs_html_attr("name", "action"), | ||
| 1051 | xs_html_attr("value", L("Like"))), | ||
| 1052 | xs_html_text(" "), | ||
| 1053 | xs_html_text(L("(by URL)"))), | ||
| 1037 | xs_html_tag("p", NULL))); | 1054 | xs_html_tag("p", NULL))); |
| 1038 | 1055 | ||
| 1039 | /** user settings **/ | 1056 | /** user settings **/ |
| @@ -2917,6 +2934,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 2917 | int proxy = 0; | 2934 | int proxy = 0; |
| 2918 | const char *v; | 2935 | const char *v; |
| 2919 | 2936 | ||
| 2937 | const xs_dict *q_vars = xs_dict_get(req, "q_vars"); | ||
| 2938 | |||
| 2920 | xs *l = xs_split_n(q_path, "/", 2); | 2939 | xs *l = xs_split_n(q_path, "/", 2); |
| 2921 | v = xs_list_get(l, 1); | 2940 | v = xs_list_get(l, 1); |
| 2922 | 2941 | ||
| @@ -2925,6 +2944,23 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 2925 | return HTTP_STATUS_NOT_FOUND; | 2944 | return HTTP_STATUS_NOT_FOUND; |
| 2926 | } | 2945 | } |
| 2927 | 2946 | ||
| 2947 | if (strcmp(v, "share-bridge") == 0) { | ||
| 2948 | /* temporary redirect for a post */ | ||
| 2949 | const char *login = xs_dict_get(q_vars, "login"); | ||
| 2950 | const char *content = xs_dict_get(q_vars, "content"); | ||
| 2951 | |||
| 2952 | if (xs_type(login) == XSTYPE_STRING && xs_type(content) == XSTYPE_STRING) { | ||
| 2953 | xs *b64 = xs_base64_enc(content, strlen(content)); | ||
| 2954 | |||
| 2955 | srv_log(xs_fmt("share-bridge for user '%s'", login)); | ||
| 2956 | |||
| 2957 | *body = xs_fmt("%s/%s/share?content=%s", srv_baseurl, login, b64); | ||
| 2958 | return HTTP_STATUS_SEE_OTHER; | ||
| 2959 | } | ||
| 2960 | else | ||
| 2961 | return HTTP_STATUS_NOT_FOUND; | ||
| 2962 | } | ||
| 2963 | |||
| 2928 | uid = xs_dup(v); | 2964 | uid = xs_dup(v); |
| 2929 | 2965 | ||
| 2930 | /* rss extension? */ | 2966 | /* rss extension? */ |
| @@ -2959,7 +2995,6 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 2959 | int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); | 2995 | int def_show = xs_number_get(xs_dict_get(srv_config, "max_timeline_entries")); |
| 2960 | int show = def_show; | 2996 | int show = def_show; |
| 2961 | 2997 | ||
| 2962 | const xs_dict *q_vars = xs_dict_get(req, "q_vars"); | ||
| 2963 | if ((v = xs_dict_get(q_vars, "skip")) != NULL) | 2998 | if ((v = xs_dict_get(q_vars, "skip")) != NULL) |
| 2964 | skip = atoi(v), cache = 0, save = 0; | 2999 | skip = atoi(v), cache = 0, save = 0; |
| 2965 | if ((v = xs_dict_get(q_vars, "show")) != NULL) | 3000 | if ((v = xs_dict_get(q_vars, "show")) != NULL) |
| @@ -3045,32 +3080,30 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3045 | q = url_acct; | 3080 | q = url_acct; |
| 3046 | } | 3081 | } |
| 3047 | else { | 3082 | else { |
| 3048 | /* if it's not already here, try to bring it to the user's timeline */ | 3083 | /* bring it to the user's timeline */ |
| 3049 | xs *md5 = xs_md5_hex(q, strlen(q)); | 3084 | xs *object = NULL; |
| 3050 | 3085 | int status; | |
| 3051 | if (!timeline_here(&snac, md5)) { | ||
| 3052 | xs *object = NULL; | ||
| 3053 | int status; | ||
| 3054 | 3086 | ||
| 3055 | status = activitypub_request(&snac, q, &object); | 3087 | status = activitypub_request(&snac, q, &object); |
| 3056 | snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status)); | 3088 | snac_debug(&snac, 1, xs_fmt("Request searched URL %s %d", q, status)); |
| 3057 | 3089 | ||
| 3058 | if (valid_status(status)) { | 3090 | if (valid_status(status)) { |
| 3059 | /* got it; also request the actor */ | 3091 | /* got it; also request the actor */ |
| 3060 | const char *attr_to = get_atto(object); | 3092 | const char *attr_to = get_atto(object); |
| 3061 | 3093 | ||
| 3062 | if (!xs_is_null(attr_to)) { | 3094 | if (!xs_is_null(attr_to)) { |
| 3063 | status = actor_request(&snac, attr_to, &actor_obj); | 3095 | status = actor_request(&snac, attr_to, &actor_obj); |
| 3064 | 3096 | ||
| 3065 | snac_debug(&snac, 1, xs_fmt("Request author %s of %s %d", attr_to, q, status)); | 3097 | if (valid_status(status)) { |
| 3098 | /* reset the query string to be the real id */ | ||
| 3099 | url_acct = xs_dup(xs_dict_get(object, "id")); | ||
| 3100 | q = url_acct; | ||
| 3066 | 3101 | ||
| 3067 | if (valid_status(status)) { | 3102 | /* add the post to the timeline */ |
| 3068 | /* add the actor */ | 3103 | xs *md5 = xs_md5_hex(q, strlen(q)); |
| 3069 | actor_add(attr_to, actor_obj); | ||
| 3070 | 3104 | ||
| 3071 | /* add the post to the timeline */ | 3105 | if (!timeline_here(&snac, md5)) |
| 3072 | timeline_add(&snac, q, object); | 3106 | timeline_add(&snac, q, object); |
| 3073 | } | ||
| 3074 | } | 3107 | } |
| 3075 | } | 3108 | } |
| 3076 | } | 3109 | } |
| @@ -3473,6 +3506,30 @@ int html_get_handler(const xs_dict *req, const char *q_path, | |||
| 3473 | } | 3506 | } |
| 3474 | } | 3507 | } |
| 3475 | else | 3508 | else |
| 3509 | if (strcmp(p_path, "share") == 0) { /** direct post **/ | ||
| 3510 | if (!login(&snac, req)) { | ||
| 3511 | *body = xs_dup(uid); | ||
| 3512 | status = HTTP_STATUS_UNAUTHORIZED; | ||
| 3513 | } | ||
| 3514 | else { | ||
| 3515 | const char *b64 = xs_dict_get(q_vars, "content"); | ||
| 3516 | int sz; | ||
| 3517 | xs *content = xs_base64_dec(b64, &sz); | ||
| 3518 | xs *msg = msg_note(&snac, content, NULL, NULL, NULL, 0, NULL); | ||
| 3519 | xs *c_msg = msg_create(&snac, msg); | ||
| 3520 | |||
| 3521 | timeline_add(&snac, xs_dict_get(msg, "id"), msg); | ||
| 3522 | |||
| 3523 | enqueue_message(&snac, c_msg); | ||
| 3524 | |||
| 3525 | snac_debug(&snac, 1, xs_fmt("web action 'share' received")); | ||
| 3526 | |||
| 3527 | *body = xs_fmt("%s/admin", snac.actor); | ||
| 3528 | *b_size = strlen(*body); | ||
| 3529 | status = HTTP_STATUS_SEE_OTHER; | ||
| 3530 | } | ||
| 3531 | } | ||
| 3532 | else | ||
| 3476 | status = HTTP_STATUS_NOT_FOUND; | 3533 | status = HTTP_STATUS_NOT_FOUND; |
| 3477 | 3534 | ||
| 3478 | user_free(&snac); | 3535 | user_free(&snac); |
| @@ -3604,7 +3661,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 3604 | enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs); | 3661 | enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs); |
| 3605 | } | 3662 | } |
| 3606 | else | 3663 | else |
| 3607 | msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv); | 3664 | msg = msg_note(&snac, content_2, to, in_reply_to, attach_list, priv, NULL); |
| 3608 | 3665 | ||
| 3609 | if (sensitive != NULL) { | 3666 | if (sensitive != NULL) { |
| 3610 | msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); | 3667 | msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); |
| @@ -4038,7 +4095,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, | |||
| 4038 | int c = 0; | 4095 | int c = 0; |
| 4039 | 4096 | ||
| 4040 | while (xs_list_next(ls, &v, &c)) { | 4097 | while (xs_list_next(ls, &v, &c)) { |
| 4041 | xs *msg = msg_note(&snac, "", actor, irt, NULL, 1); | 4098 | xs *msg = msg_note(&snac, "", actor, irt, NULL, 1, NULL); |
| 4042 | 4099 | ||
| 4043 | /* set the option */ | 4100 | /* set the option */ |
| 4044 | msg = xs_dict_append(msg, "name", v); | 4101 | msg = xs_dict_append(msg, "name", v); |
| @@ -164,6 +164,24 @@ static xs_str *greeting_html(void) | |||
| 164 | } | 164 | } |
| 165 | 165 | ||
| 166 | 166 | ||
| 167 | const char *share_page = "" | ||
| 168 | "<!DOCTYPE html>\n" | ||
| 169 | "<html>\n" | ||
| 170 | "<head>\n" | ||
| 171 | "<title>%s - snac</title>\n" | ||
| 172 | "<meta content=\"width=device-width, initial-scale=1, minimum-scale=1, user-scalable=no\" name=\"viewport\">\n" | ||
| 173 | "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s/style.css\"/>\n" | ||
| 174 | "<style>:root {color-scheme: light dark}</style>\n" | ||
| 175 | "</head>\n" | ||
| 176 | "<body><h1>%s link share</h1>\n" | ||
| 177 | "<form method=\"get\" action=\"%s/share-bridge\">\n" | ||
| 178 | "<textarea name=\"content\" rows=\"6\" wrap=\"virtual\" required=\"required\" style=\"width: 50em\">%s</textarea>\n" | ||
| 179 | "<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\" required=\"required\"></p>\n" | ||
| 180 | "<input type=\"submit\" value=\"OK\">\n" | ||
| 181 | "</form><p>%s</p></body></html>\n" | ||
| 182 | ""; | ||
| 183 | |||
| 184 | |||
| 167 | int server_get_handler(xs_dict *req, const char *q_path, | 185 | int server_get_handler(xs_dict *req, const char *q_path, |
| 168 | char **body, int *b_size, char **ctype) | 186 | char **body, int *b_size, char **ctype) |
| 169 | /* basic server services */ | 187 | /* basic server services */ |
| @@ -257,6 +275,49 @@ int server_get_handler(xs_dict *req, const char *q_path, | |||
| 257 | *body = xs_str_new("User-agent: *\n" | 275 | *body = xs_str_new("User-agent: *\n" |
| 258 | "Disallow: /\n"); | 276 | "Disallow: /\n"); |
| 259 | } | 277 | } |
| 278 | else | ||
| 279 | if (strcmp(q_path, "/style.css") == 0) { | ||
| 280 | FILE *f; | ||
| 281 | xs *css_fn = xs_fmt("%s/style.css", srv_basedir); | ||
| 282 | |||
| 283 | if ((f = fopen(css_fn, "r")) != NULL) { | ||
| 284 | *body = xs_readall(f); | ||
| 285 | fclose(f); | ||
| 286 | |||
| 287 | status = HTTP_STATUS_OK; | ||
| 288 | *ctype = "text/css"; | ||
| 289 | } | ||
| 290 | } | ||
| 291 | else | ||
| 292 | if (strcmp(q_path, "/share") == 0) { | ||
| 293 | const xs_dict *q_vars = xs_dict_get(req, "q_vars"); | ||
| 294 | const char *url = xs_dict_get(q_vars, "url"); | ||
| 295 | const char *text = xs_dict_get(q_vars, "text"); | ||
| 296 | xs *s = NULL; | ||
| 297 | |||
| 298 | if (xs_type(text) == XSTYPE_STRING) { | ||
| 299 | if (xs_type(url) == XSTYPE_STRING) | ||
| 300 | s = xs_fmt("%s:\n\n%s\n", text, url); | ||
| 301 | else | ||
| 302 | s = xs_fmt("%s\n", text); | ||
| 303 | } | ||
| 304 | else | ||
| 305 | if (xs_type(url) == XSTYPE_STRING) | ||
| 306 | s = xs_fmt("%s\n", url); | ||
| 307 | else | ||
| 308 | s = xs_str_new(NULL); | ||
| 309 | |||
| 310 | status = HTTP_STATUS_OK; | ||
| 311 | *ctype = "text/html"; | ||
| 312 | *body = xs_fmt(share_page, | ||
| 313 | xs_dict_get(srv_config, "host"), | ||
| 314 | srv_baseurl, | ||
| 315 | xs_dict_get(srv_config, "host"), | ||
| 316 | srv_baseurl, | ||
| 317 | s, | ||
| 318 | USER_AGENT | ||
| 319 | ); | ||
| 320 | } | ||
| 260 | 321 | ||
| 261 | if (status != 0) | 322 | if (status != 0) |
| 262 | srv_debug(1, xs_fmt("server_get_handler serving '%s' %d", q_path, status)); | 323 | srv_debug(1, xs_fmt("server_get_handler serving '%s' %d", q_path, status)); |
| @@ -667,7 +667,7 @@ int main(int argc, char *argv[]) | |||
| 667 | else | 667 | else |
| 668 | content = xs_dup(url); | 668 | content = xs_dup(url); |
| 669 | 669 | ||
| 670 | msg = msg_note(&snac, content, NULL, NULL, attl, 0); | 670 | msg = msg_note(&snac, content, NULL, NULL, attl, 0, getenv("LANG")); |
| 671 | 671 | ||
| 672 | c_msg = msg_create(&snac, msg); | 672 | c_msg = msg_create(&snac, msg); |
| 673 | 673 | ||
| @@ -1348,7 +1348,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1348 | if (limit == 0) | 1348 | if (limit == 0) |
| 1349 | limit = 20; | 1349 | limit = 20; |
| 1350 | 1350 | ||
| 1351 | if (min_id == NULL && index_desc_first(f, md5, 0)) { | 1351 | if (index_desc_first(f, md5, 0)) { |
| 1352 | do { | 1352 | do { |
| 1353 | xs *msg = NULL; | 1353 | xs *msg = NULL; |
| 1354 | 1354 | ||
| @@ -1366,6 +1366,11 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1366 | break; | 1366 | break; |
| 1367 | } | 1367 | } |
| 1368 | 1368 | ||
| 1369 | if (min_id) { | ||
| 1370 | if (strcmp(md5, MID_TO_MD5(min_id)) == 0) | ||
| 1371 | break; | ||
| 1372 | } | ||
| 1373 | |||
| 1369 | /* get the entry */ | 1374 | /* get the entry */ |
| 1370 | if (user) { | 1375 | if (user) { |
| 1371 | if (!valid_status(timeline_get_by_md5(user, md5, &msg))) | 1376 | if (!valid_status(timeline_get_by_md5(user, md5, &msg))) |
| @@ -1435,8 +1440,14 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn | |||
| 1435 | out = xs_list_append(out, st); | 1440 | out = xs_list_append(out, st); |
| 1436 | cnt++; | 1441 | cnt++; |
| 1437 | } | 1442 | } |
| 1443 | if (min_id) { | ||
| 1444 | while (cnt > limit) { | ||
| 1445 | out = xs_list_del(out, 0); | ||
| 1446 | cnt--; | ||
| 1447 | } | ||
| 1448 | } | ||
| 1438 | 1449 | ||
| 1439 | } while (cnt < limit && index_desc_next(f, md5)); | 1450 | } while ((min_id || (cnt < limit)) && index_desc_next(f, md5)); |
| 1440 | } | 1451 | } |
| 1441 | 1452 | ||
| 1442 | int more = index_desc_next(f, md5); | 1453 | int more = index_desc_next(f, md5); |
| @@ -2589,6 +2600,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2589 | const char *visibility = xs_dict_get(args, "visibility"); | 2600 | const char *visibility = xs_dict_get(args, "visibility"); |
| 2590 | const char *summary = xs_dict_get(args, "spoiler_text"); | 2601 | const char *summary = xs_dict_get(args, "spoiler_text"); |
| 2591 | const char *media_ids = xs_dict_get(args, "media_ids"); | 2602 | const char *media_ids = xs_dict_get(args, "media_ids"); |
| 2603 | const char *language = xs_dict_get(args, "language"); | ||
| 2592 | 2604 | ||
| 2593 | if (xs_is_null(media_ids)) | 2605 | if (xs_is_null(media_ids)) |
| 2594 | media_ids = xs_dict_get(args, "media_ids[]"); | 2606 | media_ids = xs_dict_get(args, "media_ids[]"); |
| @@ -2639,7 +2651,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2639 | 2651 | ||
| 2640 | /* prepare the message */ | 2652 | /* prepare the message */ |
| 2641 | xs *msg = msg_note(&snac, content, NULL, irt, attach_list, | 2653 | xs *msg = msg_note(&snac, content, NULL, irt, attach_list, |
| 2642 | strcmp(visibility, "public") == 0 ? 0 : 1); | 2654 | strcmp(visibility, "public") == 0 ? 0 : 1, language); |
| 2643 | 2655 | ||
| 2644 | if (!xs_is_null(summary) && *summary) { | 2656 | if (!xs_is_null(summary) && *summary) { |
| 2645 | msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); | 2657 | msg = xs_dict_set(msg, "sensitive", xs_stock(XSTYPE_TRUE)); |
| @@ -2989,7 +3001,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, | |||
| 2989 | if (o) { | 3001 | if (o) { |
| 2990 | const char *name = xs_dict_get(o, "name"); | 3002 | const char *name = xs_dict_get(o, "name"); |
| 2991 | 3003 | ||
| 2992 | xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1); | 3004 | xs *msg = msg_note(&snac, "", atto, (char *)id, NULL, 1, NULL); |
| 2993 | msg = xs_dict_append(msg, "name", name); | 3005 | msg = xs_dict_append(msg, "name", name); |
| 2994 | 3006 | ||
| 2995 | xs *c_msg = msg_create(&snac, msg); | 3007 | xs *c_msg = msg_create(&snac, msg); |
| @@ -1,7 +1,7 @@ | |||
| 1 | /* snac - A simple, minimalistic ActivityPub instance */ | 1 | /* snac - A simple, minimalistic ActivityPub instance */ |
| 2 | /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ | 2 | /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ |
| 3 | 3 | ||
| 4 | #define VERSION "2.67" | 4 | #define VERSION "2.68-dev" |
| 5 | 5 | ||
| 6 | #define USER_AGENT "snac/" VERSION | 6 | #define USER_AGENT "snac/" VERSION |
| 7 | 7 | ||
| @@ -32,7 +32,7 @@ extern int dbglevel; | |||
| 32 | 32 | ||
| 33 | #define L(s) (s) | 33 | #define L(s) (s) |
| 34 | 34 | ||
| 35 | #define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Event" | 35 | #define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Audio|Event" |
| 36 | 36 | ||
| 37 | int mkdirx(const char *pathname); | 37 | int mkdirx(const char *pathname); |
| 38 | 38 | ||
| @@ -316,7 +316,8 @@ xs_dict *msg_create(snac *snac, const xs_dict *object); | |||
| 316 | xs_dict *msg_follow(snac *snac, const char *actor); | 316 | xs_dict *msg_follow(snac *snac, const char *actor); |
| 317 | 317 | ||
| 318 | xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, | 318 | xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, |
| 319 | const xs_str *in_reply_to, const xs_list *attach, int priv); | 319 | const xs_str *in_reply_to, const xs_list *attach, |
| 320 | int priv, const char *lang); | ||
| 320 | 321 | ||
| 321 | xs_dict *msg_undo(snac *snac, const xs_val *object); | 322 | xs_dict *msg_undo(snac *snac, const xs_val *object); |
| 322 | xs_dict *msg_delete(snac *snac, const char *id); | 323 | xs_dict *msg_delete(snac *snac, const char *id); |
| @@ -446,7 +446,8 @@ int deluser(snac *user) | |||
| 446 | void verify_links(snac *user) | 446 | void verify_links(snac *user) |
| 447 | /* verifies a user's links */ | 447 | /* verifies a user's links */ |
| 448 | { | 448 | { |
| 449 | const xs_dict *p = xs_dict_get(user->config, "metadata"); | 449 | xs *metadata = NULL; |
| 450 | const xs_dict *md = xs_dict_get(user->config, "metadata"); | ||
| 450 | const char *k, *v; | 451 | const char *k, *v; |
| 451 | int changed = 0; | 452 | int changed = 0; |
| 452 | 453 | ||
| @@ -454,8 +455,30 @@ void verify_links(snac *user) | |||
| 454 | headers = xs_dict_append(headers, "accept", "text/html"); | 455 | headers = xs_dict_append(headers, "accept", "text/html"); |
| 455 | headers = xs_dict_append(headers, "user-agent", USER_AGENT " (link verify)"); | 456 | headers = xs_dict_append(headers, "user-agent", USER_AGENT " (link verify)"); |
| 456 | 457 | ||
| 458 | if (xs_type(md) == XSTYPE_DICT) | ||
| 459 | metadata = xs_dup(md); | ||
| 460 | else | ||
| 461 | if (xs_type(md) == XSTYPE_STRING) { | ||
| 462 | /* convert to dict for easier iteration */ | ||
| 463 | metadata = xs_dict_new(); | ||
| 464 | xs *l = xs_split(md, "\n"); | ||
| 465 | const char *ll; | ||
| 466 | |||
| 467 | xs_list_foreach(l, ll) { | ||
| 468 | xs *kv = xs_split_n(ll, "=", 1); | ||
| 469 | const char *k = xs_list_get(kv, 0); | ||
| 470 | const char *v = xs_list_get(kv, 1); | ||
| 471 | |||
| 472 | if (k && v) { | ||
| 473 | xs *kk = xs_strip_i(xs_dup(k)); | ||
| 474 | xs *vv = xs_strip_i(xs_dup(v)); | ||
| 475 | metadata = xs_dict_set(metadata, kk, vv); | ||
| 476 | } | ||
| 477 | } | ||
| 478 | } | ||
| 479 | |||
| 457 | int c = 0; | 480 | int c = 0; |
| 458 | while (p && xs_dict_next(p, &k, &v, &c)) { | 481 | while (metadata && xs_dict_next(metadata, &k, &v, &c)) { |
| 459 | /* not an https link? skip */ | 482 | /* not an https link? skip */ |
| 460 | if (!xs_startswith(v, "https:/" "/")) | 483 | if (!xs_startswith(v, "https:/" "/")) |
| 461 | continue; | 484 | continue; |
| @@ -571,9 +594,9 @@ void export_csv(snac *user) | |||
| 571 | /* exports user data to current directory in a way that pleases Mastodon */ | 594 | /* exports user data to current directory in a way that pleases Mastodon */ |
| 572 | { | 595 | { |
| 573 | FILE *f; | 596 | FILE *f; |
| 574 | const char *fn; | 597 | xs *fn = NULL; |
| 575 | 598 | ||
| 576 | fn = "bookmarks.csv"; | 599 | fn = xs_fmt("%s/export/bookmarks.csv", user->basedir); |
| 577 | if ((f = fopen(fn, "w")) != NULL) { | 600 | if ((f = fopen(fn, "w")) != NULL) { |
| 578 | snac_log(user, xs_fmt("Creating %s...", fn)); | 601 | snac_log(user, xs_fmt("Creating %s...", fn)); |
| 579 | 602 | ||
| @@ -596,7 +619,8 @@ void export_csv(snac *user) | |||
| 596 | else | 619 | else |
| 597 | snac_log(user, xs_fmt("Cannot create file %s", fn)); | 620 | snac_log(user, xs_fmt("Cannot create file %s", fn)); |
| 598 | 621 | ||
| 599 | fn = "blocked_accounts.csv"; | 622 | xs_free(fn); |
| 623 | fn = xs_fmt("%s/export/blocked_accounts.csv", user->basedir); | ||
| 600 | if ((f = fopen(fn, "w")) != NULL) { | 624 | if ((f = fopen(fn, "w")) != NULL) { |
| 601 | snac_log(user, xs_fmt("Creating %s...", fn)); | 625 | snac_log(user, xs_fmt("Creating %s...", fn)); |
| 602 | 626 | ||
| @@ -615,7 +639,8 @@ void export_csv(snac *user) | |||
| 615 | else | 639 | else |
| 616 | snac_log(user, xs_fmt("Cannot create file %s", fn)); | 640 | snac_log(user, xs_fmt("Cannot create file %s", fn)); |
| 617 | 641 | ||
| 618 | fn = "lists.csv"; | 642 | xs_free(fn); |
| 643 | fn = xs_fmt("%s/export/lists.csv", user->basedir); | ||
| 619 | if ((f = fopen(fn, "w")) != NULL) { | 644 | if ((f = fopen(fn, "w")) != NULL) { |
| 620 | snac_log(user, xs_fmt("Creating %s...", fn)); | 645 | snac_log(user, xs_fmt("Creating %s...", fn)); |
| 621 | 646 | ||
| @@ -647,7 +672,8 @@ void export_csv(snac *user) | |||
| 647 | else | 672 | else |
| 648 | snac_log(user, xs_fmt("Cannot create file %s", fn)); | 673 | snac_log(user, xs_fmt("Cannot create file %s", fn)); |
| 649 | 674 | ||
| 650 | fn = "following_accounts.csv"; | 675 | xs_free(fn); |
| 676 | fn = xs_fmt("%s/export/following_accounts.csv", user->basedir); | ||
| 651 | if ((f = fopen(fn, "w")) != NULL) { | 677 | if ((f = fopen(fn, "w")) != NULL) { |
| 652 | snac_log(user, xs_fmt("Creating %s...", fn)); | 678 | snac_log(user, xs_fmt("Creating %s...", fn)); |
| 653 | 679 | ||
| @@ -670,10 +696,12 @@ void export_csv(snac *user) | |||
| 670 | } | 696 | } |
| 671 | 697 | ||
| 672 | 698 | ||
| 673 | void import_blocked_accounts_csv(snac *user, const char *fn) | 699 | void import_blocked_accounts_csv(snac *user, const char *ifn) |
| 674 | /* imports a Mastodon CSV file of blocked accounts */ | 700 | /* imports a Mastodon CSV file of blocked accounts */ |
| 675 | { | 701 | { |
| 676 | FILE *f; | 702 | FILE *f; |
| 703 | xs *l = xs_split(ifn, "/"); | ||
| 704 | xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1)); | ||
| 677 | 705 | ||
| 678 | if ((f = fopen(fn, "r")) != NULL) { | 706 | if ((f = fopen(fn, "r")) != NULL) { |
| 679 | snac_log(user, xs_fmt("Importing from %s...", fn)); | 707 | snac_log(user, xs_fmt("Importing from %s...", fn)); |
| @@ -705,10 +733,12 @@ void import_blocked_accounts_csv(snac *user, const char *fn) | |||
| 705 | } | 733 | } |
| 706 | 734 | ||
| 707 | 735 | ||
| 708 | void import_following_accounts_csv(snac *user, const char *fn) | 736 | void import_following_accounts_csv(snac *user, const char *ifn) |
| 709 | /* imports a Mastodon CSV file of accounts to follow */ | 737 | /* imports a Mastodon CSV file of accounts to follow */ |
| 710 | { | 738 | { |
| 711 | FILE *f; | 739 | FILE *f; |
| 740 | xs *l = xs_split(ifn, "/"); | ||
| 741 | xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1)); | ||
| 712 | 742 | ||
| 713 | if ((f = fopen(fn, "r")) != NULL) { | 743 | if ((f = fopen(fn, "r")) != NULL) { |
| 714 | snac_log(user, xs_fmt("Importing from %s...", fn)); | 744 | snac_log(user, xs_fmt("Importing from %s...", fn)); |
| @@ -764,10 +794,12 @@ void import_following_accounts_csv(snac *user, const char *fn) | |||
| 764 | } | 794 | } |
| 765 | 795 | ||
| 766 | 796 | ||
| 767 | void import_list_csv(snac *user, const char *fn) | 797 | void import_list_csv(snac *user, const char *ifn) |
| 768 | /* imports a Mastodon CSV file list */ | 798 | /* imports a Mastodon CSV file list */ |
| 769 | { | 799 | { |
| 770 | FILE *f; | 800 | FILE *f; |
| 801 | xs *l = xs_split(ifn, "/"); | ||
| 802 | xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1)); | ||
| 771 | 803 | ||
| 772 | if ((f = fopen(fn, "r")) != NULL) { | 804 | if ((f = fopen(fn, "r")) != NULL) { |
| 773 | snac_log(user, xs_fmt("Importing from %s...", fn)); | 805 | snac_log(user, xs_fmt("Importing from %s...", fn)); |
| @@ -825,7 +857,6 @@ void import_csv(snac *user) | |||
| 825 | /* import CSV files from Mastodon */ | 857 | /* import CSV files from Mastodon */ |
| 826 | { | 858 | { |
| 827 | FILE *f; | 859 | FILE *f; |
| 828 | const char *fn; | ||
| 829 | 860 | ||
| 830 | import_blocked_accounts_csv(user, "blocked_accounts.csv"); | 861 | import_blocked_accounts_csv(user, "blocked_accounts.csv"); |
| 831 | 862 | ||
| @@ -833,7 +864,7 @@ void import_csv(snac *user) | |||
| 833 | 864 | ||
| 834 | import_list_csv(user, "lists.csv"); | 865 | import_list_csv(user, "lists.csv"); |
| 835 | 866 | ||
| 836 | fn = "bookmarks.csv"; | 867 | xs *fn = xs_fmt("%s/import/bookmarks.csv", user->basedir); |
| 837 | if ((f = fopen(fn, "r")) != NULL) { | 868 | if ((f = fopen(fn, "r")) != NULL) { |
| 838 | snac_log(user, xs_fmt("Importing from %s...", fn)); | 869 | snac_log(user, xs_fmt("Importing from %s...", fn)); |
| 839 | 870 | ||
diff --git a/xs_socket.h b/xs_socket.h index 1bd053a..fb67b9d 100644 --- a/xs_socket.h +++ b/xs_socket.h | |||
| @@ -54,7 +54,39 @@ int xs_socket_server(const char *addr, const char *serv) | |||
| 54 | /* opens a server socket by service name (or port as string) */ | 54 | /* opens a server socket by service name (or port as string) */ |
| 55 | { | 55 | { |
| 56 | int rs = -1; | 56 | int rs = -1; |
| 57 | struct sockaddr_in host; | 57 | #ifndef WITHOUT_GETADDRINFO |
| 58 | struct addrinfo *res; | ||
| 59 | struct addrinfo hints; | ||
| 60 | int status; | ||
| 61 | |||
| 62 | memset(&hints, '\0', sizeof(hints)); | ||
| 63 | |||
| 64 | hints.ai_family = AF_UNSPEC; | ||
| 65 | hints.ai_socktype = SOCK_STREAM; | ||
| 66 | |||
| 67 | if (getaddrinfo(addr, serv, &hints, &res) != 0) { | ||
| 68 | goto end; | ||
| 69 | } | ||
| 70 | |||
| 71 | rs = socket(res->ai_family, res->ai_socktype, res->ai_protocol); | ||
| 72 | |||
| 73 | /* reuse addr */ | ||
| 74 | int i = 1; | ||
| 75 | setsockopt(rs, SOL_SOCKET, SO_REUSEADDR, (char *)&i, sizeof(i)); | ||
| 76 | |||
| 77 | status = bind(rs, res->ai_addr, res->ai_addrlen); | ||
| 78 | if (status == -1) { | ||
| 79 | fprintf(stderr, "Error binding with status %d\n", status); | ||
| 80 | close(rs); | ||
| 81 | rs = -1; | ||
| 82 | } | ||
| 83 | else { | ||
| 84 | |||
| 85 | listen(rs, SOMAXCONN); | ||
| 86 | } | ||
| 87 | |||
| 88 | #else /* WITHOUT_GETADDRINFO */ | ||
| 89 | struct sockaddr_in host; | ||
| 58 | 90 | ||
| 59 | memset(&host, '\0', sizeof(host)); | 91 | memset(&host, '\0', sizeof(host)); |
| 60 | 92 | ||
| @@ -89,6 +121,7 @@ int xs_socket_server(const char *addr, const char *serv) | |||
| 89 | listen(rs, SOMAXCONN); | 121 | listen(rs, SOMAXCONN); |
| 90 | } | 122 | } |
| 91 | 123 | ||
| 124 | #endif /* WITHOUT_GETADDRINFO */ | ||
| 92 | end: | 125 | end: |
| 93 | return rs; | 126 | return rs; |
| 94 | } | 127 | } |