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