diff options
| -rw-r--r-- | RELEASE_NOTES.md | 16 | ||||
| -rw-r--r-- | activitypub.c | 101 | ||||
| -rw-r--r-- | data.c | 119 | ||||
| -rw-r--r-- | doc/snac.1 | 36 | ||||
| -rw-r--r-- | html.c | 230 | ||||
| -rw-r--r-- | main.c | 16 | ||||
| -rw-r--r-- | mastoapi.c | 25 | ||||
| -rw-r--r-- | snac.h | 13 | ||||
| -rw-r--r-- | utils.c | 53 | ||||
| -rw-r--r-- | xs_url.h | 8 | ||||
| -rw-r--r-- | xs_version.h | 2 |
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 | |||
| 5 | 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. | ||
| 6 | |||
| 7 | Some fixes to blocked instances code (posts from them were sometimes shown). | ||
| 8 | |||
| 9 | ## 2.65 | ||
| 10 | |||
| 11 | Added a new user option to disable automatic follow confirmations (follow requests must be manually approved from the people page). | ||
| 12 | |||
| 13 | The search box also searches for accounts (via webfinger). | ||
| 14 | |||
| 15 | 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). | ||
| 16 | |||
| 17 | New 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 | ||
| 5 | Some tweaks for better integration with https://bsky.brid.gy (the BlueSky bridge by brid.gy). | 21 | Some 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 | ||
| 1041 | xs_dict *msg_collection(snac *snac, const char *id) | 1041 | xs_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/")) { |
| @@ -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 | |||
| 1173 | int 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 | |||
| 1193 | int 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 | |||
| 1203 | xs_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 | |||
| 1220 | void 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 | |||
| 1230 | xs_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 | ||
| 1144 | double timeline_mtime(snac *snac) | 1263 | double timeline_mtime(snac *snac) |
| @@ -129,6 +129,28 @@ Just what it says in the tin. This is to mitigate spammers | |||
| 129 | coming from Fediverse instances with lax / open registration | 129 | coming from Fediverse instances with lax / open registration |
| 130 | processes. Please take note that this also avoids possibly | 130 | processes. Please take note that this also avoids possibly |
| 131 | legitimate people trying to contact you. | 131 | legitimate people trying to contact you. |
| 132 | .It This account is a bot | ||
| 133 | Set this checkbox if this account behaves like a bot (i.e. | ||
| 134 | posts are automatically generated). | ||
| 135 | .It Auto-boost all mentions to this account | ||
| 136 | If this toggle is set, all mentions to this account are boosted | ||
| 137 | to all followers. This can be used to create groups. | ||
| 138 | .It This account is private | ||
| 139 | If this toggle is set, posts are not published via the public | ||
| 140 | web interface, only via the ActivityPub protocol. | ||
| 141 | .It Collapse top threads by default | ||
| 142 | If this toggle is set, the private timeline will always show | ||
| 143 | conversations collapsed by default. This allows easier navigation | ||
| 144 | through long threads. | ||
| 145 | .It Follow requests must be approved | ||
| 146 | If this toggle is set, follow requests are not automatically | ||
| 147 | accepted, but notified and stored for later review. Pending | ||
| 148 | follow requests will be shown in the people page to be | ||
| 149 | approved or discarded. | ||
| 150 | .It Publish follower and following metrics | ||
| 151 | If this toggle is set, the number of followers and following | ||
| 152 | accounts are made public (this is only the number; the specific | ||
| 153 | lists of accounts are never published). | ||
| 132 | .It Password | 154 | .It Password |
| 133 | Write the same string in these two fields to change your | 155 | Write the same string in these two fields to change your |
| 134 | password. Don't write anything if you don't want to do this. | 156 | password. Don't write anything if you don't want to do this. |
| @@ -262,6 +284,13 @@ section 'Migrating from snac to Mastodon'). | |||
| 262 | Starts a migration from this account to the one set as an alias (see | 284 | Starts a migration from this account to the one set as an alias (see |
| 263 | .Xr snac 8 , | 285 | .Xr snac 8 , |
| 264 | section 'Migrating from snac to Mastodon'). | 286 | section 'Migrating from snac to Mastodon'). |
| 287 | .It Cm import_csv Ar basedir Ar uid | ||
| 288 | Imports CSV data files from a Mastodon export. This command expects the | ||
| 289 | following 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 |
| 266 | Dumps the current state of the server and its threads. For example: | 295 | Dumps 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 | |||
| 284 | for a job to be assigned), input or output (processing I/O packets) | 313 | for a job to be assigned), input or output (processing I/O packets) |
| 285 | or stopped (not running, only to be seen while starting or stopping | 314 | or stopped (not running, only to be seen while starting or stopping |
| 286 | the server). | 315 | the server). |
| 316 | .It Cm import_list Ar basedir Ar uid Ar file | ||
| 317 | Imports a Mastodon list in CSV format. This option can be used to | ||
| 318 | import "Mastodon Follow Packs". | ||
| 319 | .It Cm import_block_list Ar basedir Ar uid Ar file | ||
| 320 | Imports 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 |
| 289 | See | 323 | See |
| @@ -349,4 +383,4 @@ See the LICENSE file for details. | |||
| 349 | .Sh CAVEATS | 383 | .Sh CAVEATS |
| 350 | Use the Fediverse sparingly. Don't fear the MUTE button. | 384 | Use the Fediverse sparingly. Don't fear the MUTE button. |
| 351 | .Sh BUGS | 385 | .Sh BUGS |
| 352 | Probably plenty. Some issues may be even documented in the TODO.md file. | 386 | Probably many. Some issues may be even documented in the TODO.md file. |
| @@ -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 }; |
| @@ -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; |
| @@ -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"); |
| @@ -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); | |||
| 143 | int follower_check(snac *snac, const char *actor); | 143 | int follower_check(snac *snac, const char *actor); |
| 144 | xs_list *follower_list(snac *snac); | 144 | xs_list *follower_list(snac *snac); |
| 145 | 145 | ||
| 146 | int pending_add(snac *user, const char *actor, const xs_dict *msg); | ||
| 147 | int pending_check(snac *user, const char *actor); | ||
| 148 | xs_dict *pending_get(snac *user, const char *actor); | ||
| 149 | void pending_del(snac *user, const char *actor); | ||
| 150 | xs_list *pending_list(snac *user); | ||
| 151 | |||
| 146 | double timeline_mtime(snac *snac); | 152 | double timeline_mtime(snac *snac); |
| 147 | int timeline_touch(snac *snac); | 153 | int timeline_touch(snac *snac); |
| 148 | int timeline_here(snac *snac, const char *md5); | 154 | int timeline_here(snac *snac, const char *md5); |
| @@ -316,6 +322,7 @@ xs_dict *msg_update(snac *snac, const xs_dict *object); | |||
| 316 | xs_dict *msg_ping(snac *user, const char *rcpt); | 322 | xs_dict *msg_ping(snac *user, const char *rcpt); |
| 317 | xs_dict *msg_pong(snac *user, const char *rcpt, const char *object); | 323 | xs_dict *msg_pong(snac *user, const char *rcpt, const char *object); |
| 318 | xs_dict *msg_move(snac *user, const char *new_account); | 324 | xs_dict *msg_move(snac *user, const char *new_account); |
| 325 | xs_dict *msg_accept(snac *snac, const xs_val *object, const char *to); | ||
| 319 | xs_dict *msg_question(snac *user, const char *content, xs_list *attach, | 326 | xs_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 | ||
| 400 | void export_csv(snac *user); | 407 | void export_csv(snac *user); |
| 401 | int migrate_account(snac *user); | 408 | int migrate_account(snac *user); |
| 409 | |||
| 410 | void import_blocked_accounts_csv(snac *user, const char *fn); | ||
| 411 | void import_following_accounts_csv(snac *user, const char *fn); | ||
| 412 | void import_list_csv(snac *user, const char *fn); | ||
| 402 | void import_csv(snac *user); | 413 | void import_csv(snac *user); |
| 403 | 414 | ||
| 404 | typedef enum { | 415 | typedef enum { |
| @@ -670,20 +670,18 @@ void export_csv(snac *user) | |||
| 670 | } | 670 | } |
| 671 | 671 | ||
| 672 | 672 | ||
| 673 | void import_csv(snac *user) | 673 | void 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 | |||
| 708 | void 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 | |||
| 767 | void 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 | |||
| 824 | void 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) { |
| @@ -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 */ | ||