Unverified Commit 3340fc7a authored by Hong Minhee's avatar Hong Minhee
Browse files

`Context.getNodeInfo()` method

parent 74652529
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@ To be released.
     -  Added `ActorCallbackSetters.mapAlias()` method.
     -  Added `ActorAliasMapper` type.

 -  Added `Context.getNodeInfo()` method.  [[#203]]

 -  Added `shares` property to `Object` class in Activity Vocabulary API.

     -  Added `Object.sharesId` property.
+1 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ The key features of the `Context` object are as follows:
 -  [Enqueuing an outgoing activity](#enqueuing-an-outgoing-activity)
 -  [Getting a `DocumentLoader`](#getting-a-documentloader)
 -  [Looking up remote objects](#looking-up-remote-objects)
 -  [NodeInfo client](./nodeinfo.md#nodeinfo-client)


Where to get a `Context` object
+14 −11
Original line number Diff line number Diff line
@@ -157,30 +157,33 @@ The `NodeInfo` interface is defined as follows:
NodeInfo client
---------------

*This API is available since Fedify 1.2.0.*
*This API is available since Fedify 1.4.0.*

You may want to fetch NodeInfo objects from other servers.  Fedify provides
a way to fetch NodeInfo objects with the `getNodeInfo()` function:
a way to fetch NodeInfo objects with the `Context.lookupNodeInfo()` method:

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

const nodeInfo = await getNodeInfo("https://example.com/");
import { type Context } from "@fedify/fedify";
const ctx = null as unknown as Context<void>;
// ---cut-before---
const nodeInfo = await ctx.lookupNodeInfo("https://example.com/");
if (nodeInfo != null) console.log(nodeInfo);
~~~~

The `getNodeInfo()` function returns a `NodeInfo` object if the server provides
a NodeInfo endpoint and the response is valid.  Otherwise, it returns `null`.
The `Context.lookupNodeInfo()` method returns a `NodeInfo` object if the server
provides a NodeInfo endpoint and the response is valid.  Otherwise, it returns
`undefined`.

> [!TIP]
> Sometimes, a server may provide a slightly invalid NodeInfo object.  In such
> case, you can enforce parsing the object by passing `{ parse: "best-effort" }`
> option as the second argument to `getNodeInfo()` function:
> option as the second argument to `Context.lookupNodeInfo()` method:
>
> ~~~~ typescript twoslash
> import { getNodeInfo } from "@fedify/fedify";
>
> const nodeInfo = await getNodeInfo("https://example.com/", {
> import { type Context } from "@fedify/fedify";
> const ctx = null as unknown as Context<void>;
> // ---cut-before---
> const nodeInfo = await ctx.lookupNodeInfo("https://example.com/", {
>   parse: "best-effort",
> });
>
+36 −0
Original line number Diff line number Diff line
import type { TracerProvider } from "@opentelemetry/api";
import type { GetNodeInfoOptions } from "../nodeinfo/client.ts";
import type { JsonValue, NodeInfo } from "../nodeinfo/types.ts";
import type { DocumentLoader } from "../runtime/docloader.ts";
import type { Actor, Recipient } from "../vocab/actor.ts";
import type {
@@ -280,6 +282,40 @@ export interface Context<TContextData> {
    options?: TraverseCollectionOptions,
  ): AsyncIterable<Object | Link>;

  /**
   * Fetches the 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.4.0
   */
  lookupNodeInfo(
    url: URL | string,
    options?: GetNodeInfoOptions & { parse?: "strict" | "best-effort" },
  ): Promise<NodeInfo | undefined>;

  /**
   * Fetches the 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.4.0
   */
  lookupNodeInfo(
    url: URL | string,
    options?: GetNodeInfoOptions & { parse: "none" },
  ): Promise<JsonValue | undefined>;

  /**
   * Sends an activity to recipients' inboxes.
   * @param sender The sender's identifier or the sender's username or
+51 −0
Original line number Diff line number Diff line
@@ -402,6 +402,57 @@ test("Federation.createContext()", async (t) => {
    assertEquals(ctx.parseUri(null), null);
  });

  mf.mock("GET@/.well-known/nodeinfo", (req) => {
    assertEquals(new URL(req.url).host, "example.com");
    assertEquals(req.headers.get("User-Agent"), "CustomUserAgent/1.2.3");
    return new Response(
      JSON.stringify({
        links: [
          {
            rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
            href: "https://example.com/nodeinfo/2.1",
          },
        ],
      }),
    );
  });

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

  await t.step("Context.lookupNodeInfo()", async () => {
    const federation = createFederation<number>({
      kv,
      userAgent: "CustomUserAgent/1.2.3",
    });
    const ctx = federation.createContext(new URL("https://example.com/"), 123);
    const nodeInfo = await ctx.lookupNodeInfo("https://example.com/");
    assertEquals(nodeInfo, {
      software: {
        name: "foo",
        version: { major: 1, minor: 2, patch: 3, build: [], prerelease: [] },
      },
      protocols: ["activitypub", "diaspora"],
      usage: { users: {}, localPosts: 123, localComments: 456 },
    });

    const rawNodeInfo = await ctx.lookupNodeInfo("https://example.com/", {
      parse: "none",
    });
    assertEquals(rawNodeInfo, {
      software: { name: "foo", version: "1.2.3" },
      protocols: ["activitypub", "diaspora"],
      usage: { users: {}, localPosts: 123, localComments: 456 },
    });
  });

  await t.step("RequestContext", async () => {
    const federation = createFederation<number>({
      kv,
Loading