2.76
utils.c
1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
3
4#include "xs.h"
5#include "xs_io.h"
6#include "xs_json.h"
7#include "xs_time.h"
8#include "xs_openssl.h"
9#include "xs_random.h"
10#include "xs_glob.h"
11#include "xs_curl.h"
12#include "xs_regex.h"
13
14#include "snac.h"
15
16#include <sys/stat.h>
17#include <stdlib.h>
18
19static const char *default_srv_config = "{"
20 "\"host\": \"\","
21 "\"prefix\": \"\","
22 "\"address\": \"127.0.0.1\","
23 "\"port\": 8001,"
24 "\"layout\": 0.0,"
25 "\"dbglevel\": 0,"
26 "\"queue_retry_minutes\": 2,"
27 "\"queue_retry_max\": 10,"
28 "\"queue_timeout\": 6,"
29 "\"queue_timeout_2\": 8,"
30 "\"cssurls\": [\"\"],"
31 "\"def_timeline_entries\": 50,"
32 "\"max_timeline_entries\": 50,"
33 "\"timeline_purge_days\": 120,"
34 "\"local_purge_days\": 0,"
35 "\"min_account_age\": 0,"
36 "\"admin_email\": \"\","
37 "\"admin_account\": \"\","
38 "\"title\": \"\","
39 "\"short_description\": \"\","
40 "\"short_description_raw\": false,"
41 "\"protocol\": \"https\","
42 "\"fastcgi\": false"
43 "}";
44
45static const char *default_css =
46 "body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; }\n"
47 "pre { overflow-x: scroll; }\n"
48 ".snac-embedded-video, img { max-width: 100% }\n"
49 ".snac-origin { font-size: 85% }\n"
50 ".snac-score { float: right; font-size: 85% }\n"
51 ".snac-top-user { text-align: center; padding-bottom: 2em }\n"
52 ".snac-top-user-name { font-size: 200% }\n"
53 ".snac-top-user-id { font-size: 150% }\n"
54 ".snac-announcement { border: black 1px solid; padding: 0.5em }\n"
55 ".snac-avatar { float: left; height: 2.5em; width: 2.5em; padding: 0.25em }\n"
56 ".snac-author { font-size: 90%; text-decoration: none }\n"
57 ".snac-author-tag { font-size: 80% }\n"
58 ".snac-pubdate { color: #a0a0a0; font-size: 90% }\n"
59 ".snac-top-controls { padding-bottom: 1.5em }\n"
60 ".snac-post { border-top: 1px solid #a0a0a0; padding-top: 0.5em; padding-bottom: 0.5em; }\n"
61 ".snac-children { padding-left: 1em; border-left: 1px solid #a0a0a0; }\n"
62 ".snac-thread-cont { border-top: 1px dashed #a0a0a0; }\n"
63 ".snac-textarea { font-family: inherit; width: 100% }\n"
64 ".snac-history { border: 1px solid #606060; border-radius: 3px; margin: 2.5em 0; padding: 0 2em }\n"
65 ".snac-btn-mute { float: right; margin-left: 0.5em }\n"
66 ".snac-btn-unmute { float: right; margin-left: 0.5em }\n"
67 ".snac-btn-follow { float: right; margin-left: 0.5em }\n"
68 ".snac-btn-unfollow { float: right; margin-left: 0.5em }\n"
69 ".snac-btn-hide { float: right; margin-left: 0.5em }\n"
70 ".snac-btn-delete { float: right; margin-left: 0.5em }\n"
71 ".snac-btn-limit { float: right; margin-left: 0.5em }\n"
72 ".snac-btn-unlimit { float: right; margin-left: 0.5em }\n"
73 ".snac-footer { margin-top: 2em; font-size: 75% }\n"
74 ".snac-poll-result { margin-left: auto; margin-right: auto; }\n"
75 ".snac-list-of-lists { padding-left: 0; }\n"
76 ".snac-list-of-lists li { display: inline; border: 1px solid #a0a0a0; border-radius: 25px;\n"
77 " margin-right: 0.5em; padding-left: 0.5em; padding-right: 0.5em; }\n"
78 ".snac-no-more-unseen-posts { border-top: 1px solid #a0a0a0; border-bottom: 1px solid #a0a0a0; padding: 0.5em 0; margin: 1em 0; }\n"
79 "@media (prefers-color-scheme: dark) { \n"
80 " body, input, textarea { background-color: #000; color: #fff; }\n"
81 " a { color: #7799dd }\n"
82 " a:visited { color: #aa99dd }\n"
83 "}\n"
84;
85
86const char *snac_blurb =
87 "<p><b>%host%</b> is a <a href=\"https:/"
88 "/en.wikipedia.org/wiki/Fediverse\">Fediverse</a> "
89 "instance that uses the <a href=\"https:/"
90 "/en.wikipedia.org/wiki/ActivityPub\">ActivityPub</a> "
91 "protocol. In other words, users at this host can communicate with people "
92 "that use software like Mastodon, Pleroma, Friendica, etc. "
93 "all around the world.</p>\n"
94 "<p>This server runs the "
95 "<a href=\"" WHAT_IS_SNAC_URL "\">snac</a> software and there is no "
96 "automatic sign-up process.</p>\n"
97;
98
99static const char *greeting_html =
100 "<!DOCTYPE html>\n"
101 "<html><head>\n"
102 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n"
103 "<link rel=\"icon\" type=\"image/x-icon\" href=\"https://%host%/favicon.ico\"/>\n"
104 "<title>Welcome to %host%</title>\n</head>\n"
105 "<body style=\"margin: auto; max-width: 50em\">\n"
106 "%blurb%"
107 "<p>The following users are part of this community:</p>\n"
108 "\n"
109 "%userlist%\n"
110 "\n"
111 "<p>This site is powered by <abbr title=\"Social Networks Are Crap\">snac</abbr>.</p>\n"
112 "</body></html>\n";
113
114
115int write_default_css(void)
116{
117 FILE *f;
118
119 xs *sfn = xs_fmt("%s/style.css", srv_basedir);
120 if ((f = fopen(sfn, "w")) == NULL)
121 return 1;
122
123 fwrite(default_css, strlen(default_css), 1, f);
124 fclose(f);
125
126 return 0;
127}
128
129
130int snac_init(const char *basedir)
131{
132 FILE *f;
133
134 if (basedir == NULL) {
135 printf("Base directory: "); fflush(stdout);
136 srv_basedir = xs_strip_i(xs_readline(stdin));
137 }
138 else
139 srv_basedir = xs_str_new(basedir);
140
141 if (srv_basedir == NULL || *srv_basedir == '\0')
142 return 1;
143
144 if (xs_endswith(srv_basedir, "/"))
145 srv_basedir = xs_crop_i(srv_basedir, 0, -1);
146
147 if (mtime(srv_basedir) != 0.0) {
148 printf("ERROR: directory '%s' must not exist.\n", srv_basedir);
149 return 1;
150 }
151
152 srv_config = xs_json_loads(default_srv_config);
153
154 xs *layout = xs_number_new(disk_layout);
155 srv_config = xs_dict_set(srv_config, "layout", layout);
156
157 int is_unix_socket = 0;
158
159 printf("Network address or full path to unix socket [%s]: ", xs_dict_get(srv_config, "address")); fflush(stdout);
160 {
161 xs *i = xs_strip_i(xs_readline(stdin));
162 if (*i) {
163 srv_config = xs_dict_set(srv_config, "address", i);
164
165 if (*i == '/')
166 is_unix_socket = 1;
167 }
168 }
169
170 if (!is_unix_socket) {
171 printf("Network port [%d]: ", (int)xs_number_get(xs_dict_get(srv_config, "port"))); fflush(stdout);
172 {
173 xs *i = xs_strip_i(xs_readline(stdin));
174 if (*i) {
175 xs *n = xs_number_new(atoi(i));
176 srv_config = xs_dict_set(srv_config, "port", n);
177 }
178 }
179 }
180 else {
181 xs *n = xs_number_new(0);
182 srv_config = xs_dict_set(srv_config, "port", n);
183 }
184
185 printf("Host name: "); fflush(stdout);
186 {
187 xs *i = xs_strip_i(xs_readline(stdin));
188 if (*i == '\0')
189 return 1;
190
191 srv_config = xs_dict_set(srv_config, "host", i);
192 }
193
194 printf("URL prefix: "); fflush(stdout);
195 {
196 xs *i = xs_strip_i(xs_readline(stdin));
197
198 if (*i) {
199 if (xs_endswith(i, "/"))
200 i = xs_crop_i(i, 0, -1);
201
202 srv_config = xs_dict_set(srv_config, "prefix", i);
203 }
204 }
205
206 printf("Admin email address (optional): "); fflush(stdout);
207 {
208 xs *i = xs_strip_i(xs_readline(stdin));
209
210 srv_config = xs_dict_set(srv_config, "admin_email", i);
211 }
212
213 if (mkdirx(srv_basedir) == -1) {
214 printf("ERROR: cannot create directory '%s'\n", srv_basedir);
215 return 1;
216 }
217
218 xs *udir = xs_fmt("%s/user", srv_basedir);
219 mkdirx(udir);
220
221 xs *odir = xs_fmt("%s/object", srv_basedir);
222 mkdirx(odir);
223
224 xs *qdir = xs_fmt("%s/queue", srv_basedir);
225 mkdirx(qdir);
226
227 xs *ibdir = xs_fmt("%s/inbox", srv_basedir);
228 mkdirx(ibdir);
229
230 xs *gfn = xs_fmt("%s/greeting.html", srv_basedir);
231 if ((f = fopen(gfn, "w")) == NULL) {
232 printf("ERROR: cannot create '%s'\n", gfn);
233 return 1;
234 }
235
236 xs *gh = xs_replace(greeting_html, "%blurb%", snac_blurb);
237 fwrite(gh, strlen(gh), 1, f);
238 fclose(f);
239
240 if (write_default_css()) {
241 printf("ERROR: cannot create style.css\n");
242 return 1;
243 }
244
245 xs *cfn = xs_fmt("%s/server.json", srv_basedir);
246 if ((f = fopen(cfn, "w")) == NULL) {
247 printf("ERROR: cannot create '%s'\n", cfn);
248 return 1;
249 }
250
251 xs_json_dump(srv_config, 4, f);
252 fclose(f);
253
254 printf("Done.\n");
255 return 0;
256}
257
258
259void new_password(const char *uid, xs_str **clear_pwd, xs_str **hashed_pwd)
260/* creates a random password */
261{
262 int rndbuf[3];
263
264 xs_rnd_buf(rndbuf, sizeof(rndbuf));
265
266 *clear_pwd = xs_base64_enc((char *)rndbuf, sizeof(rndbuf));
267 *hashed_pwd = hash_password(uid, *clear_pwd, NULL);
268}
269
270
271int adduser(const char *uid)
272/* creates a new user */
273{
274 snac snac;
275 xs *config = xs_dict_new();
276 xs *date = xs_str_utctime(0, ISO_DATE_SPEC);
277 xs *pwd = NULL;
278 xs *pwd_f = NULL;
279 xs *key = NULL;
280 FILE *f;
281
282 if (uid == NULL) {
283 printf("Username: "); fflush(stdout);
284 uid = xs_strip_i(xs_readline(stdin));
285 }
286
287 if (!validate_uid(uid)) {
288 printf("ERROR: only alphanumeric characters and _ are allowed in user ids.\n");
289 return 1;
290 }
291
292 if (user_open(&snac, uid)) {
293 printf("ERROR: user '%s' already exists\n", snac.uid);
294 return 1;
295 }
296
297 new_password(uid, &pwd, &pwd_f);
298
299 config = xs_dict_append(config, "uid", uid);
300 config = xs_dict_append(config, "name", uid);
301 config = xs_dict_append(config, "avatar", "");
302 config = xs_dict_append(config, "bio", "");
303 config = xs_dict_append(config, "cw", "");
304 config = xs_dict_append(config, "published", date);
305 config = xs_dict_append(config, "passwd", pwd_f);
306
307 xs *basedir = xs_fmt("%s/user/%s", srv_basedir, uid);
308
309 if (mkdirx(basedir) == -1) {
310 printf("ERROR: cannot create directory '%s'\n", basedir);
311 return 0;
312 }
313
314 const char *dirs[] = {
315 "followers", "following", "muted", "hidden",
316 "public", "private", "queue", "history",
317 "static", NULL };
318 int n;
319
320 for (n = 0; dirs[n]; n++) {
321 xs *d = xs_fmt("%s/%s", basedir, dirs[n]);
322 mkdirx(d);
323 }
324
325 /* add a specially short data retention time for the relay */
326 if (strcmp(uid, "relay") == 0)
327 config = xs_dict_set(config, "purge_days", xs_stock(1));
328
329 xs *cfn = xs_fmt("%s/user.json", basedir);
330
331 if ((f = fopen(cfn, "w")) == NULL) {
332 printf("ERROR: cannot create '%s'\n", cfn);
333 return 1;
334 }
335 else {
336 xs_json_dump(config, 4, f);
337 fclose(f);
338 }
339
340 printf("\nCreating RSA key...\n");
341 key = xs_evp_genkey(2048);
342 printf("Done.\n");
343
344 xs *kfn = xs_fmt("%s/key.json", basedir);
345
346 if ((f = fopen(kfn, "w")) == NULL) {
347 printf("ERROR: cannot create '%s'\n", kfn);
348 return 1;
349 }
350 else {
351 xs_json_dump(key, 4, f);
352 fclose(f);
353 }
354
355 printf("\nUser password is %s\n", pwd);
356
357 printf("\nGo to %s/%s and continue configuring your user there.\n", srv_baseurl, uid);
358
359 return 0;
360}
361
362
363int resetpwd(snac *snac)
364/* creates a new password for the user */
365{
366 xs *clear_pwd = NULL;
367 xs *hashed_pwd = NULL;
368 xs *fn = xs_fmt("%s/user.json", snac->basedir);
369 FILE *f;
370 int ret = 0;
371
372 new_password(snac->uid, &clear_pwd, &hashed_pwd);
373
374 snac->config = xs_dict_set(snac->config, "passwd", hashed_pwd);
375
376 if ((f = fopen(fn, "w")) != NULL) {
377 xs_json_dump(snac->config, 4, f);
378 fclose(f);
379
380 printf("New password for user %s is %s\n", snac->uid, clear_pwd);
381 }
382 else {
383 printf("ERROR: cannot write to %s\n", fn);
384 ret = 1;
385 }
386
387 return ret;
388}
389
390
391void rm_rf(const char *dir)
392/* does an rm -rf (yes, I'm also scared) */
393{
394 xs *d = xs_str_cat(xs_dup(dir), "/" "*");
395 xs *l = xs_glob(d, 0, 0);
396 xs_list *p = l;
397 const xs_str *v;
398
399 if (dbglevel >= 1)
400 printf("Deleting directory %s\n", dir);
401
402 while (xs_list_iter(&p, &v)) {
403 struct stat st;
404
405 if (stat(v, &st) != -1) {
406 if (st.st_mode & S_IFDIR) {
407 rm_rf(v);
408 }
409 else {
410 if (dbglevel >= 1)
411 printf("Deleting file %s\n", v);
412
413 if (unlink(v) == -1)
414 printf("ERROR: cannot delete file %s\n", v);
415 }
416 }
417 else
418 printf("ERROR: stat() fail for %s\n", v);
419 }
420
421 if (rmdir(dir) == -1)
422 printf("ERROR: cannot delete directory %s\n", dir);
423}
424
425
426int deluser(snac *user)
427/* deletes a user */
428{
429 int ret = 0;
430 xs *fwers = following_list(user);
431 xs_list *p = fwers;
432 const xs_str *v;
433
434 while (xs_list_iter(&p, &v)) {
435 xs *object = NULL;
436
437 if (valid_status(following_get(user, v, &object))) {
438 xs *msg = msg_undo(user, xs_dict_get(object, "object"));
439
440 following_del(user, v);
441
442 enqueue_output_by_actor(user, msg, v, 0);
443
444 printf("Unfollowing actor %s\n", v);
445 }
446 }
447
448 rm_rf(user->basedir);
449
450 return ret;
451}
452
453
454void verify_links(snac *user)
455/* verifies a user's links */
456{
457 xs *metadata = NULL;
458 const xs_dict *md = xs_dict_get(user->config, "metadata");
459 const char *k, *v;
460 int changed = 0;
461
462 xs *headers = xs_dict_new();
463 headers = xs_dict_append(headers, "accept", "text/html");
464 headers = xs_dict_append(headers, "user-agent", USER_AGENT " (link verify)");
465
466 if (xs_type(md) == XSTYPE_DICT)
467 metadata = xs_dup(md);
468 else
469 if (xs_type(md) == XSTYPE_STRING) {
470 /* convert to dict for easier iteration */
471 metadata = xs_dict_new();
472 xs *l = xs_split(md, "\n");
473 const char *ll;
474
475 xs_list_foreach(l, ll) {
476 xs *kv = xs_split_n(ll, "=", 1);
477 const char *k = xs_list_get(kv, 0);
478 const char *v = xs_list_get(kv, 1);
479
480 if (k && v) {
481 xs *kk = xs_strip_i(xs_dup(k));
482 xs *vv = xs_strip_i(xs_dup(v));
483 metadata = xs_dict_set(metadata, kk, vv);
484 }
485 }
486 }
487
488 int c = 0;
489 while (metadata && xs_dict_next(metadata, &k, &v, &c)) {
490 /* not an https link? skip */
491 if (!xs_startswith(v, "https:/" "/"))
492 continue;
493
494 int status;
495 xs *req = NULL;
496 xs *payload = NULL;
497 int p_size = 0;
498
499 req = xs_http_request("GET", v, headers, NULL, 0, &status,
500 &payload, &p_size, 0);
501
502 if (!valid_status(status)) {
503 snac_log(user, xs_fmt("link %s verify error %d", v, status));
504 continue;
505 }
506
507 /* extract the links */
508 xs *ls = xs_regex_select(payload, "< *(a|link) +[^>]+>");
509
510 xs_list *lp = ls;
511 const char *ll;
512 int vfied = 0;
513
514 while (!vfied && xs_list_iter(&lp, &ll)) {
515 /* extract href and rel */
516 xs *r = xs_regex_select(ll, "(href|rel) *= *(\"[^\"]*\"|'[^']*')");
517
518 /* must have both attributes */
519 if (xs_list_len(r) != 2)
520 continue;
521
522 xs *href = NULL;
523 int is_rel_me = 0;
524 xs_list *pr = r;
525 const char *ar;
526
527 while (xs_list_iter(&pr, &ar)) {
528 xs *nq = xs_dup(ar);
529
530 nq = xs_replace_i(nq, "\"", "");
531 nq = xs_replace_i(nq, "'", "");
532
533 xs *r2 = xs_split_n(nq, "=", 1);
534 if (xs_list_len(r2) != 2)
535 continue;
536
537 xs *ak = xs_strip_i(xs_dup(xs_list_get(r2, 0)));
538 xs *av = xs_strip_i(xs_dup(xs_list_get(r2, 1)));
539
540 if (strcmp(ak, "href") == 0)
541 href = xs_dup(av);
542 else
543 if (strcmp(ak, "rel") == 0) {
544 /* split the value by spaces */
545 xs *vbs = xs_split(av, " ");
546
547 /* is any of it "me"? */
548 if (xs_list_in(vbs, "me") != -1)
549 is_rel_me = 1;
550 }
551 }
552
553 /* after all this acrobatics, do we have an href and a rel="me"? */
554 if (href != NULL && is_rel_me) {
555 /* is it the same as the actor? */
556 if (strcmp(href, user->actor) == 0) {
557 /* got it! */
558 xs *verified_time = xs_number_new((double)time(NULL));
559
560 if (user->links == NULL)
561 user->links = xs_dict_new();
562
563 user->links = xs_dict_set(user->links, v, verified_time);
564
565 vfied = 1;
566 }
567 else
568 snac_debug(user, 1,
569 xs_fmt("verify link %s rel='me' found but not related (%s)", v, href));
570 }
571 }
572
573 if (vfied) {
574 changed++;
575 snac_log(user, xs_fmt("link %s verified", v));
576 }
577 else {
578 snac_log(user, xs_fmt("link %s not verified (rel='me' not found)", v));
579 }
580 }
581
582 if (changed) {
583 FILE *f;
584
585 /* update the links.json file */
586 xs *fn = xs_fmt("%s/links.json", user->basedir);
587 xs *bfn = xs_fmt("%s.bak", fn);
588
589 rename(fn, bfn);
590
591 if ((f = fopen(fn, "w")) != NULL) {
592 xs_json_dump(user->links, 4, f);
593 fclose(f);
594 }
595 else
596 rename(bfn, fn);
597 }
598}
599
600
601void export_csv(snac *user)
602/* exports user data to current directory in a way that pleases Mastodon */
603{
604 FILE *f;
605 xs *fn = NULL;
606
607 fn = xs_fmt("%s/export/bookmarks.csv", user->basedir);
608 if ((f = fopen(fn, "w")) != NULL) {
609 snac_log(user, xs_fmt("Creating %s...", fn));
610
611 xs *l = bookmark_list(user);
612 const char *md5;
613
614 xs_list_foreach(l, md5) {
615 xs *post = NULL;
616
617 if (valid_status(object_get_by_md5(md5, &post))) {
618 const char *id = xs_dict_get(post, "id");
619
620 if (xs_type(id) == XSTYPE_STRING)
621 fprintf(f, "%s\n", id);
622 }
623 }
624
625 fclose(f);
626 }
627 else
628 snac_log(user, xs_fmt("Cannot create file %s", fn));
629
630 xs_free(fn);
631 fn = xs_fmt("%s/export/blocked_accounts.csv", user->basedir);
632 if ((f = fopen(fn, "w")) != NULL) {
633 snac_log(user, xs_fmt("Creating %s...", fn));
634
635 xs *l = muted_list(user);
636 const char *actor;
637
638 xs_list_foreach(l, actor) {
639 xs *uid = NULL;
640
641 webfinger_request_fake(actor, NULL, &uid);
642 fprintf(f, "%s\n", uid);
643 }
644
645 fclose(f);
646 }
647 else
648 snac_log(user, xs_fmt("Cannot create file %s", fn));
649
650 xs_free(fn);
651 fn = xs_fmt("%s/export/lists.csv", user->basedir);
652 if ((f = fopen(fn, "w")) != NULL) {
653 snac_log(user, xs_fmt("Creating %s...", fn));
654
655 xs *lol = list_maint(user, NULL, 0);
656 const xs_list *li;
657
658 xs_list_foreach(lol, li) {
659 const char *lid = xs_list_get(li, 0);
660 const char *ltitle = xs_list_get(li, 1);
661
662 xs *actors = list_content(user, lid, NULL, 0);
663 const char *md5;
664
665 xs_list_foreach(actors, md5) {
666 xs *actor = NULL;
667
668 if (valid_status(object_get_by_md5(md5, &actor))) {
669 const char *id = xs_dict_get(actor, "id");
670 xs *uid = NULL;
671
672 webfinger_request_fake(id, NULL, &uid);
673 fprintf(f, "%s,%s\n", ltitle, uid);
674 }
675 }
676 }
677
678 fclose(f);
679 }
680 else
681 snac_log(user, xs_fmt("Cannot create file %s", fn));
682
683 xs_free(fn);
684 fn = xs_fmt("%s/export/following_accounts.csv", user->basedir);
685 if ((f = fopen(fn, "w")) != NULL) {
686 snac_log(user, xs_fmt("Creating %s...", fn));
687
688 fprintf(f, "Account address,Show boosts,Notify on new posts,Languages\n");
689
690 xs *fwing = following_list(user);
691 const char *actor;
692
693 xs_list_foreach(fwing, actor) {
694 xs *uid = NULL;
695
696 webfinger_request_fake(actor, NULL, &uid);
697 fprintf(f, "%s,%s,false,\n", uid, limited(user, actor, 0) ? "false" : "true");
698 }
699
700 fclose(f);
701 }
702 else
703 snac_log(user, xs_fmt("Cannot create file %s", fn));
704}
705
706
707void import_blocked_accounts_csv(snac *user, const char *ifn)
708/* imports a Mastodon CSV file of blocked accounts */
709{
710 FILE *f;
711 xs *l = xs_split(ifn, "/");
712 xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1));
713
714 if ((f = fopen(fn, "r")) != NULL) {
715 snac_log(user, xs_fmt("Importing from %s...", fn));
716
717 while (!feof(f)) {
718 xs *l = xs_strip_i(xs_readline(f));
719
720 if (*l && strchr(l, '@') != NULL) {
721 xs *url = NULL;
722 xs *uid = NULL;
723
724 if (valid_status(webfinger_request(l, &url, &uid))) {
725 if (is_muted(user, url))
726 snac_log(user, xs_fmt("Actor %s already MUTEd", url));
727 else {
728 mute(user, url);
729 snac_log(user, xs_fmt("MUTEd actor %s", url));
730 }
731 }
732 else
733 snac_log(user, xs_fmt("Webfinger error for account %s", l));
734 }
735 }
736
737 fclose(f);
738 }
739 else
740 snac_log(user, xs_fmt("Cannot open file %s", fn));
741}
742
743
744void import_following_accounts_csv(snac *user, const char *ifn)
745/* imports a Mastodon CSV file of accounts to follow */
746{
747 FILE *f;
748 xs *l = xs_split(ifn, "/");
749 xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1));
750
751 if ((f = fopen(fn, "r")) != NULL) {
752 snac_log(user, xs_fmt("Importing from %s...", fn));
753
754 while (!feof(f)) {
755 xs *l = xs_strip_i(xs_readline(f));
756
757 if (*l) {
758 xs *l2 = xs_split(l, ",");
759 const char *acct = xs_list_get(l2, 0);
760 const char *show = xs_list_get(l2, 1);
761
762 if (acct) {
763 /* not a valid account? skip (probably the CSV header) */
764 if (strchr(acct, '@') == NULL)
765 continue;
766
767 xs *msg = msg_follow(user, acct);
768
769 if (msg == NULL) {
770 snac_log(user, xs_fmt("Cannot follow %s -- server down?", acct));
771 continue;
772 }
773
774 const char *actor = xs_dict_get(msg, "object");
775
776 if (following_check(user, actor))
777 snac_log(user, xs_fmt("Actor %s already followed", actor));
778 else {
779 following_add(user, actor, msg);
780
781 enqueue_output_by_actor(user, msg, actor, 0);
782
783 snac_log(user, xs_fmt("Following %s", actor));
784 }
785
786 if (show && strcmp(show, "false") == 0) {
787 limit(user, actor);
788 snac_log(user, xs_fmt("Limiting boosts from actor %s", actor));
789 }
790 else {
791 unlimit(user, actor);
792 snac_log(user, xs_fmt("Unlimiting boosts from actor %s", actor));
793 }
794 }
795 }
796 }
797
798 fclose(f);
799 }
800 else
801 snac_log(user, xs_fmt("Cannot open file %s", fn));
802}
803
804
805void import_list_csv(snac *user, const char *ifn)
806/* imports a Mastodon CSV file list */
807{
808 FILE *f;
809 xs *l = xs_split(ifn, "/");
810 xs *fn = xs_fmt("%s/import/%s", user->basedir, xs_list_get(l, -1));
811
812 if ((f = fopen(fn, "r")) != NULL) {
813 snac_log(user, xs_fmt("Importing from %s...", fn));
814
815 while (!feof(f)) {
816 xs *l = xs_strip_i(xs_readline(f));
817
818 if (*l) {
819 xs *l2 = xs_split(l, ",");
820 const char *lname = xs_list_get(l2, 0);
821 const char *acct = xs_list_get(l2, 1);
822
823 if (lname && acct) {
824 /* create the list */
825 xs *list_id = list_maint(user, lname, 1);
826
827 xs *url = NULL;
828 xs *uid = NULL;
829
830 if (valid_status(webfinger_request(acct, &url, &uid))) {
831 xs *actor_md5 = xs_md5_hex(url, strlen(url));
832
833 list_content(user, list_id, actor_md5, 1);
834 snac_log(user, xs_fmt("Added %s to list %s", url, lname));
835
836 if (!following_check(user, url)) {
837 xs *msg = msg_follow(user, url);
838
839 if (msg == NULL) {
840 snac_log(user, xs_fmt("Cannot follow %s -- server down?", acct));
841 continue;
842 }
843
844 following_add(user, url, msg);
845
846 enqueue_output_by_actor(user, msg, url, 0);
847
848 snac_log(user, xs_fmt("Following %s", url));
849 }
850 }
851 else
852 snac_log(user, xs_fmt("Webfinger error while adding %s to list %s", acct, lname));
853 }
854 }
855 }
856
857 fclose(f);
858 }
859 else
860 snac_log(user, xs_fmt("Cannot open file %s", fn));
861}
862
863
864void import_csv(snac *user)
865/* import CSV files from Mastodon */
866{
867 FILE *f;
868
869 import_blocked_accounts_csv(user, "blocked_accounts.csv");
870
871 import_following_accounts_csv(user, "following_accounts.csv");
872
873 import_list_csv(user, "lists.csv");
874
875 xs *fn = xs_fmt("%s/import/bookmarks.csv", user->basedir);
876 if ((f = fopen(fn, "r")) != NULL) {
877 snac_log(user, xs_fmt("Importing from %s...", fn));
878
879 while (!feof(f)) {
880 xs *l = xs_strip_i(xs_readline(f));
881
882 if (*l) {
883 xs *post = NULL;
884
885 if (!valid_status(object_get(l, &post))) {
886 if (!valid_status(activitypub_request(user, l, &post))) {
887 snac_log(user, xs_fmt("Error getting object %s for bookmarking", l));
888 continue;
889 }
890 }
891
892 if (post == NULL)
893 continue;
894
895 /* request the actor that created the post */
896 const char *actor = get_atto(post);
897
898 if (xs_type(actor) == XSTYPE_STRING)
899 actor_request(user, actor, NULL);
900
901 object_add_ow(l, post);
902 timeline_add(user, l, post);
903
904 bookmark(user, l);
905
906 snac_log(user, xs_fmt("Bookmarked %s", l));
907 }
908 }
909
910 fclose(f);
911 }
912 else
913 snac_log(user, xs_fmt("Cannot open file %s", fn));
914}
915
916static const struct {
917 const char *proto;
918 unsigned short default_port;
919} FALLBACK_PORTS[] = {
920 /* caution: https > http, smpts > smtp */
921 {"https", 443},
922 {"http", 80},
923 {"smtps", 465},
924 {"smtp", 25}
925};
926
927int parse_port(const char *url, const char **errstr)
928{
929 const char *col, *rcol;
930 int tmp, ret = -1;
931
932 if (errstr)
933 *errstr = NULL;
934
935 if (!(col = strchr(url, ':'))) {
936 if (errstr)
937 *errstr = "bad url";
938
939 return -1;
940 }
941
942 for (size_t i = 0; i < sizeof(FALLBACK_PORTS) / sizeof(*FALLBACK_PORTS); ++i) {
943 if (memcmp(url, FALLBACK_PORTS[i].proto, strlen(FALLBACK_PORTS[i].proto)) == 0) {
944 ret = FALLBACK_PORTS[i].default_port;
945 break;
946 }
947 }
948
949 if (!(rcol = strchr(col + 1, ':')))
950 rcol = col;
951
952 if (rcol) {
953 tmp = atoi(rcol + 1);
954 if (tmp == 0) {
955 if (ret != -1)
956 return ret;
957
958 if (errstr)
959 *errstr = strerror(errno);
960
961 return -1;
962 }
963
964 return tmp;
965 }
966
967 if (errstr)
968 *errstr = "unknown protocol";
969
970 return -1;
971}