2.54 main.c
  1/* snac - A simple, minimalistic ActivityPub instance */
  2/* copyright (c) 2022 - 2024 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
 10#include "snac.h"
 11
 12#include <sys/stat.h>
 13
 14int usage(void)
 15{
 16    printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n");
 17    printf("Copyright (c) 2022 - 2024 grunfink et al. / MIT license\n");
 18    printf("\n");
 19    printf("Commands:\n");
 20    printf("\n");
 21    printf("init [{basedir}]                     Initializes the data storage\n");
 22    printf("upgrade {basedir}                    Upgrade to a new version\n");
 23    printf("adduser {basedir} [{uid}]            Adds a new user\n");
 24    printf("deluser {basedir} {uid}              Deletes a user\n");
 25    printf("httpd {basedir}                      Starts the HTTPD daemon\n");
 26    printf("purge {basedir}                      Purges old data\n");
 27    printf("state {basedir}                      Prints server state\n");
 28    printf("webfinger {basedir} {actor}          Queries about an actor (@user@host or actor url)\n");
 29    printf("queue {basedir} {uid}                Processes a user queue\n");
 30    printf("follow {basedir} {uid} {actor}       Follows an actor\n");
 31    printf("unfollow {basedir} {uid} {actor}     Unfollows an actor\n");
 32    printf("request {basedir} {uid} {url}        Requests an object\n");
 33    printf("actor {basedir} [{uid}] {url}        Requests an actor\n");
 34    printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n");
 35    printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n");
 36    printf("unboost {basedir} {uid} {url}        Unboosts a post\n");
 37    printf("resetpwd {basedir} {uid}             Resets the password of a user\n");
 38    printf("ping {basedir} {uid} {actor}         Pings an actor\n");
 39    printf("webfinger_s {basedir} {uid} {actor}  Queries about an actor (@user@host or actor url)\n");
 40    printf("pin {basedir} {uid} {msg_url}        Pins a message\n");
 41    printf("unpin {basedir} {uid} {msg_url}      Unpins a message\n");
 42    printf("block {basedir} {instance_url}       Blocks a full instance\n");
 43    printf("unblock {basedir} {instance_url}     Unblocks a full instance\n");
 44    printf("limit {basedir} {uid} {actor}        Limits an actor (drops their announces)\n");
 45    printf("unlimit {basedir} {uid} {actor}      Unlimits an actor\n");
 46    printf("verify_links {basedir} {uid}         Verifies a user's links (in the metadata)\n");
 47    printf("search {basedir} {uid} {regex}       Searches posts by content\n");
 48
 49    return 1;
 50}
 51
 52
 53char *get_argv(int *argi, int argc, char *argv[])
 54{
 55    if (*argi < argc)
 56        return argv[(*argi)++];
 57    else
 58        return NULL;
 59}
 60
 61
 62#define GET_ARGV() get_argv(&argi, argc, argv)
 63
 64int main(int argc, char *argv[])
 65{
 66    char *cmd;
 67    char *basedir;
 68    char *user;
 69    char *url;
 70    int argi = 1;
 71    snac snac;
 72
 73    /* ensure group has write access */
 74    umask(0007);
 75
 76    if ((cmd = GET_ARGV()) == NULL)
 77        return usage();
 78
 79    if (strcmp(cmd, "init") == 0) { /** **/
 80        /* initialize the data storage */
 81        /* ... */
 82        basedir = GET_ARGV();
 83
 84        return snac_init(basedir);
 85    }
 86
 87    if (strcmp(cmd, "upgrade") == 0) { /** **/
 88        int ret;
 89
 90        /* upgrade */
 91        if ((basedir = GET_ARGV()) == NULL)
 92            return usage();
 93
 94        if ((ret = srv_open(basedir, 1)) == 1)
 95            srv_log(xs_dup("OK"));
 96
 97        return ret;
 98    }
 99
