commit bd74ffd

Giacomo Tesio  ·  2024-12-05 21:53:40 +0000 UTC
parents b6107c8, 221eeab
Merge branch 'master' into build-with-musl
16 files changed,  +673, -95
M data.c
M html.c
M main.c
M snac.h
+28, -0
 1@@ -1,5 +1,33 @@
 2 # Release Notes
 3 
 4+## UNRELEASED
 5+
 6+As 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.
 7+
 8+Some fixes to blocked instances code (posts from them were sometimes shown).
 9+
10+## 2.65
11+
12+Added a new user option to disable automatic follow confirmations (follow requests must be manually approved from the people page).
13+
14+The search box also searches for accounts (via webfinger).
15+
16+New 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).
17+
18+New command-line action `import_block_list`, to import a Mastodon list of accounts to be blocked in CSV format.
19+
20+## 2.64
21+
22+Some tweaks for better integration with https://bsky.brid.gy (the BlueSky bridge by brid.gy).
23+
24+A corner case bug in the media proxying code has been fixed.
25+
26+Hashtags can now include underscores.
27+
28+The server now creates a pidfile inside the data directory.
29+
30+Mastodon API: fixed a crash in the notification code, fixed autocapitalization in the OAuth login field (contributed by fkooman).
31+
32 ## 2.63
33 
34 The server can now act as a proxy for all image, audio or video media coming from other account's posts (both from the Web UI and the Mastodon API). This way, other servers will see media requests coming from the server IP, not the user's, improving privacy. This is controlled by setting the `proxy_media` boolean field to `server.json` to true.
+10, -4
 1@@ -16,10 +16,6 @@ Important: deleting a follower should do more that just delete the object, see h
 2 
 3 ## Wishlist
 4 
 5-Implement Proxying for Media Links to Enhance User Privacy (see https://codeberg.org/grunfink/snac2/issues/219 for more information).
 6-
 7-Consider showing only posts by the account owner (not full trees) (see https://codeberg.org/grunfink/snac2/issues/217 for more information).
 8-
 9 Add support for subscribing and posting to relays (see https://codeberg.org/grunfink/snac2/issues/216 for more information).
10 
11 The instance timeline should also show boosts from users.
12@@ -357,3 +353,13 @@ Fix a crash when posting from the links browser (2.63, 2024-11-08T15:57:25+0100)
13 Fix some repeated images in Lemmy posts (2.63, 2024-11-08T15:57:25+0100).
14 
15 Fix a crash when posting an image from the tooot mobile app (2.63, 2024-11-11T19:42:11+0100).
16+
17+Fix some URL proxying (2.64, 2024-11-16T07:26:23+0100).
18+
19+Allow underscores in hashtags (2.64, 2024-11-16T07:26:23+0100).
20+
21+Add a pidfile (2.64, 2024-11-17T10:21:29+0100).
22+
23+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).
24+
25+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).
+96, -25
  1@@ -183,6 +183,18 @@ const char *get_atto(const xs_dict *msg)
  2 }
  3 
  4 
  5+const char *get_in_reply_to(const xs_dict *msg)
  6+/* gets the inReplyTo id */
  7+{
  8+    const xs_val *in_reply_to = xs_dict_get(msg, "inReplyTo");
  9+
 10+    if (xs_type(in_reply_to) == XSTYPE_DICT)
 11+        in_reply_to = xs_dict_get(in_reply_to, "id");
 12+
 13+    return in_reply_to;
 14+}
 15+
 16+
 17 xs_list *get_attachments(const xs_dict *msg)
 18 /* unify the garbage fire that are the attachments */
 19 {
 20@@ -373,7 +385,7 @@ int timeline_request(snac *snac, const char **id, xs_str **wrk, int level)
 21                             }
 22 
 23                             /* does it have an ancestor? */
 24-                            const char *in_reply_to = xs_dict_get(object, "inReplyTo");
 25+                            const char *in_reply_to = get_in_reply_to(object);
 26 
 27                             /* store */
 28                             timeline_add(snac, nid, object);
 29@@ -671,7 +683,7 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
 30         return 3;
 31 
 32     /* is this message a reply to another? */
 33-    const char *irt = xs_dict_get(msg, "inReplyTo");
 34+    const char *irt = get_in_reply_to(msg);
 35     if (!xs_is_null(irt)) {
 36         xs *r_msg = NULL;
 37 
 38@@ -724,7 +736,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag)
 39         /* use this same server */
 40         def_srv = xs_dup(xs_dict_get(srv_config, "host"));
 41 
 42-    split = xs_regex_split(content, "(@[A-Za-z0-9_]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#[^[:punct:][:space:]]+)");
 43+    split = xs_regex_split(content, "(@[A-Za-z0-9_]+(@[A-Za-z0-9\\.-]+)?|&#[0-9]+;|#(_|[^[:punct:][:space:]])+)");
 44 
 45     p = split;
 46     while (xs_list_iter(&p, &v)) {
 47@@ -1026,15 +1038,14 @@ xs_dict *msg_base(snac *snac, const char *type, const char *id,
 48 }
 49 
 50 
 51-xs_dict *msg_collection(snac *snac, const char *id)
 52+xs_dict *msg_collection(snac *snac, const char *id, int items)
 53 /* creates an empty OrderedCollection message */
 54 {
 55     xs_dict *msg = msg_base(snac, "OrderedCollection", id, NULL, NULL, NULL);
 56-    xs *ol = xs_list_new();
 57+    xs *n = xs_number_new(items);
 58 
 59     msg = xs_dict_append(msg, "attributedTo", snac->actor);
 60-    msg = xs_dict_append(msg, "orderedItems", ol);
 61-    msg = xs_dict_append(msg, "totalItems",   xs_stock(0));
 62+    msg = xs_dict_append(msg, "totalItems",   n);
 63 
 64     return msg;
 65 }
 66@@ -1206,7 +1217,30 @@ xs_dict *msg_actor(snac *snac)
 67     }
 68 
 69     /* add the metadata as attachments of PropertyValue */
 70-    const xs_dict *metadata = xs_dict_get(snac->config, "metadata");
 71+    xs *metadata = NULL;
 72+    const xs_dict *md = xs_dict_get(snac->config, "metadata");
 73+
 74+    if (xs_type(md) == XSTYPE_DICT)
 75+        metadata = xs_dup(md);
 76+    else
 77+    if (xs_type(md) == XSTYPE_STRING) {
 78+        metadata = xs_dict_new();
 79+        xs *l = xs_split(md, "\n");
 80+        const char *ll;
 81+
 82+        xs_list_foreach(l, ll) {
 83+            xs *kv = xs_split_n(ll, "=", 1);
 84+            const char *k = xs_list_get(kv, 0);
 85+            const char *v = xs_list_get(kv, 1);
 86+
 87+            if (k && v) {
 88+                xs *kk = xs_strip_i(xs_dup(k));
 89+                xs *vv = xs_strip_i(xs_dup(v));
 90+                metadata = xs_dict_set(metadata, kk, vv);
 91+            }
 92+        }
 93+    }
 94+
 95     if (xs_type(metadata) == XSTYPE_DICT) {
 96         xs *attach = xs_list_new();
 97         const xs_str *k;
 98@@ -1252,6 +1286,10 @@ xs_dict *msg_actor(snac *snac)
 99         msg = xs_dict_set(msg, "alsoKnownAs", loaka);
100     }
101 
102+    const xs_val *manually = xs_dict_get(snac->config, "approve_followers");
103+    msg = xs_dict_set(msg, "manuallyApprovesFollowers",
104+        xs_stock(xs_is_true(manually) ? XSTYPE_TRUE : XSTYPE_FALSE));
105+
106     return msg;
107 }
108 
109@@ -1888,22 +1926,31 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
110                 object_add(actor, actor_obj);
111             }
112 
113-            xs *f_msg = xs_dup(msg);
114-            xs *reply = msg_accept(snac, f_msg, actor);
115-
116-            post_message(snac, actor, reply);
117+            if (xs_is_true(xs_dict_get(snac->config, "approve_followers"))) {
118+                pending_add(snac, actor, msg);
119 
120-            if (xs_is_null(xs_dict_get(f_msg, "published"))) {
121-                /* add a date if it doesn't include one (Mastodon) */
122-                xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
123-                f_msg = xs_dict_set(f_msg, "published", date);
124+                snac_log(snac, xs_fmt("new pending follower approval %s", actor));
125             }
126+            else {
127+                /* automatic following */
128+                xs *f_msg = xs_dup(msg);
129+                xs *reply = msg_accept(snac, f_msg, actor);
130+
131+                post_message(snac, actor, reply);
132 
133-            timeline_add(snac, id, f_msg);
134+                if (xs_is_null(xs_dict_get(f_msg, "published"))) {
135+                    /* add a date if it doesn't include one (Mastodon) */
136+                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
137+                    f_msg = xs_dict_set(f_msg, "published", date);
138+                }
139+
140+                timeline_add(snac, id, f_msg);
141 
142-            follower_add(snac, actor);
143+                follower_add(snac, actor);
144+
145+                snac_log(snac, xs_fmt("new follower %s", actor));
146+            }
147 
148-            snac_log(snac, xs_fmt("new follower %s", actor));
149             do_notify = 1;
150         }
151         else
152@@ -1924,6 +1971,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
153                     snac_log(snac, xs_fmt("no longer following us %s", actor));
154                     do_notify = 1;
155                 }
156+                else
157+                if (pending_check(snac, actor)) {
158+                    pending_del(snac, actor);
159+                    snac_log(snac, xs_fmt("cancelled pending follow from %s", actor));
160+                }
161                 else
162                     snac_log(snac, xs_fmt("error deleting follower %s", actor));
163             }
164@@ -1957,7 +2009,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
165 
166         if (xs_match(utype, "Note|Article")) { /** **/
167             const char *id          = xs_dict_get(object, "id");
168-            const char *in_reply_to = xs_dict_get(object, "inReplyTo");
169+            const char *in_reply_to = get_in_reply_to(object);
170             const char *atto        = get_atto(object);
171             xs *wrk           = NULL;
172 
173@@ -2784,6 +2836,8 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
174 
175     *ctype  = "application/activity+json";
176 
177+    int show_contact_metrics = xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"));
178+
179     if (p_path == NULL) {
180         /* if there was no component after the user, it's an actor request */
181         msg = msg_actor(&snac);
182@@ -2797,7 +2851,6 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
183     if (strcmp(p_path, "outbox") == 0 || strcmp(p_path, "featured") == 0) {
184         xs *id = xs_fmt("%s/%s", snac.actor, p_path);
185         xs *list = xs_list_new();
186-        msg = msg_collection(&snac, id);
187         const char *v;
188         int tc = 0;
189 
190@@ -2819,14 +2872,32 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
191         }
192 
193         /* replace the 'orderedItems' with the latest posts */
194-        xs *items = xs_number_new(xs_list_len(list));
195+        msg = msg_collection(&snac, id, xs_list_len(list));
196         msg = xs_dict_set(msg, "orderedItems", list);
197-        msg = xs_dict_set(msg, "totalItems",   items);
198     }
199     else
200-    if (strcmp(p_path, "followers") == 0 || strcmp(p_path, "following") == 0) {
201+    if (strcmp(p_path, "followers") == 0) {
202+        int total = 0;
203+
204+        if (show_contact_metrics) {
205+            xs *l = follower_list(&snac);
206+            total = xs_list_len(l);
207+        }
208+
209+        xs *id = xs_fmt("%s/%s", snac.actor, p_path);
210+        msg = msg_collection(&snac, id, total);
211+    }
212+    else
213+    if (strcmp(p_path, "following") == 0) {
214+        int total = 0;
215+
216+        if (show_contact_metrics) {
217+            xs *l = following_list(&snac);
218+            total = xs_list_len(l);
219+        }
220+
221         xs *id = xs_fmt("%s/%s", snac.actor, p_path);
222-        msg = msg_collection(&snac, id);
223+        msg = msg_collection(&snac, id, total);
224     }
225     else
226     if (xs_startswith(p_path, "p/")) {
M data.c
+120, -1
  1@@ -336,6 +336,35 @@ int user_persist(snac *snac, int publish)
  2     xs *bfn = xs_fmt("%s.bak", fn);
  3     FILE *f;
  4 
  5+    if (publish) {
  6+        /* check if any of the relevant fields have really changed */
  7+        if ((f = fopen(fn, "r")) != NULL) {
  8+            xs *old = xs_json_load(f);
  9+            fclose(f);
 10+
 11+            if (old != NULL) {
 12+                int nw = 0;
 13+                const char *fields[] = { "header", "avatar", "name", "bio", "metadata", NULL };
 14+
 15+                for (int n = 0; fields[n]; n++) {
 16+                    const char *of = xs_dict_get(old, fields[n]);
 17+                    const char *nf = xs_dict_get(snac->config, fields[n]);
 18+
 19+                    if (of == NULL && nf == NULL)
 20+                        continue;
 21+
 22+                    if (xs_type(of) != XSTYPE_STRING || xs_type(nf) != XSTYPE_STRING || strcmp(of, nf)) {
 23+                        nw = 1;
 24+                        break;
 25+                    }
 26+                }
 27+
 28+                if (!nw)
 29+                    publish = 0;
 30+            }
 31+        }
 32+    }
 33+
 34     rename(fn, bfn);
 35 
 36     if ((f = fopen(fn, "w")) != NULL) {
 37@@ -799,7 +828,7 @@ int _object_add(const char *id, const xs_dict *obj, int ow)
 38         fclose(f);
 39 
 40         /* does this object has a parent? */
 41-        const char *in_reply_to = xs_dict_get(obj, "inReplyTo");
 42+        const char *in_reply_to = get_in_reply_to(obj);
 43 
 44         if (!xs_is_null(in_reply_to) && *in_reply_to) {
 45             /* update the children index of the parent */
 46@@ -1176,6 +1205,96 @@ xs_list *follower_list(snac *snac)
 47 }
 48 
 49 
 50+/** pending followers **/
 51+
 52+int pending_add(snac *user, const char *actor, const xs_dict *msg)
 53+/* stores the follow message for later confirmation */
 54+{
 55+    xs *dir = xs_fmt("%s/pending", user->basedir);
 56+    xs *md5 = xs_md5_hex(actor, strlen(actor));
 57+    xs *fn  = xs_fmt("%s/%s.json", dir, md5);
 58+    FILE *f;
 59+
 60+    mkdirx(dir);
 61+
 62+    if ((f = fopen(fn, "w")) == NULL)
 63+        return -1;
 64+
 65+    xs_json_dump(msg, 4, f);
 66+    fclose(f);
 67+
 68+    return 0;
 69+}
 70+
 71+
 72+int pending_check(snac *user, const char *actor)
 73+/* checks if there is a pending follow confirmation for the actor */
 74+{
 75+    xs *md5 = xs_md5_hex(actor, strlen(actor));
 76+    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
 77+
 78+    return mtime(fn) != 0;
 79+}
 80+
 81+
 82+xs_dict *pending_get(snac *user, const char *actor)
 83+/* returns the pending follow confirmation for the actor */
 84+{
 85+    xs *md5 = xs_md5_hex(actor, strlen(actor));
 86+    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
 87+    xs_dict *msg = NULL;
 88+    FILE *f;
 89+
 90+    if ((f = fopen(fn, "r")) != NULL) {
 91+        msg = xs_json_load(f);
 92+        fclose(f);
 93+    }
 94+
 95+    return msg;
 96+}
 97+
 98+
 99+void pending_del(snac *user, const char *actor)
100+/* deletes a pending follow confirmation for the actor */
101+{
102+    xs *md5 = xs_md5_hex(actor, strlen(actor));
103+    xs *fn = xs_fmt("%s/pending/%s.json", user->basedir, md5);
104+
105+    unlink(fn);
106+}
107+
108+
109+xs_list *pending_list(snac *user)
110+/* returns a list of pending follow confirmations */
111+{
112+    xs *spec = xs_fmt("%s/pending/""*.json", user->basedir);
113+    xs *l = xs_glob(spec, 0, 0);
114+    xs_list *r = xs_list_new();
115+    const char *v;
116+
117+    xs_list_foreach(l, v) {
118+        FILE *f;
119+        xs *msg = NULL;
120+
121+        if ((f = fopen(v, "r")) == NULL)
122+            continue;
123+
124+        msg = xs_json_load(f);
125+        fclose(f);
126+
127+        if (msg == NULL)
128+            continue;
129+
130+        const char *actor = xs_dict_get(msg, "actor");
131+
132+        if (xs_type(actor) == XSTYPE_STRING)
133+            r = xs_list_append(r, actor);
134+    }
135+
136+    return r;
137+}
138+
139+
140 /** timeline **/
141 
142 double timeline_mtime(snac *snac)
+35, -1
 1@@ -129,6 +129,28 @@ Just what it says in the tin. This is to mitigate spammers
 2 coming from Fediverse instances with lax / open registration
 3 processes. Please take note that this also avoids possibly
 4 legitimate people trying to contact you.
 5+.It This account is a bot
 6+Set this checkbox if this account behaves like a bot (i.e.
 7+posts are automatically generated).
 8+.It Auto-boost all mentions to this account
 9+If this toggle is set, all mentions to this account are boosted
10+to all followers. This can be used to create groups.
11+.It This account is private
12+If this toggle is set, posts are not published via the public
13+web interface, only via the ActivityPub protocol.
14+.It Collapse top threads by default
15+If this toggle is set, the private timeline will always show
16+conversations collapsed by default. This allows easier navigation
17+through long threads.
18+.It Follow requests must be approved
19+If this toggle is set, follow requests are not automatically
20+accepted, but notified and stored for later review. Pending
21+follow requests will be shown in the people page to be
22+approved or discarded.
23+.It Publish follower and following metrics
24+If this toggle is set, the number of followers and following
25+accounts are made public (this is only the number; the specific
26+lists of accounts are never published).
27 .It Password
28 Write the same string in these two fields to change your
29 password. Don't write anything if you don't want to do this.
30@@ -262,6 +284,13 @@ section 'Migrating from snac to Mastodon').
31 Starts a migration from this account to the one set as an alias (see
32 .Xr snac 8 ,
33 section 'Migrating from snac to Mastodon').
34+.It Cm import_csv Ar basedir Ar uid
35+Imports CSV data files from a Mastodon export. This command expects the
36+following files to be in the current directory:
37+.Pa bookmarks.csv ,
38+.Pa blocked_accounts.csv ,
39+.Pa lists.csv , and
40+.Pa following_accounts.csv .
41 .It Cm state Ar basedir
42 Dumps the current state of the server and its threads. For example:
43 .Bd -literal -offset indent
44@@ -284,6 +313,11 @@ in-memory job queue. The thread state can be: waiting (idle waiting
45 for a job to be assigned), input or output (processing I/O packets)
46 or stopped (not running, only to be seen while starting or stopping
47 the server).
48+.It Cm import_list Ar basedir Ar uid Ar file
49+Imports a Mastodon list in CSV format. This option can be used to
50+import "Mastodon Follow Packs".
51+.It Cm import_block_list Ar basedir Ar uid Ar file
52+Imports a Mastodon list of accounts to be blocked in CSV format.
53 .El
54 .Ss Migrating an account to/from Mastodon
55 See 
56@@ -349,4 +383,4 @@ See the LICENSE file for details.
57 .Sh CAVEATS
58 Use the Fediverse sparingly. Don't fear the MUTE button.
59 .Sh BUGS
60-Probably plenty. Some issues may be even documented in the TODO.md file.
61+Probably many. Some issues may be even documented in the TODO.md file.
+2, -0
1@@ -209,6 +209,8 @@ web interface.
2 .It Pa history/
3 This directory contains generated HTML files. They may be snapshots of the
4 local timeline in previous months or other cached data.
5+.It Pa server.pid
6+This file stores the server PID in a single text line.
7 .El
8 .Sh SEE ALSO
9 .Xr snac 1 ,
M html.c
+192, -42
  1@@ -770,7 +770,7 @@ static xs_html *html_user_body(snac *user, int read_only)
  2                     xs_html_sctag("input",
  3                         xs_html_attr("type", "text"),
  4                         xs_html_attr("name", "q"),
  5-                        xs_html_attr("title", L("Search posts by content (regular expression) or #tag")),
  6+                        xs_html_attr("title", L("Search posts by content (regular expression), @user@host accounts, or #tag")),
  7                         xs_html_attr("placeholder", L("Content search")))));
  8     }
  9 
 10@@ -829,21 +829,45 @@ static xs_html *html_user_body(snac *user, int read_only)
 11     }
 12 
 13     if (read_only) {
 14-        xs *es1  = encode_html(xs_dict_get(user->config, "bio"));
 15         xs *tags = xs_list_new();
 16-        xs *bio1 = not_really_markdown(es1, NULL, &tags);
 17+        xs *bio1 = not_really_markdown(xs_dict_get(user->config, "bio"), NULL, &tags);
 18         xs *bio2 = process_tags(user, bio1, &tags);
 19+        xs *bio3 = sanitize(bio2);
 20 
 21-        bio2 = replace_shortnames(bio2, tags, 2, proxy);
 22+        bio3 = replace_shortnames(bio3, tags, 2, proxy);
 23 
 24         xs_html *top_user_bio = xs_html_tag("div",
 25             xs_html_attr("class", "p-note snac-top-user-bio"),
 26-            xs_html_raw(bio2)); /* already sanitized */
 27+            xs_html_raw(bio3)); /* already sanitized */
 28 
 29         xs_html_add(top_user,
 30             top_user_bio);
 31 
 32-        const xs_dict *metadata = xs_dict_get(user->config, "metadata");
 33+        xs *metadata = NULL;
 34+        const xs_dict *md = xs_dict_get(user->config, "metadata");
 35+
 36+        if (xs_type(md) == XSTYPE_DICT)
 37+            metadata = xs_dup(md);
 38+        else
 39+        if (xs_type(md) == XSTYPE_STRING) {
 40+            /* convert to dict for easier iteration */
 41+            metadata = xs_dict_new();
 42+            xs *l = xs_split(md, "\n");
 43+            const char *ll;
 44+
 45+            xs_list_foreach(l, ll) {
 46+                xs *kv = xs_split_n(ll, "=", 1);
 47+                const char *k = xs_list_get(kv, 0);
 48+                const char *v = xs_list_get(kv, 1);
 49+
 50+                if (k && v) {
 51+                    xs *kk = xs_strip_i(xs_dup(k));
 52+                    xs *vv = xs_strip_i(xs_dup(v));
 53+                    metadata = xs_dict_set(metadata, kk, vv);
 54+                }
 55+            }
 56+        }
 57+
 58         if (xs_type(metadata) == XSTYPE_DICT) {
 59             const xs_str *k;
 60             const xs_str *v;
 61@@ -914,6 +938,18 @@ static xs_html *html_user_body(snac *user, int read_only)
 62             xs_html_add(top_user,
 63                 snac_metadata);
 64         }
 65+
 66+        if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
 67+            xs *fwers = follower_list(user);
 68+            xs *fwing = following_list(user);
 69+
 70+            xs *s1 = xs_fmt(L("%d following %d followers"),
 71+                xs_list_len(fwing), xs_list_len(fwers));
 72+
 73+            xs_html_add(top_user,
 74+                xs_html_tag("p",
 75+                    xs_html_text(s1)));
 76+        }
 77     }
 78 
 79     xs_html_add(body,
 80@@ -1025,20 +1061,31 @@ xs_html *html_top_controls(snac *snac)
 81     const xs_val *a_private = xs_dict_get(snac->config, "private");
 82     const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
 83     const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
 84+    const xs_val *pending    = xs_dict_get(snac->config, "approve_followers");
 85+    const xs_val *show_foll  = xs_dict_get(snac->config, "show_contact_metrics");
 86 
 87-    xs *metadata = xs_str_new(NULL);
 88+    xs *metadata = NULL;
 89     const xs_dict *md = xs_dict_get(snac->config, "metadata");
 90-    const xs_str *k;
 91-    const xs_str *v;
 92 
 93-    int c = 0;
 94-    while (xs_dict_next(md, &k, &v, &c)) {
 95-        xs *kp = xs_fmt("%s=%s", k, v);
 96+    if (xs_type(md) == XSTYPE_DICT) {
 97+        const xs_str *k;
 98+        const xs_str *v;
 99 
100-        if (*metadata)
101-            metadata = xs_str_cat(metadata, "\n");
102-        metadata = xs_str_cat(metadata, kp);
103+        metadata = xs_str_new(NULL);
104+
105+        xs_dict_foreach(md, k, v) {
106+            xs *kp = xs_fmt("%s=%s", k, v);
107+
108+            if (*metadata)
109+                metadata = xs_str_cat(metadata, "\n");
110+            metadata = xs_str_cat(metadata, kp);
111+        }
112     }
113+    else
114+    if (xs_type(md) == XSTYPE_STRING)
115+        metadata = xs_dup(md);
116+    else
117+        metadata = xs_str_new(NULL);
118 
119     xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
120 
121@@ -1187,6 +1234,24 @@ xs_html *html_top_controls(snac *snac)
122                     xs_html_tag("label",
123                         xs_html_attr("for", "collapse_threads"),
124                         xs_html_text(L("Collapse top threads by default")))),
125+                xs_html_tag("p",
126+                    xs_html_sctag("input",
127+                        xs_html_attr("type", "checkbox"),
128+                        xs_html_attr("name", "approve_followers"),
129+                        xs_html_attr("id",   "approve_followers"),
130+                        xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
131+                    xs_html_tag("label",
132+                        xs_html_attr("for", "approve_followers"),
133+                        xs_html_text(L("Follow requests must be approved")))),
134+                xs_html_tag("p",
135+                    xs_html_sctag("input",
136+                        xs_html_attr("type", "checkbox"),
137+                        xs_html_attr("name", "show_contact_metrics"),
138+                        xs_html_attr("id",   "show_contact_metrics"),
139+                        xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
140+                    xs_html_tag("label",
141+                        xs_html_attr("for", "show_contact_metrics"),
142+                        xs_html_text(L("Publish follower and following metrics")))),
143                 xs_html_tag("p",
144                     xs_html_text(L("Profile metadata (key=value pairs in each line):")),
145                     xs_html_sctag("br", NULL),
146@@ -1481,6 +1546,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
147     if ((read_only || !user) && !is_msg_public(msg))
148         return NULL;
149 
150+    if (id && is_instance_blocked(id))
151+        return NULL;
152+
153     if (user && level == 0 && xs_is_true(xs_dict_get(user->config, "collapse_threads")))
154         collapse_threads = 1;
155 
156@@ -1670,7 +1738,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
157     if (strcmp(type, "Note") == 0) {
158         if (level == 0) {
159             /* is the parent not here? */
160-            const char *parent = xs_dict_get(msg, "inReplyTo");
161+            const char *parent = get_in_reply_to(msg);
162 
163             if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
164                 xs_html_add(post_header,
165@@ -2329,7 +2397,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
166 
167         /* is this message a non-public reply? */
168         if (user != NULL && !is_msg_public(msg)) {
169-            const char *irt = xs_dict_get(msg, "inReplyTo");
170+            const char *irt = get_in_reply_to(msg);
171 
172             /* is it a reply to something not in the storage? */
173             if (!xs_is_null(irt) && !object_here(irt)) {
174@@ -2437,10 +2505,9 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
175                 xs_html_tag("summary",
176                     xs_html_text("..."))));
177 
178-    xs_list *p = list;
179     const char *actor_id;
180 
181-    while (xs_list_iter(&p, &actor_id)) {
182+    xs_list_foreach(list, actor_id) {
183         xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
184         xs *actor = NULL;
185 
186@@ -2509,6 +2576,15 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
187                         html_button("limit", L("Limit"),
188                                 L("Block announces (boosts) from this user")));
189             }
190+            else
191+            if (pending_check(snac, actor_id)) {
192+                xs_html_add(form,
193+                    html_button("approve", L("Approve"),
194+                                L("Approve this follow request")));
195+
196+                xs_html_add(form,
197+                    html_button("discard", L("Discard"), L("Discard this follow request")));
198+            }
199             else {
200                 xs_html_add(form,
201                     html_button("follow", L("Follow"),
202@@ -2563,13 +2639,23 @@ xs_str *html_people(snac *user)
203     xs *wing = following_list(user);
204     xs *wers = follower_list(user);
205 
206+    xs_html *lists = xs_html_tag("div",
207+        xs_html_attr("class", "snac-posts"));
208+
209+    if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
210+        xs *pending = pending_list(user);
211+        xs_html_add(lists,
212+            html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
213+    }
214+
215+    xs_html_add(lists,
216+        html_people_list(user, wing, L("People you follow"), "i", proxy),
217+        html_people_list(user, wers, L("People that follow you"), "e", proxy));
218+
219     xs_html *html = xs_html_tag("html",
220         html_user_head(user, NULL, NULL),
221         xs_html_add(html_user_body(user, 0),
222-            xs_html_tag("div",
223-                xs_html_attr("class", "snac-posts"),
224-                html_people_list(user, wing, L("People you follow"), "i", proxy),
225-                html_people_list(user, wers, L("People that follow you"), "e", proxy)),
226+            lists,
227             html_footer()));
228 
229     return xs_html_render_s(html, "<!DOCTYPE html>\n");
230@@ -2661,6 +2747,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
231                 label = wrk;
232             }
233         }
234+        else
235+        if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
236+            label = L("Follow Request");
237 
238         xs *s_date = xs_crop_i(xs_dup(date), 0, 10);
239 
240@@ -2909,6 +2998,48 @@ int html_get_handler(const xs_dict *req, const char *q_path,
241             const char *q = xs_dict_get(q_vars, "q");
242 
243             if (q && *q) {
244+                if (xs_regex_match(q, "^@?[a-zA-Z0-9_]+@[a-zA-Z0-9-]+\\.")) {
245+                    /** search account **/
246+                    xs *actor = NULL;
247+                    xs *acct = NULL;
248+                    xs *l = xs_list_new();
249+                    xs_html *page = NULL;
250+
251+                    if (valid_status(webfinger_request(q, &actor, &acct))) {
252+                        xs *actor_obj = NULL;
253+
254+                        if (valid_status(actor_request(&snac, actor, &actor_obj))) {
255+                            actor_add(actor, actor_obj);
256+
257+                            /* create a people list with only one element */
258+                            l = xs_list_append(xs_list_new(), actor);
259+
260+                            xs *title = xs_fmt(L("Search results for account %s"), q);
261+
262+                            page = html_people_list(&snac, l, title, "wf", NULL);
263+                        }
264+                    }
265+
266+                    if (page == NULL) {
267+                        xs *title = xs_fmt(L("Account %s not found"), q);
268+
269+                        page = xs_html_tag("div",
270+                            xs_html_tag("h2",
271+                                xs_html_attr("class", "snac-header"),
272+                                xs_html_text(title)));
273+                    }
274+
275+                    xs_html *html = xs_html_tag("html",
276+                        html_user_head(&snac, NULL, NULL),
277+                        xs_html_add(html_user_body(&snac, 0),
278+                        page,
279+                        html_footer()));
280+
281+                    *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
282+                    *b_size = strlen(*body);
283+                    status = HTTP_STATUS_OK;
284+                }
285+                else
286                 if (*q == '#') {
287                     /** search by tag **/
288                     xs *tl = tag_search(q, skip, show + 1);
289@@ -3646,6 +3777,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
290             unbookmark(&snac, id);
291             timeline_touch(&snac);
292         }
293+        else
294+        if (strcmp(action, L("Approve")) == 0) { /** **/
295+            xs *fwreq = pending_get(&snac, actor);
296+
297+            if (fwreq != NULL) {
298+                xs *reply = msg_accept(&snac, fwreq, actor);
299+
300+                enqueue_message(&snac, reply);
301+
302+                if (xs_is_null(xs_dict_get(fwreq, "published"))) {
303+                    /* add a date if it doesn't include one (Mastodon) */
304+                    xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
305+                    fwreq = xs_dict_set(fwreq, "published", date);
306+                }
307+
308+                timeline_add(&snac, xs_dict_get(fwreq, "id"), fwreq);
309+
310+                follower_add(&snac, actor);
311+
312+                pending_del(&snac, actor);
313+
314+                snac_log(&snac, xs_fmt("new follower %s", actor));
315+            }
316+        }
317+        else
318+        if (strcmp(action, L("Discard")) == 0) { /** **/
319+            pending_del(&snac, actor);
320+        }
321         else
322             status = HTTP_STATUS_NOT_FOUND;
323 
324@@ -3705,26 +3864,17 @@ int html_post_handler(const xs_dict *req, const char *q_path,
325             snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_TRUE));
326         else
327             snac.config = xs_dict_set(snac.config, "collapse_threads", xs_stock(XSTYPE_FALSE));
328+        if ((v = xs_dict_get(p_vars, "approve_followers")) != NULL && strcmp(v, "on") == 0)
329+            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_TRUE));
330+        else
331+            snac.config = xs_dict_set(snac.config, "approve_followers", xs_stock(XSTYPE_FALSE));
332+        if ((v = xs_dict_get(p_vars, "show_contact_metrics")) != NULL && strcmp(v, "on") == 0)
333+            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
334+        else
335+            snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
336 
337-        if ((v = xs_dict_get(p_vars, "metadata")) != NULL) {
338-            /* split the metadata and store it as a dict */
339-            xs_dict *md = xs_dict_new();
340-            xs *l = xs_split(v, "\n");
341-            xs_list *p = l;
342-            const xs_str *kp;
343-
344-            while (xs_list_iter(&p, &kp)) {
345-                xs *kpl = xs_split_n(kp, "=", 1);
346-                if (xs_list_len(kpl) == 2) {
347-                    xs *k2 = xs_strip_i(xs_dup(xs_list_get(kpl, 0)));
348-                    xs *v2 = xs_strip_i(xs_dup(xs_list_get(kpl, 1)));
349-
350-                    md = xs_dict_set(md, k2, v2);
351-                }
352-            }
353-
354-            snac.config = xs_dict_set(snac.config, "metadata", md);
355-        }
356+        if ((v = xs_dict_get(p_vars, "metadata")) != NULL)
357+            snac.config = xs_dict_set(snac.config, "metadata", v);
358 
359         /* uploads */
360         const char *uploads[] = { "avatar", "header", NULL };
+14, -0
 1@@ -774,6 +774,7 @@ void httpd(void)
 2     xs *sem_name = NULL;
 3     xs *shm_name = NULL;
 4     sem_t anon_job_sem;
 5+    xs *pidfile = xs_fmt("%s/server.pid", srv_basedir);
 6 
 7     address = xs_dict_get(srv_config, "address");
 8 
 9@@ -809,6 +810,17 @@ void httpd(void)
10     srv_log(xs_fmt("httpd%s start %s %s", p_state->use_fcgi ? " (FastCGI)" : "",
11                     full_address, USER_AGENT));
12 
13+    {
14+        FILE *f;
15+
16+        if ((f = fopen(pidfile, "w")) != NULL) {
17+            fprintf(f, "%d\n", getpid());
18+            fclose(f);
19+        }
20+        else
21+            srv_log(xs_fmt("Cannot create %s: %s", pidfile, strerror(errno)));
22+    }
23+
24     /* show the number of usable file descriptors */
25     struct rlimit r;
26     getrlimit(RLIMIT_NOFILE, &r);
27@@ -894,4 +906,6 @@ void httpd(void)
28     srv_log(xs_fmt("httpd%s stop %s (run time: %s)",
29                 p_state->use_fcgi ? " (FastCGI)" : "",
30                 full_address, uptime));
31+
32+    unlink(pidfile);
33 }
M main.c
+20, -2
 1@@ -51,7 +51,9 @@ int usage(void)
 2     printf("export_csv {basedir} {uid}           Exports data as CSV files into current directory\n");
 3     printf("alias {basedir} {uid} {account}      Sets account (@user@host or actor url) as an alias\n");
 4     printf("migrate {basedir} {uid}              Migrates to the account defined as the alias\n");
 5-    printf("import_csv {basedir} {uid}           Imports data from CSV files into current directory\n");
 6+    printf("import_csv {basedir} {uid}           Imports data from CSV files in the current directory\n");
 7+    printf("import_list {basedir} {uid} {file}   Imports a Mastodon CSV list file\n");
 8+    printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n");
 9 
10     return 1;
11 }
12@@ -558,7 +560,11 @@ int main(int argc, char *argv[])
13         if (data != NULL) {
14             xs_json_dump(data, 4, stdout);
15             enqueue_actor_refresh(&snac, xs_dict_get(data, "attributedTo"), 0);
16-            timeline_add(&snac, url, data);
17+
18+            if (!timeline_here(&snac, url))
19+                timeline_add(&snac, url, data);
20+            else
21+                printf("Post %s already here\n", url);
22         }
23 
24         return 0;
25@@ -585,6 +591,18 @@ int main(int argc, char *argv[])
26         return 0;
27     }
28 
29+    if (strcmp(cmd, "import_list") == 0) { /** **/
30+        import_list_csv(&snac, url);
31+
32+        return 0;
33+    }
34+
35+    if (strcmp(cmd, "import_block_list") == 0) { /** **/
36+        import_blocked_accounts_csv(&snac, url);
37+
38+        return 0;
39+    }
40+
41     if (strcmp(cmd, "note") == 0) { /** **/
42         xs *content = NULL;
43         xs *msg = NULL;
+53, -7
  1@@ -171,7 +171,7 @@ const char *login_page = ""
  2 "<body><h1>%s OAuth identify</h1>\n"
  3 "<div style=\"background-color: red; color: white\">%s</div>\n"
  4 "<form method=\"post\" action=\"%s:/" "/%s/%s\">\n"
  5-"<p>Login: <input type=\"text\" name=\"login\"></p>\n"
  6+"<p>Login: <input type=\"text\" name=\"login\" autocapitalize=\"off\"></p>\n"
  7 "<p>Password: <input type=\"password\" name=\"passwd\"></p>\n"
  8 "<input type=\"hidden\" name=\"redir\" value=\"%s\">\n"
  9 "<input type=\"hidden\" name=\"cid\" value=\"%s\">\n"
 10@@ -663,6 +663,17 @@ xs_dict *mastoapi_account(snac *logged, const xs_dict *actor)
 11         if (user_open(&user, prefu)) {
 12             val_links = user.links;
 13             metadata  = xs_dict_get_def(user.config, "metadata", xs_stock(XSTYPE_DICT));
 14+
 15+            /* does this user want to publish their contact metrics? */
 16+            if (xs_is_true(xs_dict_get(user.config, "show_contact_metrics"))) {
 17+                xs *fwing = following_list(&user);
 18+                xs *fwers = follower_list(&user);
 19+                xs *ni = xs_number_new(xs_list_len(fwing));
 20+                xs *ne = xs_number_new(xs_list_len(fwers));
 21+
 22+                acct = xs_dict_append(acct, "followers_count", ne);
 23+                acct = xs_dict_append(acct, "following_count", ni);
 24+            }
 25         }
 26     }
 27 
 28@@ -827,7 +838,16 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 29     st = xs_dict_append(st, "url",          id);
 30     st = xs_dict_append(st, "account",      acct);
 31 
 32-    xs *fd = mastoapi_date(xs_dict_get(msg, "published"));
 33+    const char *published = xs_dict_get(msg, "published");
 34+    xs *fd = NULL;
 35+
 36+    if (published)
 37+        fd = mastoapi_date(published);
 38+    else {
 39+        xs *p = xs_str_iso_date(0);
 40+        fd = mastoapi_date(p);
 41+    }
 42+
 43     st = xs_dict_append(st, "created_at", fd);
 44 
 45     {
 46@@ -1024,7 +1044,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
 47     st = xs_dict_append(st, "in_reply_to_id",         xs_stock(XSTYPE_NULL));
 48     st = xs_dict_append(st, "in_reply_to_account_id", xs_stock(XSTYPE_NULL));
 49 
 50-    tmp = xs_dict_get(msg, "inReplyTo");
 51+    tmp = get_in_reply_to(msg);
 52     if (!xs_is_null(tmp)) {
 53         xs *irto = NULL;
 54 
 55@@ -1266,6 +1286,17 @@ void credentials_get(char **body, char **ctype, int *status, snac snac)
 56     acct = xs_dict_append(acct, "following_count", xs_stock(0));
 57     acct = xs_dict_append(acct, "statuses_count", xs_stock(0));
 58 
 59+    /* does this user want to publish their contact metrics? */
 60+    if (xs_is_true(xs_dict_get(snac.config, "show_contact_metrics"))) {
 61+        xs *fwing = following_list(&snac);
 62+        xs *fwers = follower_list(&snac);
 63+        xs *ni = xs_number_new(xs_list_len(fwing));
 64+        xs *ne = xs_number_new(xs_list_len(fwers));
 65+
 66+        acct = xs_dict_append(acct, "followers_count", ne);
 67+        acct = xs_dict_append(acct, "following_count", ni);
 68+    }
 69+
 70     *body = xs_json_dumps(acct, 4);
 71     *ctype = "application/json";
 72     *status = HTTP_STATUS_OK;
 73@@ -1340,6 +1371,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
 74             if (!xs_match(type, POSTLIKE_OBJECT_TYPE))
 75                 continue;
 76 
 77+            if (id && is_instance_blocked(id))
 78+                continue;
 79+
 80             const char *from = NULL;
 81             if (strcmp(type, "Page") == 0)
 82                 from = xs_dict_get(msg, "audience");
 83@@ -1727,11 +1761,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 84         if (logged_in) {
 85             xs *l      = notify_list(&snac1, 0, 64);
 86             xs *out    = xs_list_new();
 87-            xs_list *p = l;
 88             const xs_dict *v;
 89             const xs_list *excl = xs_dict_get(args, "exclude_types[]");
 90+            const char *max_id = xs_dict_get(args, "max_id");
 91 
 92-            while (xs_list_iter(&p, &v)) {
 93+            xs_list_foreach(l, v) {
 94                 xs *noti = notify_get(&snac1, v);
 95 
 96                 if (noti == NULL)
 97@@ -1740,6 +1774,8 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
 98                 const char *type  = xs_dict_get(noti, "type");
 99                 const char *utype = xs_dict_get(noti, "utype");
100                 const char *objid = xs_dict_get(noti, "objid");
101+                const char *id    = xs_dict_get(noti, "id");
102+                xs *fid = xs_replace(id, ".", "");
103                 xs *actor = NULL;
104                 xs *entry = NULL;
105 
106@@ -1752,6 +1788,13 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
107                 if (is_hidden(&snac1, objid))
108                     continue;
109 
110+                if (max_id) {
111+                    if (strcmp(fid, max_id) == 0)
112+                        max_id = NULL;
113+
114+                    continue;
115+                }
116+
117                 /* convert the type */
118                 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
119                     type = "favourite";
120@@ -1778,12 +1821,15 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
121 
122                 mn = xs_dict_append(mn, "type", type);
123 
124-                xs *id = xs_replace(xs_dict_get(noti, "id"), ".", "");
125-                mn = xs_dict_append(mn, "id", id);
126+                mn = xs_dict_append(mn, "id", fid);
127 
128                 mn = xs_dict_append(mn, "created_at", xs_dict_get(noti, "date"));
129 
130                 xs *acct = mastoapi_account(&snac1, actor);
131+
132+                if (acct == NULL)
133+                    continue;
134+
135                 mn = xs_dict_append(mn, "account", acct);
136 
137                 if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
M snac.h
+13, -1
 1@@ -1,7 +1,7 @@
 2 /* snac - A simple, minimalistic ActivityPub instance */
 3 /* copyright (c) 2022 - 2024 grunfink et al. / MIT license */
 4 
 5-#define VERSION "2.63"
 6+#define VERSION "2.66-dev"
 7 
 8 #define USER_AGENT "snac/" VERSION
 9 
10@@ -141,6 +141,12 @@ int follower_del(snac *snac, const char *actor);
11 int follower_check(snac *snac, const char *actor);
12 xs_list *follower_list(snac *snac);
13 
14+int pending_add(snac *user, const char *actor, const xs_dict *msg);
15+int pending_check(snac *user, const char *actor);
16+xs_dict *pending_get(snac *user, const char *actor);
17+void pending_del(snac *user, const char *actor);
18+xs_list *pending_list(snac *user);
19+
20 double timeline_mtime(snac *snac);
21 int timeline_touch(snac *snac);
22 int timeline_here(snac *snac, const char *md5);
23@@ -296,6 +302,7 @@ const char *default_avatar_base64(void);
24 xs_str *process_tags(snac *snac, const char *content, xs_list **tag);
25 
26 const char *get_atto(const xs_dict *msg);
27+const char *get_in_reply_to(const xs_dict *msg);
28 xs_list *get_attachments(const xs_dict *msg);
29 
30 xs_dict *msg_admiration(snac *snac, const char *object, const char *type);
31@@ -313,6 +320,7 @@ xs_dict *msg_update(snac *snac, const xs_dict *object);
32 xs_dict *msg_ping(snac *user, const char *rcpt);
33 xs_dict *msg_pong(snac *user, const char *rcpt, const char *object);
34 xs_dict *msg_move(snac *user, const char *new_account);
35+xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to);
36 xs_dict *msg_question(snac *user, const char *content, xs_list *attach,
37                       const xs_list *opts, int multiple, int end_secs);
38 
39@@ -396,6 +404,10 @@ void verify_links(snac *user);
40 
41 void export_csv(snac *user);
42 int migrate_account(snac *user);
43+
44+void import_blocked_accounts_csv(snac *user, const char *fn);
45+void import_following_accounts_csv(snac *user, const char *fn);
46+void import_list_csv(snac *user, const char *fn);
47 void import_csv(snac *user);
48 
49 typedef enum {
+46, -7
 1@@ -670,20 +670,18 @@ void export_csv(snac *user)
 2 }
 3 
 4 
 5-void import_csv(snac *user)
 6-/* import CSV files from Mastodon */
 7+void import_blocked_accounts_csv(snac *user, const char *fn)
 8+/* imports a Mastodon CSV file of blocked accounts */
 9 {
10     FILE *f;
11-    const char *fn;
12 
13-    fn = "blocked_accounts.csv";
14     if ((f = fopen(fn, "r")) != NULL) {
15         snac_log(user, xs_fmt("Importing from %s...", fn));
16 
17         while (!feof(f)) {
18             xs *l = xs_strip_i(xs_readline(f));
19 
20-            if (*l) {
21+            if (*l && strchr(l, '@') != NULL) {
22                 xs *url = NULL;
23                 xs *uid = NULL;
24 
25@@ -704,8 +702,14 @@ void import_csv(snac *user)
26     }
27     else
28         snac_log(user, xs_fmt("Cannot open file %s", fn));
29+}
30+
31+
32+void import_following_accounts_csv(snac *user, const char *fn)
33+/* imports a Mastodon CSV file of accounts to follow */
34+{
35+    FILE *f;
36 
37-    fn = "following_accounts.csv";
38     if ((f = fopen(fn, "r")) != NULL) {
39         snac_log(user, xs_fmt("Importing from %s...", fn));
40 
41@@ -757,8 +761,14 @@ void import_csv(snac *user)
42     }
43     else
44         snac_log(user, xs_fmt("Cannot open file %s", fn));
45+}
46+
47+
48+void import_list_csv(snac *user, const char *fn)
49+/* imports a Mastodon CSV file list */
50+{
51+    FILE *f;
52 
53-    fn = "lists.csv";
54     if ((f = fopen(fn, "r")) != NULL) {
55         snac_log(user, xs_fmt("Importing from %s...", fn));
56 
57@@ -782,6 +792,21 @@ void import_csv(snac *user)
58 
59                         list_content(user, list_id, actor_md5, 1);
60                         snac_log(user, xs_fmt("Added %s to list %s", url, lname));
61+
62+                        if (!following_check(user, url)) {
63+                            xs *msg = msg_follow(user, url);
64+
65+                            if (msg == NULL) {
66+                                snac_log(user, xs_fmt("Cannot follow %s -- server down?", acct));
67+                                continue;
68+                            }
69+
70+                            following_add(user, url, msg);
71+
72+                            enqueue_output_by_actor(user, msg, url, 0);
73+
74+                            snac_log(user, xs_fmt("Following %s", url));
75+                        }
76                     }
77                     else
78                         snac_log(user, xs_fmt("Webfinger error while adding %s to list %s", acct, lname));
79@@ -793,6 +818,20 @@ void import_csv(snac *user)
80     }
81     else
82         snac_log(user, xs_fmt("Cannot open file %s", fn));
83+}
84+
85+
86+void import_csv(snac *user)
87+/* import CSV files from Mastodon */
88+{
89+    FILE *f;
90+    const char *fn;
91+
92+    import_blocked_accounts_csv(user, "blocked_accounts.csv");
93+
94+    import_following_accounts_csv(user, "following_accounts.csv");
95+
96+    import_list_csv(user, "lists.csv");
97 
98     fn = "bookmarks.csv";
99     if ((f = fopen(fn, "r")) != NULL) {
+24, -0
 1@@ -21,6 +21,7 @@
 2  int xs_unicode_nfd(unsigned int cpoint, unsigned int *base, unsigned int *diac);
 3  int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint);
 4  int xs_unicode_is_alpha(unsigned int cpoint);
 5+ int xs_unicode_is_right_to_left(unsigned int cpoint);
 6 
 7 #ifdef _XS_H
 8  xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset);
 9@@ -350,6 +351,29 @@ int xs_unicode_is_alpha(unsigned int cpoint)
10 }
11 
12 
13+int xs_unicode_is_right_to_left(unsigned int cpoint)
14+/* checks if a codepoint is a right-to-left letter */
15+{
16+    int b = 0;
17+    int t = xs_countof(xs_unicode_right_to_left_table) / 2 - 1;
18+
19+    while (t >= b) {
20+        int n = (b + t) / 2;
21+        unsigned int *p = &xs_unicode_right_to_left_table[n * 2];
22+
23+        if (cpoint < p[0])
24+            t = n - 1;
25+        else
26+        if (cpoint > p[1])
27+            b = n + 1;
28+        else
29+            return 1;
30+    }
31+
32+    return 0;
33+}
34+
35+
36 #ifdef _XS_H
37 
38 xs_str *xs_utf8_to_upper(const char *str)
+15, -0
 1@@ -726,5 +726,20 @@ static unsigned int xs_unicode_alpha_table[] = {
 2     0x1E7E0, 0x1E8C4, 0x1E900, 0x1E943, 0x1EE00, 0x1EEBB, 0x20000, 0x323AF, 
 3 };
 4 
 5+static unsigned int xs_unicode_right_to_left_table[] = {
 6+    0x05BE, 0x05BE, 0x05C0, 0x05C0, 0x05C3, 0x05C3, 0x05C6, 0x05C6, 
 7+    0x05D0, 0x05F4, 0x0608, 0x0608, 0x060B, 0x060B, 0x060D, 0x060D, 
 8+    0x061B, 0x064A, 0x066D, 0x066F, 0x0671, 0x06D5, 0x06E5, 0x06E6, 
 9+    0x06EE, 0x06EF, 0x06FA, 0x0710, 0x0712, 0x072F, 0x074D, 0x07A5, 
10+    0x07B1, 0x07EA, 0x07F4, 0x07F5, 0x07FA, 0x07FA, 0x07FE, 0x0815, 
11+    0x081A, 0x081A, 0x0824, 0x0824, 0x0828, 0x0828, 0x0830, 0x0858, 
12+    0x085E, 0x088E, 0x08A0, 0x08C9, 0x200F, 0x200F, 0xFB1D, 0xFB1D, 
13+    0xFB1F, 0xFB28, 0xFB2A, 0xFD3D, 0xFD50, 0xFDC7, 0xFDF0, 0xFDFC, 
14+    0xFE70, 0xFEFC, 0x10800, 0x1091B, 0x10920, 0x10A00, 0x10A10, 0x10A35, 
15+    0x10A40, 0x10AE4, 0x10AEB, 0x10B35, 0x10B40, 0x10D23, 0x10E80, 0x10EA9, 
16+    0x10EAD, 0x10EB1, 0x10F00, 0x10F45, 0x10F51, 0x10F81, 0x10F86, 0x10FF6, 
17+    0x1E800, 0x1E8CF, 0x1E900, 0x1E943, 0x1E94B, 0x1EEBB, 
18+};
19+
20 #endif /* _XS_UNICODE_TBL_H */
21 
+4, -4
 1@@ -106,13 +106,13 @@ xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *hea
 2         if (xs_list_len(l1) != 2)
 3             return NULL;
 4 
 5-        boundary = xs_dup(xs_list_get(l1, 1));
 6+        xs *t_boundary = xs_dup(xs_list_get(l1, 1));
 7 
 8         /* Tokodon sends the boundary header with double quotes surrounded */
 9-        if (xs_between("\"", boundary, "\"") != 0)
10-            boundary = xs_strip_chars_i(boundary, "\"");
11+        if (xs_between("\"", t_boundary, "\"") != 0)
12+            t_boundary = xs_strip_chars_i(t_boundary, "\"");
13 
14-        boundary = xs_fmt("--%s", boundary);
15+        boundary = xs_fmt("--%s", t_boundary);
16     }
17 
18     bsz = strlen(boundary);
+1, -1
1@@ -1 +1 @@
2-/* 35997d2dbc505320a62d3130daa95f638be8bb26 2024-11-05T16:47:36+01:00 */
3+/* 297f71e198be7819213e9122e1e78c3b963111bc 2024-11-24T18:48:42+01:00 */