Loading CHANGES.md +2 −0 Original line number Diff line number Diff line Loading @@ -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. Loading docs/manual/context.md +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading docs/manual/nodeinfo.md +14 −11 Original line number Diff line number Diff line Loading @@ -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", > }); > Loading src/federation/context.ts +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 { Loading Loading @@ -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 Loading src/federation/middleware.test.ts +51 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
CHANGES.md +2 −0 Original line number Diff line number Diff line Loading @@ -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. Loading
docs/manual/context.md +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
docs/manual/nodeinfo.md +14 −11 Original line number Diff line number Diff line Loading @@ -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", > }); > Loading
src/federation/context.ts +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 { Loading Loading @@ -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 Loading
src/federation/middleware.test.ts +51 −0 Original line number Diff line number Diff line Loading @@ -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