summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--RELEASE_NOTES.md16
-rw-r--r--activitypub.c101
-rw-r--r--data.c119
-rw-r--r--doc/snac.136
-rw-r--r--html.c230
-rw-r--r--main.c16
-rw-r--r--mastoapi.c25
-rw-r--r--snac.h13
-rw-r--r--utils.c53
-rw-r--r--xs_url.h8
-rw-r--r--xs_version.h2
11 files changed, 543 insertions, 76 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6ec959c..2218c20 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,21 @@
1# Release Notes 1# Release Notes
2 2
3## UNRELEASED
4
5As many users have asked for it, there is now an option to make the number of followed and following accounts public (still disabled by default). These are only the numbers; the lists themselves are never published.
6
7Some fixes to blocked instances code (posts from them were sometimes shown).
8
9## 2.65
10
11Added a new user option to disable automatic follow confirmations (follow requests must be manually approved from the people page).
12
13The search box also searches for accounts (via webfinger).
14
15New command-line action `import_list`, to import a Mastodon list in CSV format (so that [Mastodon Follow Packs](https://mastodonmigration.wordpress.com/?p=995) can be directly used).
16
17New command-line action `import_block_list`, to import a Mastodon list of accounts to be blocked in CSV format.
18
3## 2.64 19## 2.64
4 20
5Some tweaks for better integration with https://bsky.brid.gy (the BlueSky bridge by brid.gy). 21Some tweaks for better integration with https://bsky.brid.gy (the BlueSky bridge by brid.gy).
diff --git a/activitypub.c b/activitypub.c
index 473675d..773df78 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -1038,15 +1038,14 @@ xs_dict *msg_base(snac *snac, const char *type, const char *id,
1038} 1038}
1039 1039
1040 1040
1041xs_dict *msg_collection(snac *snac, const char *id) 1041xs_dict *msg_collection(snac *snac, const char *id, int items)
1042/* creates an empty OrderedCollection message */ 1042/* creates an empty OrderedCollection message */
1043{ 1043{
1044 xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL); 1044 xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL);
1045 xs *ol = xs_list_new(); 1045 xs *n = xs_number_new(items);
1046 1046
1047 msg = xs_dict_append(msg, "attributedTo", snac->actor); 1047 msg = xs_dict_append(msg, "attributedTo", snac->actor);
1048 msg = xs_dict_append(msg, "orderedItems", ol); 1048 msg = xs_dict_append(msg, "totalItems", n);
1049 msg = xs_dict_append(msg, "totalItems", xs_stock(0));
1050 1049
1051 return msg; 1050 return msg;
1052} 1051}
@@ -1218,7 +1217,30 @@ xs_dict *msg_actor(snac *snac)
1218 } 1217 }
1219 1218
1220 /* add the metadata as attachments of PropertyValue */ 1219 /* add the metadata as attachments of PropertyValue */
1221 const xs_dict *metadata = xs_dict_get(snac->config, "metadata"); 1220 xs *metadata = NULL;
1221 const xs_dict *md = xs_dict_get(snac->config, "metadata");
1222
1223 if (xs_type(md) == XSTYPE_DICT)
1224 metadata = xs_dup(md);
1225 else
1226 if (xs_type(md) == XSTYPE_STRING) {
1227 metadata = xs_dict_new();
1228 xs *l = xs_split(md, "\n");
1229 const char *ll;
1230
1231 xs_list_foreach(l, ll) {
1232 xs *kv = xs_split_n(ll, "=", 1);
1233 const char *k = xs_list_get(kv, 0);
1234 const char *v = xs_list_get(kv, 1);
1235
1236 if (k && v) {
1237 xs *kk = xs_strip_i(xs_dup(k));
1238 xs *vv = xs_strip_i(xs_dup(v));
1239 metadata = xs_dict_set(metadata, kk, vv);
1240 }
1241 }
1242 }
1243
1222 if (xs_type(metadata) == XSTYPE_DICT) { 1244 if (xs_type(metadata) == XSTYPE_DICT) {
1223 xs *attach = xs_list_new(); 1245 xs *attach = xs_list_new();
1224 const xs_str *k; 1246 const xs_str *k;
@@ -1264,6 +1286,10 @@ xs_dict *msg_actor(snac *snac)
1264 msg = xs_dict_set(msg, "alsoKnownAs", loaka); 1286 msg = xs_dict_set(msg, "alsoKnownAs", loaka);
1265 } 1287 }
1266 1288
1289 const xs_val *manually = xs_dict_get(snac->config, "approve_followers");
1290 msg = xs_dict_set(msg, "manuallyApprovesFollowers",
1291 xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE));
1292
1267 return msg; 1293 return msg;
1268} 1294}
1269 1295
@@ -1900,22 +1926,31 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
1900 object_add(actor, actor_obj); 1926 object_add(actor, actor_obj);
1901 } 1927 }
1902 1928
1903 xs *f_msg = xs_dup(msg); 1929 if (xs_is_true(xs_dict_get(snac->config, "approve_followers"))) {
1904 xs *reply = msg_accept(snac, f_msg, actor); 1930 pending_add(snac, actor, msg);
1905 1931
1906 post_message(snac, actor, reply); 1932 snac_log(snac, xs_fmt("new pending follower approval %s", actor));
1907
1908 if (xs_is_null(xs_dict_get(f_msg, "published"))) {
1909 /* add a date if it doesn't include one (Mastodon) */
1910 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
1911 f_msg = xs_dict_set(f_msg, "published", date);
1912 } 1933 }
1934 else {
1935 /* automatic following */
1936 xs *f_msg = xs_dup(msg);
1937 xs *reply = msg_accept(snac, f_msg, actor);
1938
1939 post_message(snac, actor, reply);
1940
1941 if (xs_is_null(xs_dict_get(f_msg, "published"))) {
1942 /* add a date if it doesn't include one (Mastodon) */
1943 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
1944 f_msg = xs_dict_set(f_msg, "published", date);
1945 }
1946
1947 timeline_add(snac, id, f_msg);
1913 1948
1914 timeline_add(snac, id, f_msg); 1949 follower_add(snac, actor);
1915 1950
1916 follower_add(snac, actor); 1951 snac_log(snac, xs_fmt("new follower %s", actor));
1952 }
1917 1953
1918 snac_log(snac, xs_fmt("new follower %s", actor));
1919 do_notify = 1; 1954 do_notify = 1;
1920 } 1955 }
1921 else 1956 else
@@ -1937,6 +1972,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
1937 do_notify = 1; 1972 do_notify = 1;
1938 } 1973 }
1939 else 1974 else
1975 if (pending_check(snac, actor)) {
1976 pending_del(snac, actor);
1977 snac_log(snac, xs_fmt("cancelled pending follow from %s", actor));
1978 }
1979 else
1940 snac_log(snac, xs_fmt("error deleting follower %s", actor)); 1980 snac_log(snac, xs_fmt("error deleting follower %s", actor));
1941 } 1981 }
1942 } 1982 }
@@ -2796,6 +2836,8 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
2796 2836
2797 *ctype = "application/activity+json"; 2837 *ctype = "application/activity+json";
2798 2838
2839 int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"));
2840
2799 if (p_path == NULL) { 2841 if (p_path == NULL) {
2800 /* if there was no component after the user, it's an actor request */ 2842 /* if there was no component after the user, it's an actor request */
2801 msg = msg_actor(&snac); 2843 msg = msg_actor(&snac);
@@ -2809,7 +2851,6 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
2809 if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) { 2851 if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) {
2810 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 2852 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
2811 xs *list = xs_list_new(); 2853 xs *list = xs_list_new();
2812 msg = msg_collection(&snac, id);
2813 const char *v; 2854 const char *v;
2814 int tc = 0; 2855 int tc = 0;
2815 2856
@@ -2831,14 +2872,32 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
2831 } 2872 }
2832 2873
2833 /* replace the 'orderedItems' with the latest posts */ 2874 /* replace the 'orderedItems' with the latest posts */
2834 xs *items = xs_number_new(xs_list_len(list)); 2875 msg = msg_collection(&snac, id, xs_list_len(list));
2835 msg = xs_dict_set(msg, "orderedItems", list); 2876 msg = xs_dict_set(msg, "orderedItems", list);
2836 msg = xs_dict_set(msg, "totalItems", items);
2837 } 2877 }
2838 else 2878 else
2839 if (strcmp(p_path, "followers") == 0 || strcmp(p_path, "following") == 0) { 2879 if (strcmp(p_path, "followers") == 0) {
2880 int total = 0;
2881
2882 if (show_contact_metrics) {
2883 xs *l = follower_list(&snac);
2884 total = xs_list_len(l);
2885 }
2886
2887 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
2888 msg = msg_collection(&snac, id, total);
2889 }
2890 else
2891 if (strcmp(p_path, "following") == 0) {
2892 int total = 0;
2893
2894 if (show_contact_metrics) {
2895 xs *l = following_list(&snac);
2896 total = xs_list_len(l);
2897 }
2898
2840 xs *id = xs_fmt("%s/%s", snac.actor, p_path); 2899 xs *id = xs_fmt("%s/%s", snac.actor, p_path);
2841 msg = msg_collection(&snac, id); 2900 msg = msg_collection(&snac, id, total);
2842 } 2901 }
2843 else 2902 else
2844 if (xs_startswith(p_path, "p/")) { 2903 if (xs_startswith(p_path, "p/")) {
diff --git a/data.c b/data.c
index 6f93098..8a3fe2d 100644
--- a/data.c
+++ b/data.c
@@ -299,6 +299,35 @@ int user_persist(snac *snac, int publish)
299 xs *bfn = xs_fmt("%s.bak", fn); 299 xs *bfn = xs_fmt("%s.bak", fn);
300 FILE *f; 300 FILE *f;
301 301
302 if (publish) {
303 /* check if any of the relevant fields have really changed */
304 if ((f = fopen(fn, "r")) != NULL) {
305 xs *old = xs_json_load(f);
306 fclose(f);
307
308 if (old != NULL) {
309 int nw = 0;
310 const char *fields[] = { "header", "avatar", "name", "bio", "metadata", NULL };
311
312 for (int n = 0; fields[n]; n++) {
313 const char *of = xs_dict_get(old, fields[n]);
314 const char *nf = xs_dict_get(snac->config, fields[n]);
315
316 if (of == NULL && nf == NULL)
317 continue;
318
319 if (xs_type(of) != XSTYPE_STRING || xs_type(nf) != XSTYPE_STRING || strcmp(of, nf)) {
320 nw = 1;
321 break;
322 }
323 }
324
325 if (!nw)
326 publish = 0;
327 }
328 }
329 }
330
302 rename(fn, bfn); 331 rename(fn, bfn);
303 332
304 if ((f = fopen(fn, "w")) != NULL) { 333 if ((f = fopen(fn, "w")) != NULL) {
@@ -1139,6 +1168,96 @@ xs_list *follower_list(snac *snac)
1139} 1168}
1140 1169
1141 1170
1171/** pending followers **/
1172
1173int pending_add(snac *user, const char *actor, const xs_dict *msg)
1174/* stores the follow message for later confirmation */
1175{
1176 xs *dir = xs_fmt("%s/pending", user->basedir);
1177 xs *md5 = xs_md5_hex(actor, strlen(actor));
1178 xs *fn = xs_fmt("%s/%s.json", dir, md5);
1179 FILE *f;
1180
1181 mkdirx(dir);
1182
1183 if ((f = fopen(fn, "w")) == NULL)
1184 return -1;
1185
1186 xs_json_dump(msg, 4, f);
1187 fclose(f);
1188
1189 return 0;
1190}
1191
1192
1193int pending_check(snac *user, const char *actor)
1194/* checks if there is a pending follow confirmation for the actor */
1195{
1196 xs *md5 = xs_md5_hex(actor, strlen(actor));
1197 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1198
1199 return mtime(fn) != 0;
1200}
1201
1202
1203xs_dict *pending_get(snac *user, const char *actor)
1204/* returns the pending follow confirmation for the actor */
1205{
1206 xs *md5 = xs_md5_hex(actor, strlen(actor));
1207 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1208 xs_dict *msg = NULL;
1209 FILE *f;
1210
1211 if ((f = fopen(fn, "r")) != NULL) {
1212 msg = xs_json_load(f);
1213 fclose(f);
1214 }
1215
1216 return msg;
1217}
1218
1219
1220void pending_del(snac *user, const char *actor)
1221/* deletes a pending follow confirmation for the actor */
1222{
1223 xs *md5 = xs_md5_hex(actor, strlen(actor));
1224 xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
1225
1226 unlink(fn);
1227}
1228
1229
1230xs_list *pending_list(snac *user)
1231/* returns a list of pending follow confirmations */
1232{
1233 xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
1234 xs *l = xs_glob(spec, 0, 0);
1235 xs_list *r = xs_list_new();
1236 const char *v;
1237
1238 xs_list_foreach(l, v) {
1239 FILE *f;
1240 xs *msg = NULL;
1241
1242 if ((f = fopen(v, "r")) == NULL)
1243 continue;
1244
1245 msg = xs_json_load(f);
1246 fclose(f);
1247
1248 if (msg == NULL)
1249 continue;
1250
1251 const char *actor = xs_dict_get(msg, "actor");
1252
1253 if (xs_type(actor) == XSTYPE_STRING)
1254 r = xs_list_append(r, actor);
1255 }
1256
1257 return r;
1258}
1259
1260
1142/** timeline **/ 1261/** timeline **/
1143 1262
1144double timeline_mtime(snac *snac) 1263double timeline_mtime(snac *snac)
diff --git a/doc/snac.1 b/doc/snac.1
index 4c40ac9..efba67e 100644
--- a/doc/snac.1
+++ b/doc/snac.1
@@ -129,6 +129,28 @@ Just what it says in the tin. This is to mitigate spammers
129coming from Fediverse instances with lax / open registration 129coming from Fediverse instances with lax / open registration
130processes. Please take note that this also avoids possibly 130processes. Please take note that this also avoids possibly
131legitimate people trying to contact you. 131legitimate people trying to contact you.
132.It This account is a bot
133Set this checkbox if this account behaves like a bot (i.e.
134posts are automatically generated).
135.It Auto-boost all mentions to this account
136If this toggle is set, all mentions to this account are boosted
137to all followers. This can be used to create groups.
138.It This account is private
139If this toggle is set, posts are not published via the public
140web interface, only via the ActivityPub protocol.
141.It Collapse top threads by default
142If this toggle is set, the private timeline will always show
143conversations collapsed by default. This allows easier navigation
144through long threads.
145.It Follow requests must be approved
146If this toggle is set, follow requests are not automatically
147accepted, but notified and stored for later review. Pending
148follow requests will be shown in the people page to be
149approved or discarded.
150.It Publish follower and following metrics
151If this toggle is set, the number of followers and following
152accounts are made public (this is only the number; the specific
153lists of accounts are never published).
132.It Password 154.It Password
133Write the same string in these two fields to change your 155Write the same string in these two fields to change your
134password. Don't write anything if you don't want to do this. 156password. Don't write anything if you don't want to do this.
@@ -262,6 +284,13 @@ section 'Migrating from snac to Mastodon').
262Starts a migration from this account to the one set as an alias (see 284Starts a migration from this account to the one set as an alias (see
263.Xr snac 8 , 285.Xr snac 8 ,
264section 'Migrating from snac to Mastodon'). 286section 'Migrating from snac to Mastodon').
287.It Cm import_csv Ar basedir Ar uid
288Imports CSV data files from a Mastodon export. This command expects the
289following files to be in the current directory:
290.Pa bookmarks.csv ,
291.Pa blocked_accounts.csv ,
292.Pa lists.csv , and
293.Pa following_accounts.csv .
265.It Cm state Ar basedir 294.It Cm state Ar basedir
266Dumps the current state of the server and its threads. For example: 295Dumps the current state of the server and its threads. For example:
267.Bd -literal -offset indent 296.Bd -literal -offset indent
@@ -284,6 +313,11 @@ in-memory job queue. The thread state can be: waiting (idle waiting
284for a job to be assigned), input or output (processing I/O packets) 313for a job to be assigned), input or output (processing I/O packets)
285or stopped (not running, only to be seen while starting or stopping 314or stopped (not running, only to be seen while starting or stopping
286the server). 315the server).
316.It Cm import_list Ar basedir Ar uid Ar file
317Imports a Mastodon list in CSV format. This option can be used to
318import "Mastodon Follow Packs".
319.It Cm import_block_list Ar basedir Ar uid Ar file
320Imports a Mastodon list of accounts to be blocked in CSV format.
287.El 321.El
288.Ss Migrating an account to/from Mastodon 322.Ss Migrating an account to/from Mastodon
289See 323See
@@ -349,4 +383,4 @@ See the LICENSE file for details.
349.Sh CAVEATS 383.Sh CAVEATS
350Use the Fediverse sparingly. Don't fear the MUTE button. 384Use the Fediverse sparingly. Don't fear the MUTE button.
351.Sh BUGS 385.Sh BUGS
352Probably plenty. Some issues may be even documented in the TODO.md file. 386Probably many. Some issues may be even documented in the TODO.md file.
diff --git a/html.c b/html.c
index 74de47d..edb7e1e 100644
--- a/html.c
+++ b/html.c
@@ -770,7 +770,7 @@ static xs_html *html_user_body(snac *user, int read_only)
770 xs_html_sctag("input", 770 xs_html_sctag("input",
771 xs_html_attr("type", "text"), 771 xs_html_attr("type", "text"),
772 xs_html_attr("name", "q"), 772 xs_html_attr("name", "q"),
773 xs_html_attr("title", L("Search posts by content (regular expression) or #tag")), 773 xs_html_attr("title", L("Search posts by content (regular expression), @user@host accounts, or #tag")),
774 xs_html_attr("placeholder", L("Content search"))))); 774 xs_html_attr("placeholder", L("Content search")))));
775 } 775 }
776 776
@@ -829,21 +829,45 @@ static xs_html *html_user_body(snac *user, int read_only)
829 } 829 }
830 830
831 if (read_only) { 831 if (read_only) {
832 xs *es1 = encode_html(xs_dict_get(user->config, "bio"));
833 xs *tags = xs_list_new(); 832 xs *tags = xs_list_new();
834 xs *bio1 = not_really_markdown(es1, NULL, &tags); 833 xs *bio1 = not_really_markdown(xs_dict_get(user->config, "bio"), NULL, &tags);
835 xs *bio2 = process_tags(user, bio1, &tags); 834 xs *bio2 = process_tags(user, bio1, &tags);
835 xs *bio3 = sanitize(bio2);
836 836
837 bio2 = replace_shortnames(bio2, tags, 2, proxy); 837 bio3 = replace_shortnames(bio3, tags, 2, proxy);
838 838
839 xs_html *top_user_bio = xs_html_tag("div", 839 xs_html *top_user_bio = xs_html_tag("div",
840 xs_html_attr("class", "p-note snac-top-user-bio"), 840 xs_html_attr("class", "p-note snac-top-user-bio"),
841 xs_html_raw(bio2)); /* already sanitized */ 841 xs_html_raw(bio3)); /* already sanitized */
842 842
843 xs_html_add(top_user, 843 xs_html_add(top_user,
844 top_user_bio); 844 top_user_bio);
845 845
846 const xs_dict *metadata = xs_dict_get(user->config, "metadata"); 846 xs *metadata = NULL;
847 const xs_dict *md = xs_dict_get(user->config, "metadata");
848
849 if (xs_type(md) == XSTYPE_DICT)
850 metadata = xs_dup(md);
851 else
852 if (xs_type(md) == XSTYPE_STRING) {
853 /* convert to dict for easier iteration */
854 metadata = xs_dict_new();
855 xs *l = xs_split(md, "\n");
856 const char *ll;
857
858 xs_list_foreach(l, ll) {
859 xs *kv = xs_split_n(ll, "=", 1);
860 const char *k = xs_list_get(kv, 0);
861 const char *v = xs_list_get(kv, 1);
862
863 if (k && v) {
864 xs *kk = xs_strip_i(xs_dup(k));
865 xs *vv = xs_strip_i(xs_dup(v));
866 metadata = xs_dict_set(metadata, kk, vv);
867 }
868 }
869 }
870
847 if (xs_type(metadata) == XSTYPE_DICT) { 871 if (xs_type(metadata) == XSTYPE_DICT) {
848 const xs_str *k; 872 const xs_str *k;
849 const xs_str *v; 873 const xs_str *v;
@@ -914,6 +938,18 @@ static xs_html *html_user_body(snac *user, int read_only)
914 xs_html_add(top_user, 938 xs_html_add(top_user,
915 snac_metadata); 939 snac_metadata);
916 } 940 }
941
942 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
943 xs *fwers = follower_list(user);
944 xs *fwing = following_list(user);
945
946 xs *s1 = xs_fmt(L("%d following %d followers"),
947 xs_list_len(fwing), xs_list_len(fwers));
948
949 xs_html_add(top_user,
950 xs_html_tag("p",
951 xs_html_text(s1)));
952 }
917 } 953 }
918 954
919 xs_html_add(body, 955 xs_html_add(body,
@@ -1025,20 +1061,31 @@ xs_html *html_top_controls(snac *snac)
1025 const xs_val *a_private = xs_dict_get(snac->config, "private"); 1061 const xs_val *a_private = xs_dict_get(snac->config, "private");
1026 const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost"); 1062 const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
1027 const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads"); 1063 const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
1064 const xs_val *pending = xs_dict_get(snac->config, "approve_followers");
1065 const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics");
1028 1066
1029 xs *metadata = xs_str_new(NULL); 1067 xs *metadata = NULL;
1030 const xs_dict *md = xs_dict_get(snac->config, "metadata"); 1068 const xs_dict *md = xs_dict_get(snac->config, "metadata");
1031 const xs_str *k;
1032 const xs_str *v;
1033 1069
1034 int c = 0; 1070 if (xs_type(md) == XSTYPE_DICT) {
1035 while (xs_dict_next(md, &k, &v, &c)) { 1071 const xs_str *k;
1036 xs *kp = xs_fmt("%s=%s", k, v); 1072 const xs_str *v;
1037 1073
1038 if (*metadata) 1074 metadata = xs_str_new(NULL);
1039 metadata = xs_str_cat(metadata, "\n"); 1075
1040 metadata = xs_str_cat(metadata, kp); 1076 xs_dict_foreach(md, k, v) {
1077 xs *kp = xs_fmt("%s=%s", k, v);
1078
1079 if (*metadata)
1080 metadata = xs_str_cat(metadata, "\n");
1081 metadata = xs_str_cat(metadata, kp);
1082 }
1041 } 1083 }
1084 else
1085 if (xs_type(md) == XSTYPE_STRING)
1086 metadata = xs_dup(md);
1087 else
1088 metadata = xs_str_new(NULL);
1042 1089
1043 xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor); 1090 xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
1044 1091
@@ -1188,6 +1235,24 @@ xs_html *html_top_controls(snac *snac)
1188 xs_html_attr("for", "collapse_threads"), 1235 xs_html_attr("for", "collapse_threads"),
1189 xs_html_text(L("Collapse top threads by default")))), 1236 xs_html_text(L("Collapse top threads by default")))),
1190 xs_html_tag("p", 1237 xs_html_tag("p",
1238 xs_html_sctag("input",
1239 xs_html_attr("type", "checkbox"),
1240 xs_html_attr("name", "approve_followers"),
1241 xs_html_attr("id", "approve_followers"),
1242 xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
1243 xs_html_tag("label",
1244 xs_html_attr("for", "approve_followers"),
1245 xs_html_text(L("Follow requests must be approved")))),
1246 xs_html_tag("p",
1247 xs_html_sctag("input",
1248 xs_html_attr("type", "checkbox"),
1249 xs_html_attr("name", "show_contact_metrics"),
1250 xs_html_attr("id", "show_contact_metrics"),
1251 xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
1252 xs_html_tag("label",
1253 xs_html_attr("for", "show_contact_metrics"),
1254 xs_html_text(L("Publish follower and following metrics")))),
1255 xs_html_tag("p",
1191 xs_html_text(L("Profile metadata (key=value pairs in each line):")), 1256 xs_html_text(L("Profile metadata (key=value pairs in each line):")),
1192 xs_html_sctag("br", NULL), 1257 xs_html_sctag("br", NULL),
1193 xs_html_tag("textarea", 1258 xs_html_tag("textarea",
@@ -1481,6 +1546,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1481 if ((read_only || !user) && !is_msg_public(msg)) 1546 if ((read_only || !user) && !is_msg_public(msg))
1482 return NULL; 1547 return NULL;
1483 1548
1549 if (id && is_instance_blocked(id))
1550 return NULL;
1551
1484 if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads"))) 1552 if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
1485 collapse_threads = 1; 1553 collapse_threads = 1;
1486 1554
@@ -2437,10 +2505,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2437 xs_html_tag("summary", 2505 xs_html_tag("summary",
2438 xs_html_text("...")))); 2506 xs_html_text("..."))));
2439 2507
2440 xs_list *p = list;
2441 const char *actor_id; 2508 const char *actor_id;
2442 2509
2443 while (xs_list_iter(&p, &actor_id)) { 2510 xs_list_foreach(list, actor_id) {
2444 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); 2511 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
2445 xs *actor = NULL; 2512 xs *actor = NULL;
2446 2513
@@ -2509,6 +2576,15 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2509 html_button("limit", L("Limit"), 2576 html_button("limit", L("Limit"),
2510 L("Block announces (boosts) from this user"))); 2577 L("Block announces (boosts) from this user")));
2511 } 2578 }
2579 else
2580 if (pending_check(snac, actor_id)) {
2581 xs_html_add(form,
2582 html_button("approve", L("Approve"),
2583 L("Approve this follow request")));
2584
2585 xs_html_add(form,
2586 html_button("discard", L("Discard"), L("Discard this follow request")));
2587 }
2512 else { 2588 else {
2513 xs_html_add(form, 2589 xs_html_add(form,
2514 html_button("follow", L("Follow"), 2590 html_button("follow", L("Follow"),
@@ -2563,13 +2639,23 @@ xs_str *html_people(snac *user)
2563 xs *wing = following_list(user); 2639 xs *wing = following_list(user);
2564 xs *wers = follower_list(user); 2640 xs *wers = follower_list(user);
2565 2641
2642 xs_html *lists = xs_html_tag("div",
2643 xs_html_attr("class", "snac-posts"));
2644
2645 if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
2646 xs *pending = pending_list(user);
2647 xs_html_add(lists,
2648 html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
2649 }
2650
2651 xs_html_add(lists,
2652 html_people_list(user, wing, L("People you follow"), "i", proxy),
2653 html_people_list(user, wers, L("People that follow you"), "e", proxy));
2654
2566 xs_html *html = xs_html_tag("html", 2655 xs_html *html = xs_html_tag("html",
2567 html_user_head(user, NULL, NULL), 2656 html_user_head(user, NULL, NULL),
2568 xs_html_add(html_user_body(user, 0), 2657 xs_html_add(html_user_body(user, 0),
2569 xs_html_tag("div", 2658 lists,
2570 xs_html_attr("class", "snac-posts"),
2571 html_people_list(user, wing, L("People you follow"), "i", proxy),
2572 html_people_list(user, wers, L("People that follow you"), "e", proxy)),
2573 html_footer())); 2659 html_footer()));
2574 2660
2575 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 2661 return xs_html_render_s(html, "<!DOCTYPE html>\n");
@@ -2661,6 +2747,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
2661 label = wrk; 2747 label = wrk;
2662 } 2748 }
2663 } 2749 }
2750 else
2751 if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
2752 label = L("Follow Request");
2664 2753
2665 xs *s_date = xs_crop_i(xs_dup(date), 0, 10); 2754 xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
2666 2755
@@ -2909,6 +2998,48 @@ int html_get_handler(const xs_dict *req, const char *q_path,
2909 const char *q = xs_dict_get(q_vars, "q"); 2998 const char *q = xs_dict_get(q_vars, "q");
2910 2999
2911 if (q && *q) { 3000 if (q && *q) {
3001 if (xs_regex_match(q, "^@?[a-zA-Z0-9_]+@[a-zA-Z0-9-]+\\.")) {
3002 /** search account **/
3003 xs *actor = NULL;
3004 xs *acct = NULL;
3005 xs *l = xs_list_new();
3006 xs_html *page = NULL;
3007
3008 if (valid_status(webfinger_request(q, &actor, &acct))) {
3009 xs *actor_obj = NULL;
3010
3011 if (valid_status(actor_request(&snac, actor, &actor_obj))) {
3012 actor_add(actor, actor_obj);
3013
3014 /* create a people list with only one element */
3015 l = xs_list_append(xs_list_new(), actor);
3016
3017 xs *title = xs_fmt(L("Search results for account %s"), q);
3018
3019 page = html_people_list(&snac, l, title, "wf", NULL);
3020 }
3021 }
3022
3023 if (page == NULL) {
3024 xs *title = xs_fmt(L("Account %s not found"), q);
3025
3026 page = xs_html_tag("div",
3027 xs_html_tag("h2",
3028 xs_html_attr("class", "snac-header"),
3029 xs_html_text(title)));
3030 }
3031
3032 xs_html *html = xs_html_tag("html",
3033 html_user_head(&snac, NULL, NULL),
3034 xs_html_add(html_user_body(&snac, 0),
3035 page,
3036 html_footer()));
3037
3038 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
3039 *b_size = strlen(*body);
3040 status = HTTP_STATUS_OK;
3041 }
3042 else
2912 if (*q == '#') { 3043 if (*q == '#') {
2913 /** search by tag **/ 3044 /** search by tag **/
2914 xs *tl = tag_search(q, skip, show + 1); 3045 xs *tl = tag_search(q, skip, show + 1);
@@ -3647,6 +3778,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
3647 timeline_touch(&snac); 3778 timeline_touch(&snac);
3648 } 3779 }
3649 else 3780 else
3781 if (strcmp(action, L("Approve")) == 0) { /** **/
3782 xs *fwreq = pending_get(&snac, actor);
3783
3784 if (fwreq != NULL) {
3785 xs *reply = msg_accept(&snac, fwreq, actor);
3786
3787 enqueue_message(&snac, reply);
3788
3789 if (xs_is_null(xs_dict_get(fwreq, "published"))) {
3790 /* add a date if it doesn't include one (Mastodon) */
3791 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
3792 fwreq = xs_dict_set(fwreq, "published", date);
3793 }
3794
3795 timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
3796
3797 follower_add(&snac, actor);
3798
3799 pending_del(&snac, actor);
3800
3801 snac_log(&snac, xs_fmt("new follower %s", actor));
3802 }
3803 }
3804 else
3805 if (strcmp(action, L("Discard")) == 0) { /** **/
3806 pending_del(&snac, actor);
3807 }
3808 else
3650 status = HTTP_STATUS_NOT_FOUND; 3809 status = HTTP_STATUS_NOT_FOUND;
3651 3810
3652 /* delete the cached timeline */ 3811 /* delete the cached timeline */
@@ -3705,26 +3864,17 @@ int html_post_handler(const xs_dict *req, const char *q_path,
3705 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE)); 3864 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
3706 else 3865 else
3707 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE)); 3866 snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
3867 if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
3868 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
3869 else
3870 snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
3871 if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
3872 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
3873 else
3874 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
3708 3875
3709 if ((v = xs_dict_get(p_vars, "metadata")) != NULL) { 3876 if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
3710 /* split the metadata and store it as a dict */ 3877 snac.config = xs_dict_set(snac.config, "metadata", v);
3711 xs_dict *md = xs_dict_new();
3712 xs *l = xs_split(v, "\n");
3713 xs_list *p = l;
3714 const xs_str *kp;
3715
3716 while (xs_list_iter(&p, &kp)) {
3717 xs *kpl = xs_split_n(kp, "=", 1);
3718 if (xs_list_len(kpl) == 2) {
3719 xs *k2 = xs_strip_i(xs_dup(xs_list_get(kpl, 0)));
3720 xs *v2 = xs_strip_i(xs_dup(xs_list_get(kpl, 1)));
3721
3722 md = xs_dict_set(md, k2, v2);
3723 }
3724 }
3725
3726 snac.config = xs_dict_set(snac.config, "metadata", md);
3727 }
3728 3878
3729 /* uploads */ 3879 /* uploads */
3730 const char *uploads[] = { "avatar", "header", NULL }; 3880 const char *uploads[] = { "avatar", "header", NULL };
diff --git a/main.c b/main.c
index c285fac..76a7961 100644
--- a/main.c
+++ b/main.c
@@ -51,7 +51,9 @@ int usage(void)
51 printf("export_csv {basedir} {uid} Exports data as CSV files into current directory\n"); 51 printf("export_csv {basedir} {uid} Exports data as CSV files into current directory\n");
52 printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n"); 52 printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n");
53 printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n"); 53 printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n");
54 printf("import_csv {basedir} {uid} Imports data from CSV files into current directory\n"); 54 printf("import_csv {basedir} {uid} Imports data from CSV files in the current directory\n");
55 printf("import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n");
56 printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n");
55 57
56 return 1; 58 return 1;
57} 59}
@@ -589,6 +591,18 @@ int main(int argc, char *argv[])
589 return 0; 591 return 0;
590 } 592 }
591 593
594 if (strcmp(cmd, "import_list") == 0) { /** **/
595 import_list_csv(&snac, url);
596
597 return 0;
598 }
599
600 if (strcmp(cmd, "import_block_list") == 0) { /** **/
601 import_blocked_accounts_csv(&snac, url);
602
603 return 0;
604 }
605
592 if (strcmp(cmd, "note") == 0) { /** **/ 606 if (strcmp(cmd, "note") == 0) { /** **/
593 xs *content = NULL; 607 xs *content = NULL;
594 xs *msg = NULL; 608 xs *msg = NULL;
diff --git a/mastoapi.c b/mastoapi.c
index a529990..990898b 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -663,6 +663,17 @@ xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
663 if (user_open(&user, prefu)) { 663 if (user_open(&user, prefu)) {
664 val_links = user.links; 664 val_links = user.links;
665 metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT)); 665 metadata = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
666
667 /* does this user want to publish their contact metrics? */
668 if (xs_is_true(xs_dict_get(user.config, "show_contact_metrics"))) {
669 xs *fwing = following_list(&user);
670 xs *fwers = follower_list(&user);
671 xs *ni = xs_number_new(xs_list_len(fwing));
672 xs *ne = xs_number_new(xs_list_len(fwers));
673
674 acct = xs_dict_append(acct, "followers_count", ne);
675 acct = xs_dict_append(acct, "following_count", ni);
676 }
666 } 677 }
667 } 678 }
668 679
@@ -1275,6 +1286,17 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
1275 acct = xs_dict_append(acct, "following_count", xs_stock(0)); 1286 acct = xs_dict_append(acct, "following_count", xs_stock(0));
1276 acct = xs_dict_append(acct, "statuses_count", xs_stock(0)); 1287 acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
1277 1288
1289 /* does this user want to publish their contact metrics? */
1290 if (xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"))) {
1291 xs *fwing = following_list(&snac);
1292 xs *fwers = follower_list(&snac);
1293 xs *ni = xs_number_new(xs_list_len(fwing));
1294 xs *ne = xs_number_new(xs_list_len(fwers));
1295
1296 acct = xs_dict_append(acct, "followers_count", ne);
1297 acct = xs_dict_append(acct, "following_count", ni);
1298 }
1299
1278 *body = xs_json_dumps(acct, 4); 1300 *body = xs_json_dumps(acct, 4);
1279 *ctype = "application/json"; 1301 *ctype = "application/json";
1280 *status = HTTP_STATUS_OK; 1302 *status = HTTP_STATUS_OK;
@@ -1349,6 +1371,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
1349 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) 1371 if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
1350 continue; 1372 continue;
1351 1373
1374 if (id && is_instance_blocked(id))
1375 continue;
1376
1352 const char *from = NULL; 1377 const char *from = NULL;
1353 if (strcmp(type, "Page") == 0) 1378 if (strcmp(type, "Page") == 0)
1354 from = xs_dict_get(msg, "audience"); 1379 from = xs_dict_get(msg, "audience");
diff --git a/snac.h b/snac.h
index ca6d63a..70b7828 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.64" 4#define VERSION "2.66-dev"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -143,6 +143,12 @@ int follower_del(snac *snac, const char *actor);
143int follower_check(snac *snac, const char *actor); 143int follower_check(snac *snac, const char *actor);
144xs_list *follower_list(snac *snac); 144xs_list *follower_list(snac *snac);
145 145
146int pending_add(snac *user, const char *actor, const xs_dict *msg);
147int pending_check(snac *user, const char *actor);
148xs_dict *pending_get(snac *user, const char *actor);
149void pending_del(snac *user, const char *actor);
150xs_list *pending_list(snac *user);
151
146double timeline_mtime(snac *snac); 152double timeline_mtime(snac *snac);
147int timeline_touch(snac *snac); 153int timeline_touch(snac *snac);
148int timeline_here(snac *snac, const char *md5); 154int timeline_here(snac *snac, const char *md5);
@@ -316,6 +322,7 @@ xs_dict *msg_update(snac *snac, const xs_dict *object);
316xs_dict *msg_ping(snac *user, const char *rcpt); 322xs_dict *msg_ping(snac *user, const char *rcpt);
317xs_dict *msg_pong(snac *user, const char *rcpt, const char *object); 323xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
318xs_dict *msg_move(snac *user, const char *new_account); 324xs_dict *msg_move(snac *user, const char *new_account);
325xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
319xs_dict *msg_question(snac *user, const char *content, xs_list *attach, 326xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
320 const xs_list *opts, int multiple, int end_secs); 327 const xs_list *opts, int multiple, int end_secs);
321 328
@@ -399,6 +406,10 @@ void verify_links(snac *user);
399 406
400void export_csv(snac *user); 407void export_csv(snac *user);
401int migrate_account(snac *user); 408int migrate_account(snac *user);
409
410void import_blocked_accounts_csv(snac *user, const char *fn);
411void import_following_accounts_csv(snac *user, const char *fn);
412void import_list_csv(snac *user, const char *fn);
402void import_csv(snac *user); 413void import_csv(snac *user);
403 414
404typedef enum { 415typedef enum {
diff --git a/utils.c b/utils.c
index 4f5ac55..df3b55d 100644
--- a/utils.c
+++ b/utils.c
@@ -670,20 +670,18 @@ void export_csv(snac *user)
670} 670}
671 671
672 672
673void import_csv(snac *user) 673void import_blocked_accounts_csv(snac *user, const char *fn)
674/* import CSV files from Mastodon */ 674/* imports a Mastodon CSV file of blocked accounts */
675{ 675{
676 FILE *f; 676 FILE *f;
677 const char *fn;
678 677
679 fn = "blocked_accounts.csv";
680 if ((f = fopen(fn, "r")) != NULL) { 678 if ((f = fopen(fn, "r")) != NULL) {
681 snac_log(user, xs_fmt("Importing from %s...", fn)); 679 snac_log(user, xs_fmt("Importing from %s...", fn));
682 680
683 while (!feof(f)) { 681 while (!feof(f)) {
684 xs *l = xs_strip_i(xs_readline(f)); 682 xs *l = xs_strip_i(xs_readline(f));
685 683
686 if (*l) { 684 if (*l && strchr(l, '@') != NULL) {
687 xs *url = NULL; 685 xs *url = NULL;
688 xs *uid = NULL; 686 xs *uid = NULL;
689 687
@@ -704,8 +702,14 @@ void import_csv(snac *user)
704 } 702 }
705 else 703 else
706 snac_log(user, xs_fmt("Cannot open file %s", fn)); 704 snac_log(user, xs_fmt("Cannot open file %s", fn));
705}
706
707
708void import_following_accounts_csv(snac *user, const char *fn)
709/* imports a Mastodon CSV file of accounts to follow */
710{
711 FILE *f;
707 712
708 fn = "following_accounts.csv";
709 if ((f = fopen(fn, "r")) != NULL) { 713 if ((f = fopen(fn, "r")) != NULL) {
710 snac_log(user, xs_fmt("Importing from %s...", fn)); 714 snac_log(user, xs_fmt("Importing from %s...", fn));
711 715
@@ -757,8 +761,14 @@ void import_csv(snac *user)
757 } 761 }
758 else 762 else
759 snac_log(user, xs_fmt("Cannot open file %s", fn)); 763 snac_log(user, xs_fmt("Cannot open file %s", fn));
764}
765
766
767void import_list_csv(snac *user, const char *fn)
768/* imports a Mastodon CSV file list */
769{
770 FILE *f;
760 771
761 fn = "lists.csv";
762 if ((f = fopen(fn, "r")) != NULL) { 772 if ((f = fopen(fn, "r")) != NULL) {
763 snac_log(user, xs_fmt("Importing from %s...", fn)); 773 snac_log(user, xs_fmt("Importing from %s...", fn));
764 774
@@ -782,6 +792,21 @@ void import_csv(snac *user)
782 792
783 list_content(user, list_id, actor_md5, 1); 793 list_content(user, list_id, actor_md5, 1);
784 snac_log(user, xs_fmt("Added %s to list %s", url, lname)); 794 snac_log(user, xs_fmt("Added %s to list %s", url, lname));
795
796 if (!following_check(user, url)) {
797 xs *msg = msg_follow(user, url);
798
799 if (msg == NULL) {
800 snac_log(user, xs_fmt("Cannot follow %s -- server down?", acct));
801 continue;
802 }
803
804 following_add(user, url, msg);
805
806 enqueue_output_by_actor(user, msg, url, 0);
807
808 snac_log(user, xs_fmt("Following %s", url));
809 }
785 } 810 }
786 else 811 else
787 snac_log(user, xs_fmt("Webfinger error while adding %s to list %s", acct, lname)); 812 snac_log(user, xs_fmt("Webfinger error while adding %s to list %s", acct, lname));
@@ -793,6 +818,20 @@ void import_csv(snac *user)
793 } 818 }
794 else 819 else
795 snac_log(user, xs_fmt("Cannot open file %s", fn)); 820 snac_log(user, xs_fmt("Cannot open file %s", fn));
821}
822
823
824void import_csv(snac *user)
825/* import CSV files from Mastodon */
826{
827 FILE *f;
828 const char *fn;
829
830 import_blocked_accounts_csv(user, "blocked_accounts.csv");
831
832 import_following_accounts_csv(user, "following_accounts.csv");
833
834 import_list_csv(user, "lists.csv");
796 835
797 fn = "bookmarks.csv"; 836 fn = "bookmarks.csv";
798 if ((f = fopen(fn, "r")) != NULL) { 837 if ((f = fopen(fn, "r")) != NULL) {
diff --git a/xs_url.h b/xs_url.h
index cd540fa..ac43585 100644
--- a/xs_url.h
+++ b/xs_url.h
@@ -106,13 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
106 if (xs_list_len(l1) != 2) 106 if (xs_list_len(l1) != 2)
107 return NULL; 107 return NULL;
108 108
109 boundary = xs_dup(xs_list_get(l1, 1)); 109 xs *t_boundary = xs_dup(xs_list_get(l1, 1));
110 110
111 /* Tokodon sends the boundary header with double quotes surrounded */ 111 /* Tokodon sends the boundary header with double quotes surrounded */
112 if (xs_between("\"", boundary, "\"") != 0) 112 if (xs_between("\"", t_boundary, "\"") != 0)
113 boundary = xs_strip_chars_i(boundary, "\""); 113 t_boundary = xs_strip_chars_i(t_boundary, "\"");
114 114
115 boundary = xs_fmt("--%s", boundary); 115 boundary = xs_fmt("--%s", t_boundary);
116 } 116 }
117 117
118 bsz = strlen(boundary); 118 bsz = strlen(boundary);
diff --git a/xs_version.h b/xs_version.h
index 7c4246b..770366a 100644
--- a/xs_version.h
+++ b/xs_version.h
@@ -1 +1 @@
/* ab0749f821f1c98d16cbec53201bdf2ba2a24a43 2024-11-20T17:02:42+01:00 */ /* 297f71e198be7819213e9122e1e78c3b963111bc 2024-11-24T18:48:42+01:00 */