Unverified Commit 67cd278a authored by Hong Minhee (洪 民憙)'s avatar Hong Minhee (洪 民憙) Committed by GitHub
Browse files

Merge pull request #285 from crohasang/feature/262-inbox-actor-customization

parents 77901b2c 131fe7af
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -31,6 +31,13 @@ To be released.
     -  Added `MemoryKvStore.cas()` method.
     -  Added `DenoKvStore.cas()` method.
  
 -  Added options to customize the temporary actor information when running
    `fedify inbox` command.  [[#262], [#285] by Hasang Cho]

     -  Added `--actor-name` option to customize the actor display name.
     -  Added `--actor-summary` option to customize the actor description.
     -  Both options provide sensible defaults when not specified.

 -  Added useful functions for fediverse handles at `@fedify/fedify/vocab`.
    This functions simplify working with fediverse handles and URLs.
    [[#278] by ChanHaeng Lee]
@@ -63,9 +70,11 @@ To be released.
[#168]: https://github.com/fedify-dev/fedify/issues/168
[#248]: https://github.com/fedify-dev/fedify/issues/248
[#260]: https://github.com/fedify-dev/fedify/issues/260
[#262]: https://github.com/fedify-dev/fedify/issues/262
[#278]: https://github.com/fedify-dev/fedify/pull/278
[#281]: https://github.com/fedify-dev/fedify/pull/281
[#282]: https://github.com/fedify-dev/fedify/pull/282
[#285]: https://github.com/fedify-dev/fedify/pull/285


Version 1.7.4
+113 −54
Original line number Diff line number Diff line
@@ -35,7 +35,25 @@ import { recordingSink } from "./log.ts";
import { tableStyle } from "./table.ts";
import { spawnTemporaryServer, type TemporaryServer } from "./tempserver.ts";

const logger = getLogger(["fedify", "cli", "inbox"]);
/**
 * Context data for the ephemeral ActivityPub inbox server.
 *
 * This interface defines the shape of context data passed to federation
 * handlers during inbox command execution.
 */
interface ContextData {
  activityIndex: number;
  actorName: string;
  actorSummary: string;
}

/**
 * Options for actor customization.
 */
interface ActorOptions {
  actorName: string;
  actorSummary: string;
}

/**
 * Options for the inbox command.
@@ -53,6 +71,8 @@ export const TunnelConfig = {
  },
} as const;

const logger = getLogger(["fedify", "cli", "inbox"]);

export const command = new Command()
  .description(
    "Spins up an ephemeral server that serves the ActivityPub inbox with " +
@@ -77,7 +97,20 @@ export const command = new Command()
    "-T, --no-tunnel",
    "Do not tunnel the ephemeral ActivityPub server to the public Internet.",
  )
  .action(async (options: InboxOptions) => {
  .option(
    "--actor-name=<name:string>",
    "Customize the actor display name.",
    { default: "Fedify Ephemeral Inbox" },
  )
  .option(
    "--actor-summary=<summary:string>",
    "Customize the actor description.",
    { default: "An ephemeral ActivityPub inbox for testing purposes." },
  )
  .action(async (options: InboxOptions & ActorOptions) => {
    const fetch = createFetchHandler(options);
    const sendDeleteToPeers = createSendDeleteToPeers(options);

    const spinner = ora({
      text: "Spinning up an ephemeral ActivityPub server...",
      discardStdin: false,
@@ -109,7 +142,13 @@ export const command = new Command()
      });
    });
    spinner.start();
    const fedCtx = federation.createContext(server.url, -1);

    const fedCtx = federation.createContext(server.url, {
      activityIndex: -1,
      actorName: options.actorName,
      actorSummary: options.actorSummary,
    });

    if (options.acceptFollow != null && options.acceptFollow.length > 0) {
      acceptFollows.push(...(options.acceptFollow ?? []));
    }
@@ -144,7 +183,7 @@ export const command = new Command()
    printServerInfo(fedCtx);
  });

const federation = createFederation<number>({
const federation = createFederation<ContextData>({
  kv: new MemoryKvStore(),
  documentLoader: await getDocumentLoader(),
});
@@ -158,8 +197,8 @@ federation
    return new Application({
      id: ctx.getActorUri(identifier),
      preferredUsername: identifier,
      name: "Fedify Ephemeral Inbox",
      summary: "An ephemeral ActivityPub inbox for testing purposes.",
      name: ctx.data.actorName,
      summary: ctx.data.actorSummary,
      inbox: ctx.getInboxUri(identifier),
      endpoints: new Endpoints({
        sharedInbox: ctx.getInboxUri(),
@@ -212,8 +251,18 @@ async function acceptsFollowFrom(actor: Actor): Promise<boolean> {

const peers: Record<string, Actor> = {};

async function sendDeleteToPeers(server: TemporaryServer): Promise<void> {
  const ctx = federation.createContext(new Request(server.url), -1);
function createSendDeleteToPeers(
  actorOptions: ActorOptions,
): (server: TemporaryServer) => Promise<void> {
  return async function sendDeleteToPeers(
    server: TemporaryServer,
  ): Promise<void> {
    const ctx = federation.createContext(new Request(server.url), {
      activityIndex: -1,
      actorName: actorOptions.actorName,
      actorSummary: actorOptions.actorSummary,
    });

    const actor = (await ctx.getActor("i"))!;
    try {
      await ctx.sendActivity(
@@ -232,6 +281,7 @@ async function sendDeleteToPeers(server: TemporaryServer): Promise<void> {
        { error },
      );
    }
  };
}

const followers: Record<string, Actor> = {};
@@ -240,7 +290,7 @@ federation
  .setInboxListeners("/{identifier}/inbox", "/inbox")
  .setSharedKeyDispatcher((_) => ({ identifier: "i" }))
  .on(Activity, async (ctx, activity) => {
    activities[ctx.data].activity = activity;
    activities[ctx.data.activityIndex].activity = activity;
    for await (const actor of activity.getActors()) {
      if (actor.id != null) peers[actor.id.href] = actor;
    }
@@ -325,7 +375,7 @@ federation.setNodeInfoDispatcher("/nodeinfo/2.1", (_ctx) => {
  };
});

function printServerInfo(fedCtx: Context<number>): void {
function printServerInfo(fedCtx: Context<ContextData>): void {
  new Table(
    [
      new Cell("Actor handle:").align("right"),
@@ -427,21 +477,29 @@ app.get("/r/:idx{[0-9]+}", (c) => {
  );
});

async function fetch(request: Request): Promise<Response> {
function createFetchHandler(
  actorOptions: ActorOptions,
): (request: Request) => Promise<Response> {
  return async function fetch(request: Request): Promise<Response> {
    const timestamp = Temporal.Now.instant();
    const idx = activities.length;
    const pathname = new URL(request.url).pathname;
    if (pathname === "/r" || pathname.startsWith("/r/")) {
      return app.fetch(request);
    }
  const inboxRequest = pathname === "/inbox" || pathname.startsWith("/i/inbox");
    const inboxRequest = pathname === "/inbox" ||
      pathname.startsWith("/i/inbox");
    if (inboxRequest) {
      recordingSink.startRecording();
      // @ts-ignore: Work around `deno publish --dry-run` bug
      activities.push({ timestamp, request: request.clone(), logs: [] });
    }
    const response = await federation.fetch(request, {
    contextData: inboxRequest ? idx : -1,
      contextData: {
        activityIndex: inboxRequest ? idx : -1,
        actorName: actorOptions.actorName,
        actorSummary: actorOptions.actorSummary,
      },
      onNotAcceptable: app.fetch.bind(app),
      onNotFound: app.fetch.bind(app),
      onUnauthorized: app.fetch.bind(app),
@@ -453,4 +511,5 @@ async function fetch(request: Request): Promise<Response> {
      printActivityEntry(idx, activities[idx]);
    }
    return response;
  };
}