Loading CHANGES.md +10 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,15 @@ To be released. - Added `FederationOrigin` interface. - Added `Context.canonicalOrigin` property. - Followers collection synchronization ([FEP-8fcf]) is now turned off by default. - Added `SendActivityOptionsForCollection` interface. - The type of `Context.sendActivity({ identifier: string } | { username: string } | { handle: string }, "followers", Activity)` overload's fourth parameter became `SendActivityOptionsForCollection | undefined` (was `SendActivityOptions | undefined`). - Fedify now accepts PEM-PKCS#1 besides PEM-SPKI for RSA public keys. [[#209]] Loading Loading @@ -102,6 +111,7 @@ To be released. [#220]: https://github.com/fedify-dev/fedify/issues/220 [#221]: https://github.com/fedify-dev/fedify/issues/221 [#223]: https://github.com/fedify-dev/fedify/pull/223 [FEP-8fcf]: https://w3id.org/fep/8fcf [multibase]: https://github.com/multiformats/js-multibase Loading docs/manual/send.md +29 −16 Original line number Diff line number Diff line Loading @@ -262,6 +262,15 @@ await ctx.sendActivity( > an array of `SenderKeyPair` objects. You need to specify the recipients > manually in this case. > [!TIP] > Does the `Context.sendActivity()` method takes quite a long time to complete > even if you configured the [`queue`](./federation.md#queue)? It might be > because the followers collection is large and the method under the hood > invokes your [followers collection dispatcher](./collections.md#followers) > multiple times to paginate the collection. To improve the performance, > you should implement the [one-short followers collection for gathering > recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients). Specifying an activity ---------------------- Loading Loading @@ -593,7 +602,8 @@ async function sendNote( Followers collection synchronization ------------------------------------ *This API is available since Fedify 0.8.0.* *This API is available since Fedify 0.8.0, and it is optional since Fedify 1.5.0.* > [!NOTE] > For efficiency, you should implement Loading @@ -612,7 +622,8 @@ so that the recipient server can check if it needs to resynchronize the followers collection. Fedify provides a way to include the digest of the followers collection in the activity delivery request by specifying the recipients parameter of the `~Context.sendActivity()` method as the `"followers"` string: the `"followers"` string and turning on the `~SendActivityOptionsForCollection.syncCollection` option: ~~~~ typescript twoslash import { type Context, Create, Note } from "@fedify/fedify"; Loading @@ -630,13 +641,17 @@ await ctx.sendActivity( to: ctx.getFollowersUri(senderId), }), }), { preferSharedInbox: true }, // [!code highlight] { preferSharedInbox: true, // [!code highlight] syncCollection: true, // [!code highlight] }, ); ~~~~ If you specify the `"followers"` string as the recipients parameter, it automatically sends the activity to the sender's followers and includes the digest of the followers collection in the payload. The `~SendActivityOptionsForCollection.syncCollection` option is only available when you specify the `"followers"` string as the recipients parameter. With turning on this option, it automatically sends the activity to the sender's followers and includes the digest of the followers collection in the payload. > [!NOTE] > The `to` and `cc` properties of an `Activity` and its `object` should be set Loading @@ -645,14 +660,12 @@ the digest of the followers collection in the payload. > the `PUBLIC_COLLECTION`, the activity is visible to everyone regardless of > the recipients parameter. > [!TIP] > Does the `Context.sendActivity()` method takes quite a long time to complete > even if you configured the [`queue`](./federation.md#queue)? It might be > because the followers collection is large and the method under the hood > invokes your [followers collection dispatcher](./collections.md#followers) > multiple times to paginate the collection. To improve the performance, > you should implement the [one-short followers collection for gathering > recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients). > [!NOTE] > Some history of this feature: The followers collection synchronization was > first introduced in Fedify 0.8.0, but it was automatically turned on when > the recipients parameter was set to the `"followers"` string then. > Since Fedify 1.5.0, it is optional, and you need to explicitly turn on > the `~SendActivityOptionsForCollection.syncCollection` option to use it. [FEP-8fcf]: https://w3id.org/fep/8fcf Loading src/federation/context.ts +15 −1 Original line number Diff line number Diff line Loading @@ -360,7 +360,7 @@ export interface Context<TContextData> { sender: { identifier: string } | { username: string } | { handle: string }, recipients: "followers", activity: Activity, options?: SendActivityOptions, options?: SendActivityOptionsForCollection, ): Promise<void>; /** Loading Loading @@ -697,6 +697,20 @@ export interface SendActivityOptions { excludeBaseUris?: URL[]; } /** * Options for {@link Context.sendActivity} method when sending to a collection. * @since 1.5.0 */ export interface SendActivityOptionsForCollection extends SendActivityOptions { /** * Whether to synchronize the collection using `Collection-Synchronization` * header ([FEP-8fcf]). * * [FEP-8fcf]: https://w3id.org/fep/8fcf */ syncCollection?: boolean; } /** * Options for {@link InboxContext.forwardActivity} method. * @since 1.0.0 Loading src/federation/middleware.test.ts +64 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import { assertEquals, assertFalse, assertInstanceOf, assertNotEquals, assertRejects, assertStrictEquals, assertThrows, Loading Loading @@ -1456,9 +1457,11 @@ test("ContextImpl.sendActivity()", async (t) => { let verified: ("http" | "ld" | "proof")[] | null = null; let request: Request | null = null; let collectionSyncHeader: string | null = null; mf.mock("POST@/inbox", async (req) => { verified = []; request = req.clone(); collectionSyncHeader = req.headers.get("Collection-Synchronization"); const options = { async documentLoader(url: string) { const response = await federation.fetch( Loading Loading @@ -1541,6 +1544,18 @@ test("ContextImpl.sendActivity()", async (t) => { }) .mapHandle((_ctx, username) => username === "john" ? "1" : null); federation.setFollowersDispatcher( "/users/{identifier}/followers", () => ({ items: [ { id: new URL("https://example.com/recipient"), inboxId: new URL("https://example.com/inbox"), }, ], }), ); await t.step("success", async () => { const activity = new Create({ actor: new URL("https://example.com/person"), Loading Loading @@ -1816,6 +1831,55 @@ test("ContextImpl.sendActivity()", async (t) => { }, ]); }); collectionSyncHeader = null; await t.step("followers collection without syncCollection", async () => { const ctx = new ContextImpl({ data: undefined, federation, url: new URL("https://example.com/"), documentLoader: fetchDocumentLoader, contextLoader: fetchDocumentLoader, }); const activity = new Create({ id: new URL("https://example.com/activity/1"), actor: ctx.getActorUri("1"), to: ctx.getFollowersUri("1"), }); await ctx.sendActivity({ identifier: "1" }, "followers", activity); assertEquals(collectionSyncHeader, null); }); collectionSyncHeader = null; await t.step("followers collection with syncCollection", async () => { const ctx = new ContextImpl({ data: undefined, federation, url: new URL("https://example.com/"), documentLoader: fetchDocumentLoader, contextLoader: fetchDocumentLoader, }); const activity = new Create({ id: new URL("https://example.com/activity/2"), actor: ctx.getActorUri("1"), to: ctx.getFollowersUri("1"), }); await ctx.sendActivity( { identifier: "1" }, "followers", activity, { syncCollection: true, preferSharedInbox: true }, ); assertNotEquals(collectionSyncHeader, null); }); }); test("ContextImpl.routeActivity()", async () => { Loading src/federation/middleware.ts +12 −10 Original line number Diff line number Diff line Loading @@ -83,7 +83,7 @@ import type { ParseUriResult, RequestContext, RouteActivityOptions, SendActivityOptions, SendActivityOptionsForCollection, } from "./context.ts"; import type { ActorCallbackSetters, Loading Loading @@ -3221,7 +3221,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, options: SendActivityOptionsForCollection = {}, ): Promise<void> { const tracer = this.tracerProvider.getTracer( metadata.name, Loading Loading @@ -3274,7 +3274,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, options: SendActivityOptionsForCollection, span: Span, ): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); Loading Loading @@ -3350,6 +3350,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { ) { expandedRecipients.push(recipient); } if (options.syncCollection) { const collectionId = this.federation.router.build( "followers", { identifier, handle: identifier }, Loading @@ -3357,6 +3358,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { opts.collectionSync = collectionId == null ? undefined : new URL(collectionId, this.canonicalOrigin).href; } } else { expandedRecipients = [recipients]; } Loading Loading
CHANGES.md +10 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,15 @@ To be released. - Added `FederationOrigin` interface. - Added `Context.canonicalOrigin` property. - Followers collection synchronization ([FEP-8fcf]) is now turned off by default. - Added `SendActivityOptionsForCollection` interface. - The type of `Context.sendActivity({ identifier: string } | { username: string } | { handle: string }, "followers", Activity)` overload's fourth parameter became `SendActivityOptionsForCollection | undefined` (was `SendActivityOptions | undefined`). - Fedify now accepts PEM-PKCS#1 besides PEM-SPKI for RSA public keys. [[#209]] Loading Loading @@ -102,6 +111,7 @@ To be released. [#220]: https://github.com/fedify-dev/fedify/issues/220 [#221]: https://github.com/fedify-dev/fedify/issues/221 [#223]: https://github.com/fedify-dev/fedify/pull/223 [FEP-8fcf]: https://w3id.org/fep/8fcf [multibase]: https://github.com/multiformats/js-multibase Loading
docs/manual/send.md +29 −16 Original line number Diff line number Diff line Loading @@ -262,6 +262,15 @@ await ctx.sendActivity( > an array of `SenderKeyPair` objects. You need to specify the recipients > manually in this case. > [!TIP] > Does the `Context.sendActivity()` method takes quite a long time to complete > even if you configured the [`queue`](./federation.md#queue)? It might be > because the followers collection is large and the method under the hood > invokes your [followers collection dispatcher](./collections.md#followers) > multiple times to paginate the collection. To improve the performance, > you should implement the [one-short followers collection for gathering > recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients). Specifying an activity ---------------------- Loading Loading @@ -593,7 +602,8 @@ async function sendNote( Followers collection synchronization ------------------------------------ *This API is available since Fedify 0.8.0.* *This API is available since Fedify 0.8.0, and it is optional since Fedify 1.5.0.* > [!NOTE] > For efficiency, you should implement Loading @@ -612,7 +622,8 @@ so that the recipient server can check if it needs to resynchronize the followers collection. Fedify provides a way to include the digest of the followers collection in the activity delivery request by specifying the recipients parameter of the `~Context.sendActivity()` method as the `"followers"` string: the `"followers"` string and turning on the `~SendActivityOptionsForCollection.syncCollection` option: ~~~~ typescript twoslash import { type Context, Create, Note } from "@fedify/fedify"; Loading @@ -630,13 +641,17 @@ await ctx.sendActivity( to: ctx.getFollowersUri(senderId), }), }), { preferSharedInbox: true }, // [!code highlight] { preferSharedInbox: true, // [!code highlight] syncCollection: true, // [!code highlight] }, ); ~~~~ If you specify the `"followers"` string as the recipients parameter, it automatically sends the activity to the sender's followers and includes the digest of the followers collection in the payload. The `~SendActivityOptionsForCollection.syncCollection` option is only available when you specify the `"followers"` string as the recipients parameter. With turning on this option, it automatically sends the activity to the sender's followers and includes the digest of the followers collection in the payload. > [!NOTE] > The `to` and `cc` properties of an `Activity` and its `object` should be set Loading @@ -645,14 +660,12 @@ the digest of the followers collection in the payload. > the `PUBLIC_COLLECTION`, the activity is visible to everyone regardless of > the recipients parameter. > [!TIP] > Does the `Context.sendActivity()` method takes quite a long time to complete > even if you configured the [`queue`](./federation.md#queue)? It might be > because the followers collection is large and the method under the hood > invokes your [followers collection dispatcher](./collections.md#followers) > multiple times to paginate the collection. To improve the performance, > you should implement the [one-short followers collection for gathering > recipients](./collections.md#one-shot-followers-collection-for-gathering-recipients). > [!NOTE] > Some history of this feature: The followers collection synchronization was > first introduced in Fedify 0.8.0, but it was automatically turned on when > the recipients parameter was set to the `"followers"` string then. > Since Fedify 1.5.0, it is optional, and you need to explicitly turn on > the `~SendActivityOptionsForCollection.syncCollection` option to use it. [FEP-8fcf]: https://w3id.org/fep/8fcf Loading
src/federation/context.ts +15 −1 Original line number Diff line number Diff line Loading @@ -360,7 +360,7 @@ export interface Context<TContextData> { sender: { identifier: string } | { username: string } | { handle: string }, recipients: "followers", activity: Activity, options?: SendActivityOptions, options?: SendActivityOptionsForCollection, ): Promise<void>; /** Loading Loading @@ -697,6 +697,20 @@ export interface SendActivityOptions { excludeBaseUris?: URL[]; } /** * Options for {@link Context.sendActivity} method when sending to a collection. * @since 1.5.0 */ export interface SendActivityOptionsForCollection extends SendActivityOptions { /** * Whether to synchronize the collection using `Collection-Synchronization` * header ([FEP-8fcf]). * * [FEP-8fcf]: https://w3id.org/fep/8fcf */ syncCollection?: boolean; } /** * Options for {@link InboxContext.forwardActivity} method. * @since 1.0.0 Loading
src/federation/middleware.test.ts +64 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,7 @@ import { assertEquals, assertFalse, assertInstanceOf, assertNotEquals, assertRejects, assertStrictEquals, assertThrows, Loading Loading @@ -1456,9 +1457,11 @@ test("ContextImpl.sendActivity()", async (t) => { let verified: ("http" | "ld" | "proof")[] | null = null; let request: Request | null = null; let collectionSyncHeader: string | null = null; mf.mock("POST@/inbox", async (req) => { verified = []; request = req.clone(); collectionSyncHeader = req.headers.get("Collection-Synchronization"); const options = { async documentLoader(url: string) { const response = await federation.fetch( Loading Loading @@ -1541,6 +1544,18 @@ test("ContextImpl.sendActivity()", async (t) => { }) .mapHandle((_ctx, username) => username === "john" ? "1" : null); federation.setFollowersDispatcher( "/users/{identifier}/followers", () => ({ items: [ { id: new URL("https://example.com/recipient"), inboxId: new URL("https://example.com/inbox"), }, ], }), ); await t.step("success", async () => { const activity = new Create({ actor: new URL("https://example.com/person"), Loading Loading @@ -1816,6 +1831,55 @@ test("ContextImpl.sendActivity()", async (t) => { }, ]); }); collectionSyncHeader = null; await t.step("followers collection without syncCollection", async () => { const ctx = new ContextImpl({ data: undefined, federation, url: new URL("https://example.com/"), documentLoader: fetchDocumentLoader, contextLoader: fetchDocumentLoader, }); const activity = new Create({ id: new URL("https://example.com/activity/1"), actor: ctx.getActorUri("1"), to: ctx.getFollowersUri("1"), }); await ctx.sendActivity({ identifier: "1" }, "followers", activity); assertEquals(collectionSyncHeader, null); }); collectionSyncHeader = null; await t.step("followers collection with syncCollection", async () => { const ctx = new ContextImpl({ data: undefined, federation, url: new URL("https://example.com/"), documentLoader: fetchDocumentLoader, contextLoader: fetchDocumentLoader, }); const activity = new Create({ id: new URL("https://example.com/activity/2"), actor: ctx.getActorUri("1"), to: ctx.getFollowersUri("1"), }); await ctx.sendActivity( { identifier: "1" }, "followers", activity, { syncCollection: true, preferSharedInbox: true }, ); assertNotEquals(collectionSyncHeader, null); }); }); test("ContextImpl.routeActivity()", async () => { Loading
src/federation/middleware.ts +12 −10 Original line number Diff line number Diff line Loading @@ -83,7 +83,7 @@ import type { ParseUriResult, RequestContext, RouteActivityOptions, SendActivityOptions, SendActivityOptionsForCollection, } from "./context.ts"; import type { ActorCallbackSetters, Loading Loading @@ -3221,7 +3221,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, options: SendActivityOptionsForCollection = {}, ): Promise<void> { const tracer = this.tracerProvider.getTracer( metadata.name, Loading Loading @@ -3274,7 +3274,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { | { handle: string }, recipients: Recipient | Recipient[] | "followers", activity: Activity, options: SendActivityOptions = {}, options: SendActivityOptionsForCollection, span: Span, ): Promise<void> { const logger = getLogger(["fedify", "federation", "outbox"]); Loading Loading @@ -3350,6 +3350,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { ) { expandedRecipients.push(recipient); } if (options.syncCollection) { const collectionId = this.federation.router.build( "followers", { identifier, handle: identifier }, Loading @@ -3357,6 +3358,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> { opts.collectionSync = collectionId == null ? undefined : new URL(collectionId, this.canonicalOrigin).href; } } else { expandedRecipients = [recipients]; } Loading