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