snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

utils.c (28587B)


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