100    if (strcmp(cmd, "markdown") == 0) { /** **/
101        /* undocumented, for testing only */
102        xs *c = xs_readall(stdin);
103        xs *fc = not_really_markdown(c, NULL, NULL);
104
105        printf("<html>\n%s\n</html>\n", fc);
106        return 0;
107    }
108
109    if ((basedir = GET_ARGV()) == NULL)
110        return usage();
111
112    if (!srv_open(basedir, 0)) {
113        srv_log(xs_fmt("error opening data storage at %s", basedir));
114        return 1;
115    }
116
117    if (strcmp(cmd, "adduser") == 0) { /** **/
118        user = GET_ARGV();
119
120        return adduser(user);
121
122        return 0;
123    }
124
125    if (strcmp(cmd, "httpd") == 0) { /** **/
126        httpd();
127        srv_free();
128        return 0;
129    }
130
131    if (strcmp(cmd, "purge") == 0) { /** **/
132        purge_all();
133        return 0;
134    }
135
136    if (strcmp(cmd, "state") == 0) { /** **/
137        xs *shm_name = NULL;
138        srv_state *p_state = srv_state_op(&shm_name, 1);
139
140        if (p_state == NULL)
141            return 1;
142
143        srv_state ss = *p_state;
144        int n;
145
146        printf("server: %s (%s)\n", xs_dict_get(srv_config, "host"), USER_AGENT);
147        xs *uptime = xs_str_time_diff(time(NULL) - ss.srv_start_time);
148        printf("uptime: %s\n", uptime);
149        printf("job fifo size (cur): %d\n", ss.job_fifo_size);
150        printf("job fifo size (peak): %d\n", ss.peak_job_fifo_size);
151        char *th_states[] = { "stopped", "waiting", "input", "output" };
152
153        for (n = 0; n < ss.n_threads; n++)
154            printf("thread #%d state: %s\n", n, th_states[ss.th_state[n]]);
155
156        return 0;
157    }
158
159    if ((user = GET_ARGV()) == NULL)
160        return usage();
161
162    if (strcmp(cmd, "block") == 0) { /** **/
163        int ret = instance_block(user);
164
165        if (ret < 0) {
166            fprintf(stderr, "Error blocking instance %s: %d\n", user, ret);
167            return 1;
168        }
169
170        return 0;
171    }
172
173    if (strcmp(cmd, "unblock") == 0) { /** **/
174        int ret = instance_unblock(user);
175
176        if (ret < 0) {
177            fprintf(stderr, "Error unblocking instance %s: %d\n", user, ret);
178            return 1;
179        }
180
181        return 0;
182    }
183
184    if (strcmp(cmd, "webfinger") == 0) { /** **/
185        xs *actor = NULL;
186        xs *uid = NULL;
187        int status;
188
189        status = webfinger_request(user, &actor, &uid);
190
191        printf("status: %d\n", status);
192        if (actor != NULL)
193            printf("actor: %s\n", actor);
194        if (uid != NULL)
195            printf("uid: %s\n", uid);
196
197        return 0;
198    }
199
200    if (argi == argc && strcmp(cmd, "actor") == 0) { /** **/
201        /* query an actor without user (non-signed) */
202        xs *actor = NULL;
203        int status;
204
205        status = actor_request(NULL, user, &actor);
206
207        printf("status: %d\n", status);
208        if (valid_status(status)) {
209            xs_json_dump(actor, 4, stdout);
210            printf("\n");
211        }
212
213        return 0;
214    }
215
216    if (!user_open(&snac, user)) {
217        printf("invalid user '%s'\n", user);
218        return 1;
219    }
220
221    lastlog_write(&snac, "cmdline");
222
223    if (strcmp(cmd, "resetpwd") == 0) { /** **/
224        return resetpwd(&snac);
225    }
226
227    if (strcmp(cmd, "deluser") == 0) { /** **/
228        return deluser(&snac);
229    }
230
231    if (strcmp(cmd, "update") == 0) { /** **/
232        xs *a_msg = msg_actor(&snac);
233        xs *u_msg = msg_update(&snac, a_msg);
234        enqueue_message(&snac, u_msg);
235        return 0;
236    }
237
238    if (strcmp(cmd, "queue") == 0) { /** **/
239        process_user_queue(&snac);
240        return 0;
241    }
242
243    if (strcmp(cmd, "verify_links") == 0) { /** **/
244        verify_links(&snac);
245        return 0;
246    }
247
248    if (strcmp(cmd, "timeline") == 0) { /** **/
249#if 0
250        xs *list = local_list(&snac, XS_ALL);
251        xs *body = html_timeline(&snac, list, 1);
252
253        printf("%s\n", body);
254        user_free(&snac);
255        srv_free();
256#endif
257
258        xs *idx  = xs_fmt("%s/private.idx", snac.basedir);
259        xs *list = index_list_desc(idx, 0, 256);
260        xs *tl   = timeline_top_level(&snac, list);
261
262        xs_json_dump(tl, 4, stdout);
263
264        return 0;
265    }
266
267    if ((url = GET_ARGV()) == NULL)
268        return usage();
269
270    if (strcmp(cmd, "webfinger_s") == 0) { /** **/
271        xs *actor = NULL;
272        xs *uid = NULL;
273        int status;
274
275        status = webfinger_request_signed(&snac, url, &actor, &uid);
276
277        printf("status: %d\n", status);
278        if (actor != NULL)
279            printf("actor: %s\n", actor);
280        if (uid != NULL)
281            printf("uid: %s\n", uid);
282
283        return 0;
284    }
285
286    if (strcmp(cmd, "boost") == 0 || strcmp(cmd, "announce") == 0) { /** **/
287        xs *msg = msg_admiration(&snac, url, "Announce");
288
289        if (msg != NULL) {
290            enqueue_message(&snac, msg);
291
292            if (dbglevel) {
293                xs_json_dump(msg, 4, stdout);
294            }
295        }
296
297        return 0;
298    }
299
300    if (strcmp(cmd, "unboost") == 0) { /** **/
301        xs *msg = msg_repulsion(&snac, url, "Announce");
302
303        if (msg != NULL) {
304            enqueue_message(&snac, msg);
305
306            if (dbglevel) {
307                xs_json_dump(msg, 4, stdout);
308            }
309        }
310
311        return 0;
312    }
313
314    if (strcmp(cmd, "follow") == 0) { /** **/
315        xs *msg = msg_follow(&snac, url);
316
317        if (msg != NULL) {
318            const char *actor = xs_dict_get(msg, "object");
319
320            following_add(&snac, actor, msg);
321
322            enqueue_output_by_actor(&snac, msg, actor, 0);
323
324            if (dbglevel) {
325                xs_json_dump(msg, 4, stdout);
326            }
327        }
328
329        return 0;
330    }
331
332    if (strcmp(cmd, "unfollow") == 0) { /** **/
333        xs *object = NULL;
334
335        if (valid_status(following_get(&snac, url, &object))) {
336            xs *msg = msg_undo(&snac, xs_dict_get(object, "object"));
337
338            following_del(&snac, url);
339
340            enqueue_output_by_actor(&snac, msg, url, 0);
341
342            snac_log(&snac, xs_fmt("unfollowed actor %s", url));
343        }
344        else
345            snac_log(&snac, xs_fmt("actor is not being followed %s", url));
346
347        return 0;
348    }
349
350    if (strcmp(cmd, "limit") == 0) { /** **/
351        int ret;
352
353        if (!following_check(&snac, url))
354            snac_log(&snac, xs_fmt("actor %s is not being followed", url));
355        else
356        if ((ret = limit(&snac, url)) == 0)
357            snac_log(&snac, xs_fmt("actor %s is now limited", url));
358        else
359            snac_log(&snac, xs_fmt("error limiting actor %s (%d)", url, ret));
360
361        return 0;
362    }
363
364    if (strcmp(cmd, "unlimit") == 0) { /** **/
365        int ret;
366
367        if (!following_check(&snac, url))
368            snac_log(&snac, xs_fmt("actor %s is not being followed", url));
369        else
370        if ((ret = unlimit(&snac, url)) == 0)
371            snac_log(&snac, xs_fmt("actor %s is no longer limited", url));
372        else
373            snac_log(&snac, xs_fmt("error unlimiting actor %s (%d)", url, ret));
374
375        return 0;
376    }
377
378    if (strcmp(cmd, "search") == 0) { /** **/
379        int to;
380
381        /* 'url' contains the regex */
382        xs *r = content_search(&snac, url, 1, 0, XS_ALL, 10, &to);
383
384        int c = 0;
385        const char *v;
386
387        /* print results as standalone links */
388        while (xs_list_next(r, &v, &c)) {
389            printf("%s/admin/p/%s\n", snac.actor, v);
390        }
391
392        return 0;
393    }
394
395    if (strcmp(cmd, "ping") == 0) { /** **/
396        xs *actor_o = NULL;
397
398        if (!xs_startswith(url, "https:/")) {
399            /* try to resolve via webfinger */
400            if (!valid_status(webfinger_request(url, &url, NULL))) {
401                srv_log(xs_fmt("cannot resolve %s via webfinger", url));
402                return 1;
403            }
404        }
405
406        if (valid_status(actor_request(&snac, url, &actor_o))) {
407            xs *msg = msg_ping(&snac, url);
408
409            enqueue_output_by_actor(&snac, msg, url, 0);
410
411            if (dbglevel) {
412                xs_json_dump(msg, 4, stdout);
413            }
414
415            srv_log(xs_fmt("Ping sent to %s -- see log for Pong reply", url));
416        }
417        else {
418            srv_log(xs_fmt("Error getting actor %s", url));
419            return 1;
420        }
421
422        return 0;
423    }
424
425    if (strcmp(cmd, "pin") == 0) { /** **/
426        int ret = pin(&snac, url);
427        if (ret < 0) {
428            fprintf(stderr, "error pinning %s %d\n", url, ret);
429            return 1;
430        }
431
432        return 0;
433    }
434
435    if (strcmp(cmd, "unpin") == 0) { /** **/
436        int ret = unpin(&snac, url);
437        if (ret < 0) {
438            fprintf(stderr, "error unpinning %s %d\n", url, ret);
439            return 1;
440        }
441
442        return 0;
443    }
444
445    if (strcmp(cmd, "question") == 0) { /** **/
446        int end_secs = 5 * 60;
447        xs *opts = xs_split(url, ";");
448
449        xs *msg = msg_question(&snac, "Poll", NULL, opts, 0, end_secs);
450        xs *c_msg = msg_create(&snac, msg);
451
452        if (dbglevel) {
453            xs_json_dump(c_msg, 4, stdout);
454        }
455
456        enqueue_message(&snac, c_msg);
457        enqueue_close_question(&snac, xs_dict_get(msg, "id"), end_secs);
458
459        timeline_add(&snac, xs_dict_get(msg, "id"), msg);
460
461        return 0;
462    }
463
464    if (strcmp(cmd, "request") == 0) { /** **/
465        int status;
466        xs *data = NULL;
467
468        status = activitypub_request(&snac, url, &data);
469
470        printf("status: %d\n", status);
471
472        if (data != NULL) {
473            xs_json_dump(data, 4, stdout);
474        }
475
476        return 0;
477    }
478
479    if (strcmp(cmd, "request2") == 0) { /** **/
480        enqueue_object_request(&snac, url, 2);
481
482        return 0;
483    }
484
485    if (strcmp(cmd, "actor") == 0) { /** **/
486        int status;
487        xs *data = NULL;
488
489        status = actor_request(&snac, url, &data);
490
491        printf("status: %d\n", status);
492
493        if (valid_status(status)) {
494            xs_json_dump(data, 4, stdout);
495        }
496
497        return 0;
498    }
499
500    if (strcmp(cmd, "note") == 0) { /** **/
501        xs *content = NULL;
502        xs *msg = NULL;
503        xs *c_msg = NULL;
504        xs *attl = xs_list_new();
505        char *fn = NULL;
506
507        /* iterate possible attachments */
508        while ((fn = GET_ARGV())) {
509            FILE *f;
510
511            if ((f = fopen(fn, "rb")) != NULL) {
512                /* get the file size and content */
513                fseek(f, 0, SEEK_END);
514                int sz = ftell(f);
515                fseek(f, 0, SEEK_SET);
516                xs *atc = xs_readall(f);
517                fclose(f);
518
519                char *ext = strrchr(fn, '.');
520                xs *hash  = xs_md5_hex(fn, strlen(fn));
521                xs *id    = xs_fmt("%s%s", hash, ext);
522                xs *url   = xs_fmt("%s/s/%s", snac.actor, id);
523
524                /* store */
525                static_put(&snac, id, atc, sz);
526
527                xs *l = xs_list_new();
528
529                l = xs_list_append(l, url);
530                l = xs_list_append(l, ""); /* alt text */
531
532                attl = xs_list_append(attl, l);
533            }
534            else
535                fprintf(stderr, "Error opening '%s' as attachment\n", fn);
536        }
537
538        if (strcmp(url, "-e") == 0) {
539            /* get the content from an editor */
540            FILE *f;
541
542            unlink("/tmp/snac-edit.txt");
543            system("$EDITOR /tmp/snac-edit.txt");
544
545            if ((f = fopen("/tmp/snac-edit.txt", "r")) != NULL) {
546                content = xs_readall(f);
547                fclose(f);
548
549                unlink("/tmp/snac-edit.txt");
550            }
551            else {
552                printf("Nothing to send\n");
553                return 1;
554            }
555        }
556        else
557        if (strcmp(url, "-") == 0) {
558            /* get the content from stdin */
559            content = xs_readall(stdin);
560        }
561        else
562            content = xs_dup(url);
563
564        msg = msg_note(&snac, content, NULL, NULL, attl, 0);
565
566        c_msg = msg_create(&snac, msg);
567
568        if (dbglevel) {
569            xs_json_dump(c_msg, 4, stdout);
570        }
571
572        enqueue_message(&snac, c_msg);
573
574        timeline_add(&snac, xs_dict_get(msg, "id"), msg);
575
576        return 0;
577    }
578
579    fprintf(stderr, "ERROR: bad command '%s'\n", cmd);
580
581    return 1;
582}