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