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