Unverified Commit c9309d76 authored by Hong Minhee's avatar Hong Minhee
Browse files

`GetNodeInfoOptions.parse` option

parent 133bc111
Loading
Loading
Loading
Loading
+6 −6
Original line number Diff line number Diff line
@@ -163,9 +163,9 @@ You may want to fetch NodeInfo objects from other servers. Fedify provides
a way to fetch NodeInfo objects with the `getNodeInfo()` function:

~~~~ typescript twoslash
import { type NodeInfo, getNodeInfo } from "@fedify/fedify";
import { getNodeInfo } from "@fedify/fedify";

const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/");
const nodeInfo = await getNodeInfo("https://example.com/");
if (nodeInfo != null) console.log(nodeInfo);
~~~~

@@ -174,14 +174,14 @@ a NodeInfo endpoint and the response is valid. Otherwise, it returns `null`.

> [!TIP]
> Sometimes, a server may provide a slightly invalid NodeInfo object.  In such
> case, you can enforce parsing the object by passing `{ tryBestEffort: true }`
> case, you can enforce parsing the object by passing `{ parse: "best-effort" }`
> option as the second argument to `getNodeInfo()` function:
>
> ~~~~ typescript twoslash
> import { type NodeInfo, getNodeInfo } from "@fedify/fedify";
> import { getNodeInfo } from "@fedify/fedify";
>
> const nodeInfo: NodeInfo | null = await getNodeInfo("https://example.com/", {
>   tryBestEffort: true,
> const nodeInfo = await getNodeInfo("https://example.com/", {
>   parse: "best-effort",
> });
>
> if (nodeInfo != null) console.log(nodeInfo);
+14 −9
Original line number Diff line number Diff line
@@ -35,14 +35,16 @@ test("getNodeInfo()", async (t) => {
    );
  });

  mf.mock("GET@/nodeinfo/2.1", (req) => {
    assertEquals(new URL(req.url).host, "example.com");
    return new Response(
      JSON.stringify({
  const rawExpected = {
    software: { name: "foo", version: "1.2.3" },
    protocols: ["activitypub", "diaspora"],
    usage: { users: {}, localPosts: 123, localComments: 456 },
      }),
  };

  mf.mock("GET@/nodeinfo/2.1", (req) => {
    assertEquals(new URL(req.url).host, "example.com");
    return new Response(
      JSON.stringify(rawExpected),
    );
  });

@@ -63,6 +65,9 @@ test("getNodeInfo()", async (t) => {
  await t.step("indirect", async () => {
    const info = await getNodeInfo("https://example.com/");
    assertEquals(info, expected);

    const raw = await getNodeInfo("https://example.com/", { parse: "none" });
    assertEquals(raw, rawExpected);
  });

  await t.step("direct", async () => {
@@ -82,7 +87,7 @@ test("getNodeInfo()", async (t) => {

  await t.step("indirect: no links", async () => {
    const info = await getNodeInfo("https://example.com/");
    assertEquals(info, null);
    assertEquals(info, undefined);
  });

  mf.mock("GET@/.well-known/nodeinfo", (req) => {
@@ -92,7 +97,7 @@ test("getNodeInfo()", async (t) => {

  await t.step("indirect: 404", async () => {
    const info = await getNodeInfo("https://example.com/");
    assertEquals(info, null);
    assertEquals(info, undefined);
  });

  await t.step("direct: 404", async () => {
@@ -100,12 +105,12 @@ test("getNodeInfo()", async (t) => {
      "https://example.com/nodeinfo/2.0",
      { direct: true },
    );
    assertEquals(info, null);
    assertEquals(info, undefined);
    const info2 = await getNodeInfo(
      "https://example.com/404",
      { direct: true },
    );
    assertEquals(info2, null);
    assertEquals(info2, undefined);
  });

  mf.uninstall();
+45 −8
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@ const logger = getLogger(["fedify", "nodeinfo", "client"]);
 * Options for {@link getNodeInfo} function.
 * @since 1.2.0
 */
export interface GetNodeInfoOptions extends ParseNodeInfoOptions {
export interface GetNodeInfoOptions {
  /**
   * Whether to directly fetch the NodeInfo document from the given URL.
   * Otherwise, the NodeInfo document will be fetched from the `.well-known`
@@ -27,6 +27,18 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions {
   * Turned off by default.
   */
  direct?: boolean;

  /**
   * How strictly to parse the NodeInfo document.
   *
   *  -  `"strict"`: Parse the NodeInfo document strictly.  If the document is
   *     invalid, `undefined` is returned.  This is the default.
   *  -  `"best-effort"`: Try to parse the NodeInfo document even if it is
   *     invalid.
   *  -  `"none"`: Do not parse the NodeInfo document.  The function will return
   *     the raw JSON value.
   */
  parse?: "strict" | "best-effort" | "none";
}

/**
@@ -38,13 +50,35 @@ export interface GetNodeInfoOptions extends ParseNodeInfoOptions {
 *            the NodeInfo document will be fetched from the given URL.
 * @param options Options for fetching the NodeInfo document.
 * @returns The NodeInfo document if it could be fetched successfully.
 *          Otherwise, `null` is returned.
 *          Otherwise, `undefined` is returned.
 * @since 1.2.0
 */
export async function getNodeInfo(
  url: URL | string,
  options?: GetNodeInfoOptions & { parse?: "strict" | "best-effort" },
): Promise<NodeInfo | undefined>;

/**
 * Fetches a NodeInfo document from the given URL.
 * @param url The base URL of the server.  If `options.direct` is turned off
 *            (default), the NodeInfo document will be fetched from
 *            the `.well-known` location of this URL (hence the only origin
 *            of the URL is used).  If `options.direct` is turned on,
 *            the NodeInfo document will be fetched from the given URL.
 * @param options Options for fetching the NodeInfo document.
 * @returns The NodeInfo document if it could be fetched successfully.
 *          Otherwise, `undefined` is returned.
 * @since 1.2.0
 */
export async function getNodeInfo(
  url: URL | string,
  options: GetNodeInfoOptions & { parse: "none" },
): Promise<JsonValue | undefined>;

export async function getNodeInfo(
  url: URL | string,
  options: GetNodeInfoOptions = {},
): Promise<NodeInfo | null> {
): Promise<NodeInfo | JsonValue | undefined> {
  try {
    let nodeInfoUrl: URL | string = url;
    if (!options.direct) {
@@ -56,7 +90,7 @@ export async function getNodeInfo(
          status: wellKnownResponse.status,
          statusText: wellKnownResponse.statusText,
        });
        return null;
        return undefined;
      }
      const wellKnownRd = await wellKnownResponse.json() as ResourceDescriptor;
      const link = wellKnownRd?.links?.find((link) =>
@@ -72,7 +106,7 @@ export async function getNodeInfo(
          "Failed to find a NodeInfo document link from {url}: {resourceDescriptor}",
          { url: wellKnownUrl.href, resourceDescriptor: wellKnownRd },
        );
        return null;
        return undefined;
      }
      nodeInfoUrl = link.href;
    }
@@ -86,16 +120,19 @@ export async function getNodeInfo(
          statusText: response.statusText,
        },
      );
      return null;
      return undefined;
    }
    const data = await response.json();
    return parseNodeInfo(data, options);
    if (options.parse === "none") return data as JsonValue;
    return parseNodeInfo(data, {
      tryBestEffort: options.parse === "best-effort",
    }) ?? undefined;
  } catch (error) {
    logger.error("Failed to fetch NodeInfo document from {url}: {error}", {
      url: url.toString(),
      error,
    });
    return null;
    return undefined;
  }
}