summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--RELEASE_NOTES.md22
-rw-r--r--TODO.md6
-rw-r--r--activitypub.c82
-rw-r--r--data.c17
-rw-r--r--doc/snac.119
-rw-r--r--doc/snac.56
-rw-r--r--doc/snac.813
-rw-r--r--format.c13
-rw-r--r--html.c103
-rw-r--r--httpd.c61
-rw-r--r--main.c2
-rw-r--r--mastoapi.c20
-rw-r--r--snac.h7
-rw-r--r--utils.c55
-rw-r--r--xs_socket.h35
16 files changed, 392 insertions, 70 deletions
diff --git a/README.md b/README.md
index 2d484f8..26d9fa1 100644
--- a/README.md
+++ b/README.md
@@ -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
5Fixed regression in link verification code (contributed by nowster).
6
7Added ipv6 support for the https frontend connection (contributed by hb9hnt).
8
9New "Like by URL" operation (contributed by dheadshot).
10
11Some search fixes regarding repeated matches.
12
13The `export_csv` cmdline operation now exports the CSV files inside a user's `export/` subdirectory instead of the current directory.
14
15All CSV files to be imported must now be stored inside a user's `import/` subdirectory instead of the current directory.
16
17Mastodon API: more timeline paging tunings (contributed by nowster).
18
19The command-line operation `note` new reads the `LANG` environment variable to set the post's language.
20
21Fixed support for `Audio` objects.
22
23Made xmpp and mailto URLs clickable.
24
3## 2.67 25## 2.67
4 26
5The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.). 27The search box also accepts post URLs; the post is requested and, if it's found, can be interacted with (liked, boosted, replied to, etc.).
diff --git a/TODO.md b/TODO.md
index 0f2cbd0..ff32e73 100644
--- a/TODO.md
+++ b/TODO.md
@@ -8,8 +8,6 @@ Editing / Updating a post does not index newly added hashtags.
8 8
9Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042 9Wrong level of message visibility when using the Mastodon API: https://codeberg.org/grunfink/snac2/issues/200#issuecomment-2351042
10 10
11Unfollowing lemmy groups gets rejected with an http status of 400.
12
13Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't. 11Unfollowing guppe groups seems to work (http status of 200), but messages continue to arrive as if it didn't.
14 12
15Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 13Important: 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).
363Implement 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). 361Implement 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
365Consider 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). 363Consider 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
365Unfollowing lemmy groups gets rejected with an http status of 400 (it seems to work now; 2024-12-28T16:50:16+0100).
366
367CSV 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
1392xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, 1424xs_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 }
diff --git a/data.c b/data.c
index eb4c9d5..0fd3528 100644
--- a/data.c
+++ b/data.c
@@ -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)
diff --git a/doc/snac.1 b/doc/snac.1
index efba67e..999c594 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -270,7 +270,9 @@ user url that also contains a rel="me" attribute. These links are specially
270marked as verified in the user's public timeline and also via the Mastodon API. 270marked 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
272Exports some account data as Mastodon-compatible CSV files. After executing 272Exports some account data as Mastodon-compatible CSV files. After executing
273this command, the following files will be written to the current directory: 273this command, the following files will be written to the
274.Pa export/
275subdirectory 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
286section 'Migrating from snac to Mastodon'). 288section 'Migrating from snac to Mastodon').
287.It Cm import_csv Ar basedir Ar uid 289.It Cm import_csv Ar basedir Ar uid
288Imports CSV data files from a Mastodon export. This command expects the 290Imports CSV data files from a Mastodon export. This command expects the
289following files to be in the current directory: 291following files to be inside the
292.Pa import/
293subdirectory 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)
314or stopped (not running, only to be seen while starting or stopping 318or stopped (not running, only to be seen while starting or stopping
315the server). 319the server).
316.It Cm import_list Ar basedir Ar uid Ar file 320.It Cm import_list Ar basedir Ar uid Ar file
317Imports a Mastodon list in CSV format. This option can be used to 321Imports a Mastodon list in CSV format. The file must be stored inside the
318import "Mastodon Follow Packs". 322.Pa import/
323subdirectory of a user's directory inside the server base directory.
324This 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
320Imports a Mastodon list of accounts to be blocked in CSV format. 326Imports a Mastodon list of accounts to be blocked in CSV format. The
327file must be stored inside the
328.Pa import/
329subdirectory 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
323See 332See
diff --git a/doc/snac.5 b/doc/snac.5
index 0168430..be7bfd7 100644
--- a/doc/snac.5
+++ b/doc/snac.5
@@ -209,6 +209,12 @@ web interface.
209.It Pa history/ 209.It Pa history/
210This directory contains generated HTML files. They may be snapshots of the 210This directory contains generated HTML files. They may be snapshots of the
211local timeline in previous months or other cached data. 211local timeline in previous months or other cached data.
212.It Pa export/
213This directory will contain exported data in Mastodon-compatible CSV format
214after executing the 'export_csv' command-line operation.
215.It Pa import/
216Mastodon-compatible CSV files must be copied into this directory to use
217any of the importing functions.
212.It Pa server.pid 218.It Pa server.pid
213This file stores the server PID in a single text line. 219This file stores the server PID in a single text line.
214.El 220.El
diff --git a/doc/snac.8 b/doc/snac.8
index f5e4bd5..f94d53f 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -421,7 +421,9 @@ server, first export your data to CSV by running:
421snac export_csv $SNAC_BASEDIR origin 421snac export_csv $SNAC_BASEDIR origin
422.Ed 422.Ed
423.Pp 423.Pp
424You'll find the following CSV files in the current directory: 424You'll find the following CSV files in the
425.Pa export/
426subdirectory 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
480account you migrated from is not disabled nor changed in any way, so can still 482account you migrated from is not disabled nor changed in any way, so can still
481use it as it no migration was done. This behaviour may or may not match what other 483use it as it no migration was done. This behaviour may or may not match what other
482ActivityPub implementations do. 484ActivityPub implementations do.
485.Pp
483.Ss Migrating from Mastodon to snac 486.Ss Migrating from Mastodon to snac
484Since version 2.61, you can migrate accounts on other ActivityPub instances to your 487Since 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
5112. From the directory where those files are stored, run 5142. Copy all those files to the
515.Pa import/
516subdirectory of the user's directory inside the server base directory, and run
512.Bd -literal -offset indent 517.Bd -literal -offset indent
513snac import_csv $SNAC_BASEDIR destination 518snac import_csv $SNAC_BASEDIR destination
514.Ed 519.Ed
@@ -518,7 +523,9 @@ of all the ActivityPub servers involved (webfinger, accounts, posts, etc.). Some
518may be transient and retried later. Also, if 523may be transient and retried later. Also, if
519.Nm 524.Nm
520complains that it can't find any of these files, please check that they are really 525complains that it can't find any of these files, please check that they are really
521stored in the current directory and that their names match exactly. Some of them may be 526stored in the
527.Pa import/
528subdirectory and that their names match exactly. Some of them may be
522empty (for example, if you didn't create any list) and that's fine. 529empty (for example, if you didn't create any list) and that's fine.
523.Pp 530.Pp
5243. Again on your 5313. Again on your
diff --git a/format.c b/format.c
index 12783ae..41e4162 100644
--- a/format.c
+++ b/format.c
@@ -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
diff --git a/html.c b/html.c
index 2c0a823..cd8279e 100644
--- a/html.c
+++ b/html.c
@@ -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);
diff --git a/httpd.c b/httpd.c
index 11e4d17..804ff1b 100644
--- a/httpd.c
+++ b/httpd.c
@@ -164,6 +164,24 @@ static xs_str *greeting_html(void)
164} 164}
165 165
166 166
167const 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
167int server_get_handler(xs_dict *req, const char *q_path, 185int 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));
diff --git a/main.c b/main.c
index 76a7961..7a86fbd 100644
--- a/main.c
+++ b/main.c
@@ -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
diff --git a/mastoapi.c b/mastoapi.c
index 09e18a1..62108ad 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -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);
diff --git a/snac.h b/snac.h
index 96916d1..ec3ee5c 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 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
37int mkdirx(const char *pathname); 37int mkdirx(const char *pathname);
38 38
@@ -316,7 +316,8 @@ xs_dict *msg_create(snac *snac, const xs_dict *object);
316xs_dict *msg_follow(snac *snac, const char *actor); 316xs_dict *msg_follow(snac *snac, const char *actor);
317 317
318xs_dict *msg_note(snac *snac, const xs_str *content, const xs_val *rcpts, 318xs_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
321xs_dict *msg_undo(snac *snac, const xs_val *object); 322xs_dict *msg_undo(snac *snac, const xs_val *object);
322xs_dict *msg_delete(snac *snac, const char *id); 323xs_dict *msg_delete(snac *snac, const char *id);
diff --git a/utils.c b/utils.c
index df3b55d..0740d4d 100644
--- a/utils.c
+++ b/utils.c
@@ -446,7 +446,8 @@ int deluser(snac *user)
446void verify_links(snac *user) 446void 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
673void import_blocked_accounts_csv(snac *user, const char *fn) 699void 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
708void import_following_accounts_csv(snac *user, const char *fn) 736void 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
767void import_list_csv(snac *user, const char *fn) 797void 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 */
92end: 125end:
93 return rs; 126 return rs;
94} 127}