Loading cli/inbox.tsx +49 −52 Original line number Diff line number Diff line Loading @@ -32,7 +32,8 @@ const logger = getLogger(["fedify", "cli", "inbox"]); export const command = new Command() .description( "Spins up an ephemeral ActivityPub server and receives activities. " + "Spins up an ephemeral server that serves the ActivityPub inbox with " + "an one-time actor, through a short-lived public DNS with HTTPS. " + "You can monitor the incoming activities in real-time.", ) .option( Loading @@ -49,15 +50,23 @@ export const command = new Command() "requests will be accepted.", { collect: true }, ) .option( "-T, --no-tunnel", "Do not tunnel the ephemeral ActivityPub server to the public Internet.", ) .action(async (options) => { const spinner = ora({ text: "Spinning up an ephemeral ActivityPub server...", discardStdin: false, }).start(); const server = await spawnTemporaryServer(fetch); const server = await spawnTemporaryServer(fetch, { noTunnel: !options.tunnel, }); spinner.succeed( `The ephemeral ActivityPub server is up and running: ${ colors.green(server.url.href) colors.green( server.url.href, ) }`, ); Deno.addSignalListener("SIGINT", () => { Loading Loading @@ -90,7 +99,7 @@ export const command = new Command() object: actor.id, }), ); spinner.succeed(`Followed ${colors.green(uri)}`); spinner.succeed(`Sent follow request to ${colors.green(uri)}.`); spinner.start(); } } Loading @@ -101,7 +110,6 @@ export const command = new Command() const federation = new Federation<number>({ kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), treatHttps: true, documentLoader: await getDocumentLoader(), }); Loading Loading @@ -167,16 +175,14 @@ federation if (!isActor(follower)) return; const accepts = await acceptsFollowFrom(follower); if (!accepts) { logger.debug( "Does not accept follow from {actor}.", { actor: follower.id?.href }, ); logger.debug("Does not accept follow from {actor}.", { actor: follower.id?.href, }); return; } logger.debug( "Accepting follow from {actor}.", { actor: follower.id?.href }, ); logger.debug("Accepting follow from {actor}.", { actor: follower.id?.href, }); await ctx.sendActivity( { handle }, follower, Loading @@ -192,7 +198,7 @@ function printServerInfo(fedCtx: Context<number>): void { new Table( [ new Cell("Actor handle:").align("right"), colors.green(`i@${fedCtx.getActorUri("i").hostname}`), colors.green(`i@${fedCtx.getActorUri("i").host}`), ], [ new Cell("Actor URI:").align("right"), Loading @@ -206,22 +212,19 @@ function printServerInfo(fedCtx: Context<number>): void { new Cell("Shared inbox:").align("right"), colors.green(fedCtx.getInboxUri().href), ], ).chars(tableStyle).border().render(); ) .chars(tableStyle) .border() .render(); } function printActivityEntry( idx: number, entry: ActivityEntry, ): void { function printActivityEntry(idx: number, entry: ActivityEntry): void { const request = entry.request.clone(); const response = entry.response?.clone(); const url = new URL(request.url); const activity = entry.activity; new Table( [ new Cell("Request #:").align("right"), colors.bold(idx.toString()), ], [new Cell("Request #:").align("right"), colors.bold(idx.toString())], [ new Cell("Activity type:").align("right"), activity == null Loading @@ -236,42 +239,36 @@ function printActivityEntry( : colors.red(request.method) } ${url.pathname + url.search}`, ], ...(response == null ? [] : [[ ...(response == null ? [] : [ [ new Cell("HTTP response:").align("right"), `${ response.ok ? colors.green(response.status.toString()) : colors.red(response.status.toString()) } ${response.statusText}`, ]]), [ new Cell("Details").align("right"), new URL(`/r/${idx}`, url).href, ], ).chars(tableStyle).border().render(); ]), [new Cell("Details").align("right"), new URL(`/r/${idx}`, url).href], ) .chars(tableStyle) .border() .render(); } const app = new Hono(); app.get("/", (c) => c.redirect("/r")); app.get("/r", (c) => c.html( <ActivityListPage entries={activities} />, )); app.get("/r", (c) => c.html(<ActivityListPage entries={activities} />)); app.get( "/r/:idx{[0-9]+}", (c) => { app.get("/r/:idx{[0-9]+}", (c) => { const idx = parseInt(c.req.param("idx")); const tab = c.req.query("tab") ?? "request"; const activity = activities[idx]; if (activity == null) return c.notFound(); return c.html( <ActivityEntryPage idx={idx} entry={activity} tabPage={tab} />, ); }, ); return c.html(<ActivityEntryPage idx={idx} entry={activity} tabPage={tab} />); }); async function fetch(request: Request): Promise<Response> { const timestamp = Temporal.Now.instant(); Loading cli/tempserver.ts +46 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,10 @@ import { getLogger } from "@logtape/logtape"; const logger = getLogger(["fedify", "cli", "tempserver"]); export interface SpawnTemporaryServerOptions { noTunnel?: boolean; } export interface TemporaryServer { url: URL; close(): Promise<void>; Loading @@ -10,12 +14,53 @@ export interface TemporaryServer { export function spawnTemporaryServer( handler: Deno.ServeHandler, options: SpawnTemporaryServerOptions = {}, ): Promise<TemporaryServer> { if (options.noTunnel) { return new Promise((resolve) => { const server = Deno.serve({ handler, port: 0, hostname: "::", onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port, }); resolve({ url: new URL(`http://localhost:${port}`), async close() { await server.shutdown(); }, }); }, }); }); } return new Promise((resolve) => { const server = Deno.serve({ async handler(request: Request, info: Deno.ServeHandlerInfo) { const url = new URL(request.url); url.protocol = "https:"; request = new Request(url, { method: request.method, headers: request.headers, body: request.method === "GET" || request.method === "HEAD" ? null : await request.blob(), referrer: request.referrer, referrerPolicy: request.referrerPolicy, mode: request.mode, credentials: request.credentials, cache: request.cache, redirect: request.redirect, integrity: request.integrity, keepalive: request.keepalive, signal: request.signal, }); return await handler(request, info); }, port: 0, hostname: "::", onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port }); openTunnel({ port }).then((tun) => { Loading docs/cli.md +70 −4 Original line number Diff line number Diff line Loading @@ -413,10 +413,11 @@ Person { `fedify inbox`: Ephemeral inbox server -------------------------------------- The `fedify inbox` command is used to spin up an ephemeral inbox server that serves the ActivityPub inbox with an one-time actor. This is useful when you want to test and debug the outgoing activities of your server. To start an ephemeral inbox server, run the below command: The `fedify inbox` command is used to spin up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. This is useful when you want to test and debug the outgoing activities of your server. To start an ephemeral inbox server, run the below command: ~~~~ sh fedify inbox Loading Loading @@ -465,6 +466,71 @@ You can also see the details of the incoming activities by visiting the  ### `-f`/`--follow`: Follow an actor The `-f`/`--follow` option is used to follow an actor. You can specify the actor handle or URI to follow. For example, to follow the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ~~~~ sh fedify inbox -f @john@doe.com -f @jane@doe.com ~~~~ > [!NOTE] > Although `-f`/`--follow` option sends `Follow` activities to the specified > actors, it does not guarantee that they will accept the follow requests. > If the actors accept the follow requests, you will receive the `Accept` > activities in the inbox server, and the server will log them to the console: > > ~~~~ > ╭────────────────┬─────────────────────────────────────╮ > │ Request #: │ 0 │ > ├────────────────┼─────────────────────────────────────┤ > │ Activity type: │ Accept │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP request: │ POST /i/inbox │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP response: │ 202 │ > ├────────────────┼─────────────────────────────────────┤ > │ Details │ https://876f71397f5c31.lhr.life/r/0 │ > ╰────────────────┴─────────────────────────────────────╯ > ~~~~ ### `-a`/`--accept-follow`: Accept follow requests The `-a`/`--accept-follow` option is used to accept follow requests from actors. You can specify the actor handle or URI to accept follow requests. Or you can accept all follow requests by specifying the wildcard `*`. For example, to accept follow requests from the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ~~~~ sh fedify inbox -a @john@doe.com -a @jane@doe.com ~~~~ When the follow requests are received from the specified actors, the server will immediately send the `Accept` activities to them. Otherwise, the server will just log the `Follow` activities to the console without sending the `Accept` activities. ### `-T`/`--no-tunnel`: Local server without tunneling The `-T`/`--no-tunnel` option is used to disable the tunneling feature of the inbox server. By default, the inbox server tunnels the local server to the public internet, so that the server is accessible from the outside. If you want to disable the tunneling feature, run the below command: ~~~~ sh fedify inbox --no-tunnel ~~~~ It would be useful when you want to test the server locally but are worried about the security implications of exposing the server to the public internet. > [!NOTE] > If you disable the tunneling feature, the ephemeral ActivityPub instance will > be served via HTTP instead of HTTPS. Shell completions ----------------- Loading Loading
cli/inbox.tsx +49 −52 Original line number Diff line number Diff line Loading @@ -32,7 +32,8 @@ const logger = getLogger(["fedify", "cli", "inbox"]); export const command = new Command() .description( "Spins up an ephemeral ActivityPub server and receives activities. " + "Spins up an ephemeral server that serves the ActivityPub inbox with " + "an one-time actor, through a short-lived public DNS with HTTPS. " + "You can monitor the incoming activities in real-time.", ) .option( Loading @@ -49,15 +50,23 @@ export const command = new Command() "requests will be accepted.", { collect: true }, ) .option( "-T, --no-tunnel", "Do not tunnel the ephemeral ActivityPub server to the public Internet.", ) .action(async (options) => { const spinner = ora({ text: "Spinning up an ephemeral ActivityPub server...", discardStdin: false, }).start(); const server = await spawnTemporaryServer(fetch); const server = await spawnTemporaryServer(fetch, { noTunnel: !options.tunnel, }); spinner.succeed( `The ephemeral ActivityPub server is up and running: ${ colors.green(server.url.href) colors.green( server.url.href, ) }`, ); Deno.addSignalListener("SIGINT", () => { Loading Loading @@ -90,7 +99,7 @@ export const command = new Command() object: actor.id, }), ); spinner.succeed(`Followed ${colors.green(uri)}`); spinner.succeed(`Sent follow request to ${colors.green(uri)}.`); spinner.start(); } } Loading @@ -101,7 +110,6 @@ export const command = new Command() const federation = new Federation<number>({ kv: new MemoryKvStore(), queue: new InProcessMessageQueue(), treatHttps: true, documentLoader: await getDocumentLoader(), }); Loading Loading @@ -167,16 +175,14 @@ federation if (!isActor(follower)) return; const accepts = await acceptsFollowFrom(follower); if (!accepts) { logger.debug( "Does not accept follow from {actor}.", { actor: follower.id?.href }, ); logger.debug("Does not accept follow from {actor}.", { actor: follower.id?.href, }); return; } logger.debug( "Accepting follow from {actor}.", { actor: follower.id?.href }, ); logger.debug("Accepting follow from {actor}.", { actor: follower.id?.href, }); await ctx.sendActivity( { handle }, follower, Loading @@ -192,7 +198,7 @@ function printServerInfo(fedCtx: Context<number>): void { new Table( [ new Cell("Actor handle:").align("right"), colors.green(`i@${fedCtx.getActorUri("i").hostname}`), colors.green(`i@${fedCtx.getActorUri("i").host}`), ], [ new Cell("Actor URI:").align("right"), Loading @@ -206,22 +212,19 @@ function printServerInfo(fedCtx: Context<number>): void { new Cell("Shared inbox:").align("right"), colors.green(fedCtx.getInboxUri().href), ], ).chars(tableStyle).border().render(); ) .chars(tableStyle) .border() .render(); } function printActivityEntry( idx: number, entry: ActivityEntry, ): void { function printActivityEntry(idx: number, entry: ActivityEntry): void { const request = entry.request.clone(); const response = entry.response?.clone(); const url = new URL(request.url); const activity = entry.activity; new Table( [ new Cell("Request #:").align("right"), colors.bold(idx.toString()), ], [new Cell("Request #:").align("right"), colors.bold(idx.toString())], [ new Cell("Activity type:").align("right"), activity == null Loading @@ -236,42 +239,36 @@ function printActivityEntry( : colors.red(request.method) } ${url.pathname + url.search}`, ], ...(response == null ? [] : [[ ...(response == null ? [] : [ [ new Cell("HTTP response:").align("right"), `${ response.ok ? colors.green(response.status.toString()) : colors.red(response.status.toString()) } ${response.statusText}`, ]]), [ new Cell("Details").align("right"), new URL(`/r/${idx}`, url).href, ], ).chars(tableStyle).border().render(); ]), [new Cell("Details").align("right"), new URL(`/r/${idx}`, url).href], ) .chars(tableStyle) .border() .render(); } const app = new Hono(); app.get("/", (c) => c.redirect("/r")); app.get("/r", (c) => c.html( <ActivityListPage entries={activities} />, )); app.get("/r", (c) => c.html(<ActivityListPage entries={activities} />)); app.get( "/r/:idx{[0-9]+}", (c) => { app.get("/r/:idx{[0-9]+}", (c) => { const idx = parseInt(c.req.param("idx")); const tab = c.req.query("tab") ?? "request"; const activity = activities[idx]; if (activity == null) return c.notFound(); return c.html( <ActivityEntryPage idx={idx} entry={activity} tabPage={tab} />, ); }, ); return c.html(<ActivityEntryPage idx={idx} entry={activity} tabPage={tab} />); }); async function fetch(request: Request): Promise<Response> { const timestamp = Temporal.Now.instant(); Loading
cli/tempserver.ts +46 −1 Original line number Diff line number Diff line Loading @@ -3,6 +3,10 @@ import { getLogger } from "@logtape/logtape"; const logger = getLogger(["fedify", "cli", "tempserver"]); export interface SpawnTemporaryServerOptions { noTunnel?: boolean; } export interface TemporaryServer { url: URL; close(): Promise<void>; Loading @@ -10,12 +14,53 @@ export interface TemporaryServer { export function spawnTemporaryServer( handler: Deno.ServeHandler, options: SpawnTemporaryServerOptions = {}, ): Promise<TemporaryServer> { if (options.noTunnel) { return new Promise((resolve) => { const server = Deno.serve({ handler, port: 0, hostname: "::", onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port, }); resolve({ url: new URL(`http://localhost:${port}`), async close() { await server.shutdown(); }, }); }, }); }); } return new Promise((resolve) => { const server = Deno.serve({ async handler(request: Request, info: Deno.ServeHandlerInfo) { const url = new URL(request.url); url.protocol = "https:"; request = new Request(url, { method: request.method, headers: request.headers, body: request.method === "GET" || request.method === "HEAD" ? null : await request.blob(), referrer: request.referrer, referrerPolicy: request.referrerPolicy, mode: request.mode, credentials: request.credentials, cache: request.cache, redirect: request.redirect, integrity: request.integrity, keepalive: request.keepalive, signal: request.signal, }); return await handler(request, info); }, port: 0, hostname: "::", onListen({ port }) { logger.debug("Temporary server is listening on port {port}.", { port }); openTunnel({ port }).then((tun) => { Loading
docs/cli.md +70 −4 Original line number Diff line number Diff line Loading @@ -413,10 +413,11 @@ Person { `fedify inbox`: Ephemeral inbox server -------------------------------------- The `fedify inbox` command is used to spin up an ephemeral inbox server that serves the ActivityPub inbox with an one-time actor. This is useful when you want to test and debug the outgoing activities of your server. To start an ephemeral inbox server, run the below command: The `fedify inbox` command is used to spin up an ephemeral server that serves the ActivityPub inbox with an one-time actor, through a short-lived public DNS with HTTPS. This is useful when you want to test and debug the outgoing activities of your server. To start an ephemeral inbox server, run the below command: ~~~~ sh fedify inbox Loading Loading @@ -465,6 +466,71 @@ You can also see the details of the incoming activities by visiting the  ### `-f`/`--follow`: Follow an actor The `-f`/`--follow` option is used to follow an actor. You can specify the actor handle or URI to follow. For example, to follow the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ~~~~ sh fedify inbox -f @john@doe.com -f @jane@doe.com ~~~~ > [!NOTE] > Although `-f`/`--follow` option sends `Follow` activities to the specified > actors, it does not guarantee that they will accept the follow requests. > If the actors accept the follow requests, you will receive the `Accept` > activities in the inbox server, and the server will log them to the console: > > ~~~~ > ╭────────────────┬─────────────────────────────────────╮ > │ Request #: │ 0 │ > ├────────────────┼─────────────────────────────────────┤ > │ Activity type: │ Accept │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP request: │ POST /i/inbox │ > ├────────────────┼─────────────────────────────────────┤ > │ HTTP response: │ 202 │ > ├────────────────┼─────────────────────────────────────┤ > │ Details │ https://876f71397f5c31.lhr.life/r/0 │ > ╰────────────────┴─────────────────────────────────────╯ > ~~~~ ### `-a`/`--accept-follow`: Accept follow requests The `-a`/`--accept-follow` option is used to accept follow requests from actors. You can specify the actor handle or URI to accept follow requests. Or you can accept all follow requests by specifying the wildcard `*`. For example, to accept follow requests from the actor with the handle *@john@doe.com* and *@jane@doe.com*, run the below command: ~~~~ sh fedify inbox -a @john@doe.com -a @jane@doe.com ~~~~ When the follow requests are received from the specified actors, the server will immediately send the `Accept` activities to them. Otherwise, the server will just log the `Follow` activities to the console without sending the `Accept` activities. ### `-T`/`--no-tunnel`: Local server without tunneling The `-T`/`--no-tunnel` option is used to disable the tunneling feature of the inbox server. By default, the inbox server tunnels the local server to the public internet, so that the server is accessible from the outside. If you want to disable the tunneling feature, run the below command: ~~~~ sh fedify inbox --no-tunnel ~~~~ It would be useful when you want to test the server locally but are worried about the security implications of exposing the server to the public internet. > [!NOTE] > If you disable the tunneling feature, the ephemeral ActivityPub instance will > be served via HTTP instead of HTTPS. Shell completions ----------------- Loading