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