Loading examples/blog/federation/mod.ts +40 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,12 @@ import { Undo, } from "fedify/vocab/mod.ts"; import { getBlog } from "../models/blog.ts"; import { addFollower, removeFollower } from "../models/follower.ts"; import { addFollower, countFollowers, getFollowers, removeFollower, } from "../models/follower.ts"; import { openKv } from "../models/kv.ts"; import { countPosts, getPosts, toNote } from "../models/post.ts"; Loading @@ -37,6 +42,7 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { url: new URL("/", ctx.request.url), outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), followers: ctx.getFollowersUri(handle), publicKey: key, }); }) Loading Loading @@ -148,3 +154,36 @@ federation.setInboxListeners("/users/{handle}/inbox") } }) .onError((e) => console.error(e)); federation .setFollowersDispatcher( "/users/{handle}/followers", async (_ctx, handle, cursor) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; if (cursor == null) return null; const { followers, nextCursor } = await getFollowers( undefined, // Treat the empty string as the first cursor: cursor === "" ? undefined : cursor, ); return { items: followers.map((f) => new URL(f.id)), nextCursor, }; }, ) .setCounter(async (_ctx, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; return await countFollowers(); }) .setFirstCursor(async (_ctx, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; // Treat the empty string as the first cursor: return ""; }); federation/callback.ts +7 −7 Original line number Diff line number Diff line Loading @@ -22,26 +22,26 @@ export type ActorKeyPairDispatcher<TContextData> = ( ) => CryptoKeyPair | null | Promise<CryptoKeyPair | null>; /** * A callback that dispatches an outbox. * A callback that dispatches a collection. */ export type OutboxDispatcher<TContextData> = ( export type CollectionDispatcher<TItem, TContextData> = ( context: RequestContext<TContextData>, handle: string, cursor: string | null, ) => Page<Activity> | null | Promise<Page<Activity> | null>; ) => Page<TItem> | null | Promise<Page<TItem> | null>; /** * A callback that counts the number of activities in an outbox. * A callback that counts the number of items in a collection. */ export type OutboxCounter<TContextData> = ( export type CollectionCounter<TContextData> = ( context: RequestContext<TContextData>, handle: string, ) => number | bigint | null | Promise<number | bigint | null>; /** * A callback that returns a cursor for an outbox. * A callback that returns a cursor for a collection. */ export type OutboxCursor<TContextData> = ( export type CollectionCursor<TContextData> = ( context: RequestContext<TContextData>, handle: string, ) => string | null | Promise<string | null>; Loading federation/context.ts +7 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,13 @@ export interface Context<TContextData> { */ getInboxUri(handle: string): URL; /** * Builds the URI of an actor's followers collection with the given handle. * @param handle The actor's handle. * @returns The actor's followers collection URI. */ getFollowersUri(handle: string): URL; /** * Gets a public {@link CryptographicKey} for an actor, if any exists. * @param handle The actor's handle. Loading federation/handler.ts +46 −41 Original line number Diff line number Diff line import { accepts } from "https://deno.land/std@0.217.0/http/mod.ts"; import { ActorDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, InboxListener, OutboxCounter, OutboxCursor, Loading @@ -12,6 +15,8 @@ import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; Loading Loading @@ -70,33 +75,36 @@ export async function handleActor<TContextData>( }); } export interface OutboxHandlerParameters<TContextData> { export interface CollectionHandlerParameters<TItem, TContextData> { handle: string; context: RequestContext<TContextData>; documentLoader: DocumentLoader; outboxDispatcher?: OutboxDispatcher<TContextData>; outboxCounter?: OutboxCounter<TContextData>; outboxFirstCursor?: OutboxCursor<TContextData>; outboxLastCursor?: OutboxCursor<TContextData>; collectionDispatcher?: CollectionDispatcher<TItem, TContextData>; collectionCounter?: CollectionCounter<TContextData>; collectionFirstCursor?: CollectionCursor<TContextData>; collectionLastCursor?: CollectionCursor<TContextData>; onNotFound(request: Request): Response | Promise<Response>; onNotAcceptable(request: Request): Response | Promise<Response>; } export async function handleOutbox<TContextData>( export async function handleCollection< TItem extends URL | Object | Link, TContextData, >( request: Request, { handle, context, documentLoader, outboxCounter, outboxFirstCursor, outboxLastCursor, outboxDispatcher, collectionDispatcher, collectionCounter, collectionFirstCursor, collectionLastCursor, onNotFound, onNotAcceptable, }: OutboxHandlerParameters<TContextData>, }: CollectionHandlerParameters<TItem, TContextData>, ): Promise<Response> { if (outboxDispatcher == null) { if (collectionDispatcher == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; } Loading @@ -108,16 +116,16 @@ export async function handleOutbox<TContextData>( const cursor = url.searchParams.get("cursor"); let collection: OrderedCollection | OrderedCollectionPage; if (cursor == null) { const firstCursorPromise = outboxFirstCursor?.(context, handle); const firstCursorPromise = collectionFirstCursor?.(context, handle); const firstCursor = firstCursorPromise instanceof Promise ? await firstCursorPromise : firstCursorPromise; const totalItemsPromise = outboxCounter?.(context, handle); const totalItemsPromise = collectionCounter?.(context, handle); const totalItems = totalItemsPromise instanceof Promise ? await totalItemsPromise : totalItemsPromise; if (firstCursor == null) { const pagePromise = outboxDispatcher(context, handle, null); const pagePromise = collectionDispatcher(context, handle, null); const page = pagePromise instanceof Promise ? await pagePromise : pagePromise; Loading @@ -131,26 +139,25 @@ export async function handleOutbox<TContextData>( items, }); } else { const lastCursorPromise = outboxLastCursor?.(context, handle); const lastCursorPromise = collectionLastCursor?.(context, handle); const lastCursor = lastCursorPromise instanceof Promise ? await lastCursorPromise : lastCursorPromise; const first = new URL(context.url); first.searchParams.set("cursor", firstCursor); let last = null; if (lastCursor != null) { last = new URL(context.url); last.searchParams.set("cursor", lastCursor); } collection = new OrderedCollection({ totalItems: Number(totalItems), first: new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(firstCursor) }`, ), last: lastCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(lastCursor) }`, ), first, last, }); } } else { const pagePromise = outboxDispatcher(context, handle, cursor); const pagePromise = collectionDispatcher(context, handle, cursor); const page = pagePromise instanceof Promise ? await pagePromise : pagePromise; Loading @@ -159,19 +166,17 @@ export async function handleOutbox<TContextData>( return response instanceof Promise ? await response : response; } const { items, prevCursor, nextCursor } = page; collection = new OrderedCollectionPage({ prev: prevCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(prevCursor) }`, ), next: nextCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(nextCursor) }`, ), items, }); let prev = null; if (prevCursor != null) { prev = new URL(context.url); prev.searchParams.set("cursor", prevCursor); } let next = null; if (nextCursor != null) { next = new URL(context.url); next.searchParams.set("cursor", nextCursor); } collection = new OrderedCollectionPage({ prev, next, items }); } const jsonLd = await collection.toJsonLd({ documentLoader }); return new Response(JSON.stringify(jsonLd), { Loading Loading @@ -293,7 +298,7 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } cls = Object.getPrototypeOf(cls); cls = globalThis.Object.getPrototypeOf(cls); } const listener = inboxListeners.get(cls)!; const promise = listener(context, activity); Loading federation/middleware.ts +95 −30 Original line number Diff line number Diff line Loading @@ -10,13 +10,13 @@ import { handleWebFinger } from "../webfinger/handler.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, InboxListener, OutboxCounter, OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context, RequestContext } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { handleActor, handleCollection, handleInbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; Loading Loading @@ -60,7 +60,8 @@ export class Federation<TContextData> { #kvPrefixes: FederationKvPrefixes; #router: Router; #actorCallbacks?: ActorCallbacks<TContextData>; #outboxCallbacks?: OutboxCallbacks<TContextData>; #outboxCallbacks?: CollectionCallbacks<Activity, TContextData>; #followersCallbacks?: CollectionCallbacks<Actor | URL, TContextData>; #inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading Loading @@ -201,6 +202,13 @@ export class Federation<TContextData> { } return new URL(path, url); }, getFollowersUri: (handle: string): URL => { const path = this.#router.build("followers", { handle }); if (path == null) { throw new RouterError("No followers collection path registered."); } return new URL(path, url); }, getActorKey: async (handle: string): Promise<CryptographicKey | null> => { let keyPair = this.#actorCallbacks?.keyPairDispatcher?.( contextData, Loading Loading @@ -303,8 +311,8 @@ export class Federation<TContextData> { */ setOutboxDispatcher( path: string, dispatcher: OutboxDispatcher<TContextData>, ): OutboxCallbackSetters<TContextData> { dispatcher: CollectionDispatcher<Activity, TContextData>, ): CollectionCallbackSetters<TContextData> { if (this.#router.has("outbox")) { throw new RouterError("Outbox dispatcher already set."); } Loading @@ -314,18 +322,63 @@ export class Federation<TContextData> { "Path for outbox dispatcher must have one variable: {handle}", ); } const callbacks: OutboxCallbacks<TContextData> = { dispatcher }; const callbacks: CollectionCallbacks<Activity, TContextData> = { dispatcher, }; this.#outboxCallbacks = callbacks; const setters: OutboxCallbackSetters<TContextData> = { setCounter(counter: OutboxCounter<TContextData>) { const setters: CollectionCallbackSetters<TContextData> = { setCounter(counter: CollectionCounter<TContextData>) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor: CollectionCursor<TContextData>) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor: CollectionCursor<TContextData>) { callbacks.lastCursor = cursor; return setters; }, }; return setters; } /** * Registers an followers collection dispatcher. * @param path The URI path pattern for the followers collection. The syntax * is based on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path * must have one variable: `{handle}`. * @param dispatcher An outbox collection callback to register. * @throws {@link RouterError} Thrown if the path pattern is invalid. */ setFollowersDispatcher( path: string, dispatcher: CollectionDispatcher<Actor | URL, TContextData>, ): CollectionCallbackSetters<TContextData> { if (this.#router.has("followers")) { throw new RouterError("Followers collection dispatcher already set."); } const variables = this.#router.add(path, "followers"); if (variables.size !== 1 || !variables.has("handle")) { throw new RouterError( "Path for followers collection dispatcher must have one variable: {handle}", ); } const callbacks: CollectionCallbacks<Actor | URL, TContextData> = { dispatcher, }; this.#followersCallbacks = callbacks; const setters: CollectionCallbackSetters<TContextData> = { setCounter(counter: CollectionCounter<TContextData>) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor: OutboxCursor<TContextData>) { setFirstCursor(cursor: CollectionCursor<TContextData>) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor: OutboxCursor<TContextData>) { setLastCursor(cursor: CollectionCursor<TContextData>) { callbacks.lastCursor = cursor; return setters; }, Loading Loading @@ -439,14 +492,14 @@ export class Federation<TContextData> { onNotAcceptable, }); case "outbox": return await handleOutbox(request, { return await handleCollection(request, { handle: route.values.handle, context, documentLoader: this.#documentLoader, outboxDispatcher: this.#outboxCallbacks?.dispatcher, outboxCounter: this.#outboxCallbacks?.counter, outboxFirstCursor: this.#outboxCallbacks?.firstCursor, outboxLastCursor: this.#outboxCallbacks?.lastCursor, collectionDispatcher: this.#outboxCallbacks?.dispatcher, collectionCounter: this.#outboxCallbacks?.counter, collectionFirstCursor: this.#outboxCallbacks?.firstCursor, collectionLastCursor: this.#outboxCallbacks?.lastCursor, onNotFound, onNotAcceptable, }); Loading @@ -462,6 +515,18 @@ export class Federation<TContextData> { inboxErrorHandler: this.#inboxErrorHandler, onNotFound, }); case "followers": return await handleCollection(request, { handle: route.values.handle, context, documentLoader: this.#documentLoader, collectionDispatcher: this.#followersCallbacks?.dispatcher, collectionCounter: this.#followersCallbacks?.counter, collectionFirstCursor: this.#followersCallbacks?.firstCursor, collectionLastCursor: this.#followersCallbacks?.lastCursor, onNotFound, onNotAcceptable, }); default: { const response = onNotFound(request); return response instanceof Promise ? await response : response; Loading Loading @@ -505,28 +570,28 @@ export interface ActorCallbackSetters<TContextData> { ): ActorCallbackSetters<TContextData>; } interface OutboxCallbacks<TContextData> { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; interface CollectionCallbacks<TItem, TContextData> { dispatcher: CollectionDispatcher<TItem, TContextData>; counter?: CollectionCounter<TContextData>; firstCursor?: CollectionCursor<TContextData>; lastCursor?: CollectionCursor<TContextData>; } /** * Additional settings for the outbox dispatcher. * Additional settings for a collection dispatcher. */ export interface OutboxCallbackSetters<TContextData> { export interface CollectionCallbackSetters<TContextData> { setCounter( counter: OutboxCounter<TContextData>, ): OutboxCallbackSetters<TContextData>; counter: CollectionCounter<TContextData>, ): CollectionCallbackSetters<TContextData>; setFirstCursor( cursor: OutboxCursor<TContextData>, ): OutboxCallbackSetters<TContextData>; cursor: CollectionCursor<TContextData>, ): CollectionCallbackSetters<TContextData>; setLastCursor( cursor: OutboxCursor<TContextData>, ): OutboxCallbackSetters<TContextData>; cursor: CollectionCursor<TContextData>, ): CollectionCallbackSetters<TContextData>; } /** Loading Loading
examples/blog/federation/mod.ts +40 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,12 @@ import { Undo, } from "fedify/vocab/mod.ts"; import { getBlog } from "../models/blog.ts"; import { addFollower, removeFollower } from "../models/follower.ts"; import { addFollower, countFollowers, getFollowers, removeFollower, } from "../models/follower.ts"; import { openKv } from "../models/kv.ts"; import { countPosts, getPosts, toNote } from "../models/post.ts"; Loading @@ -37,6 +42,7 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { url: new URL("/", ctx.request.url), outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), followers: ctx.getFollowersUri(handle), publicKey: key, }); }) Loading Loading @@ -148,3 +154,36 @@ federation.setInboxListeners("/users/{handle}/inbox") } }) .onError((e) => console.error(e)); federation .setFollowersDispatcher( "/users/{handle}/followers", async (_ctx, handle, cursor) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; if (cursor == null) return null; const { followers, nextCursor } = await getFollowers( undefined, // Treat the empty string as the first cursor: cursor === "" ? undefined : cursor, ); return { items: followers.map((f) => new URL(f.id)), nextCursor, }; }, ) .setCounter(async (_ctx, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; return await countFollowers(); }) .setFirstCursor(async (_ctx, handle) => { const blog = await getBlog(); if (blog == null) return null; else if (blog.handle !== handle) return null; // Treat the empty string as the first cursor: return ""; });
federation/callback.ts +7 −7 Original line number Diff line number Diff line Loading @@ -22,26 +22,26 @@ export type ActorKeyPairDispatcher<TContextData> = ( ) => CryptoKeyPair | null | Promise<CryptoKeyPair | null>; /** * A callback that dispatches an outbox. * A callback that dispatches a collection. */ export type OutboxDispatcher<TContextData> = ( export type CollectionDispatcher<TItem, TContextData> = ( context: RequestContext<TContextData>, handle: string, cursor: string | null, ) => Page<Activity> | null | Promise<Page<Activity> | null>; ) => Page<TItem> | null | Promise<Page<TItem> | null>; /** * A callback that counts the number of activities in an outbox. * A callback that counts the number of items in a collection. */ export type OutboxCounter<TContextData> = ( export type CollectionCounter<TContextData> = ( context: RequestContext<TContextData>, handle: string, ) => number | bigint | null | Promise<number | bigint | null>; /** * A callback that returns a cursor for an outbox. * A callback that returns a cursor for a collection. */ export type OutboxCursor<TContextData> = ( export type CollectionCursor<TContextData> = ( context: RequestContext<TContextData>, handle: string, ) => string | null | Promise<string | null>; Loading
federation/context.ts +7 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,13 @@ export interface Context<TContextData> { */ getInboxUri(handle: string): URL; /** * Builds the URI of an actor's followers collection with the given handle. * @param handle The actor's handle. * @returns The actor's followers collection URI. */ getFollowersUri(handle: string): URL; /** * Gets a public {@link CryptographicKey} for an actor, if any exists. * @param handle The actor's handle. Loading
federation/handler.ts +46 −41 Original line number Diff line number Diff line import { accepts } from "https://deno.land/std@0.217.0/http/mod.ts"; import { ActorDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, InboxListener, OutboxCounter, OutboxCursor, Loading @@ -12,6 +15,8 @@ import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; Loading Loading @@ -70,33 +75,36 @@ export async function handleActor<TContextData>( }); } export interface OutboxHandlerParameters<TContextData> { export interface CollectionHandlerParameters<TItem, TContextData> { handle: string; context: RequestContext<TContextData>; documentLoader: DocumentLoader; outboxDispatcher?: OutboxDispatcher<TContextData>; outboxCounter?: OutboxCounter<TContextData>; outboxFirstCursor?: OutboxCursor<TContextData>; outboxLastCursor?: OutboxCursor<TContextData>; collectionDispatcher?: CollectionDispatcher<TItem, TContextData>; collectionCounter?: CollectionCounter<TContextData>; collectionFirstCursor?: CollectionCursor<TContextData>; collectionLastCursor?: CollectionCursor<TContextData>; onNotFound(request: Request): Response | Promise<Response>; onNotAcceptable(request: Request): Response | Promise<Response>; } export async function handleOutbox<TContextData>( export async function handleCollection< TItem extends URL | Object | Link, TContextData, >( request: Request, { handle, context, documentLoader, outboxCounter, outboxFirstCursor, outboxLastCursor, outboxDispatcher, collectionDispatcher, collectionCounter, collectionFirstCursor, collectionLastCursor, onNotFound, onNotAcceptable, }: OutboxHandlerParameters<TContextData>, }: CollectionHandlerParameters<TItem, TContextData>, ): Promise<Response> { if (outboxDispatcher == null) { if (collectionDispatcher == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; } Loading @@ -108,16 +116,16 @@ export async function handleOutbox<TContextData>( const cursor = url.searchParams.get("cursor"); let collection: OrderedCollection | OrderedCollectionPage; if (cursor == null) { const firstCursorPromise = outboxFirstCursor?.(context, handle); const firstCursorPromise = collectionFirstCursor?.(context, handle); const firstCursor = firstCursorPromise instanceof Promise ? await firstCursorPromise : firstCursorPromise; const totalItemsPromise = outboxCounter?.(context, handle); const totalItemsPromise = collectionCounter?.(context, handle); const totalItems = totalItemsPromise instanceof Promise ? await totalItemsPromise : totalItemsPromise; if (firstCursor == null) { const pagePromise = outboxDispatcher(context, handle, null); const pagePromise = collectionDispatcher(context, handle, null); const page = pagePromise instanceof Promise ? await pagePromise : pagePromise; Loading @@ -131,26 +139,25 @@ export async function handleOutbox<TContextData>( items, }); } else { const lastCursorPromise = outboxLastCursor?.(context, handle); const lastCursorPromise = collectionLastCursor?.(context, handle); const lastCursor = lastCursorPromise instanceof Promise ? await lastCursorPromise : lastCursorPromise; const first = new URL(context.url); first.searchParams.set("cursor", firstCursor); let last = null; if (lastCursor != null) { last = new URL(context.url); last.searchParams.set("cursor", lastCursor); } collection = new OrderedCollection({ totalItems: Number(totalItems), first: new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(firstCursor) }`, ), last: lastCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(lastCursor) }`, ), first, last, }); } } else { const pagePromise = outboxDispatcher(context, handle, cursor); const pagePromise = collectionDispatcher(context, handle, cursor); const page = pagePromise instanceof Promise ? await pagePromise : pagePromise; Loading @@ -159,19 +166,17 @@ export async function handleOutbox<TContextData>( return response instanceof Promise ? await response : response; } const { items, prevCursor, nextCursor } = page; collection = new OrderedCollectionPage({ prev: prevCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(prevCursor) }`, ), next: nextCursor == null ? null : new URL( `${context.getOutboxUri(handle).href}?cursor=${ encodeURIComponent(nextCursor) }`, ), items, }); let prev = null; if (prevCursor != null) { prev = new URL(context.url); prev.searchParams.set("cursor", prevCursor); } let next = null; if (nextCursor != null) { next = new URL(context.url); next.searchParams.set("cursor", nextCursor); } collection = new OrderedCollectionPage({ prev, next, items }); } const jsonLd = await collection.toJsonLd({ documentLoader }); return new Response(JSON.stringify(jsonLd), { Loading Loading @@ -293,7 +298,7 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } cls = Object.getPrototypeOf(cls); cls = globalThis.Object.getPrototypeOf(cls); } const listener = inboxListeners.get(cls)!; const promise = listener(context, activity); Loading
federation/middleware.ts +95 −30 Original line number Diff line number Diff line Loading @@ -10,13 +10,13 @@ import { handleWebFinger } from "../webfinger/handler.ts"; import { ActorDispatcher, ActorKeyPairDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, InboxListener, OutboxCounter, OutboxCursor, OutboxDispatcher, } from "./callback.ts"; import { Context, RequestContext } from "./context.ts"; import { handleActor, handleInbox, handleOutbox } from "./handler.ts"; import { handleActor, handleCollection, handleInbox } from "./handler.ts"; import { OutboxMessage } from "./queue.ts"; import { Router, RouterError } from "./router.ts"; import { extractInboxes, sendActivity } from "./send.ts"; Loading Loading @@ -60,7 +60,8 @@ export class Federation<TContextData> { #kvPrefixes: FederationKvPrefixes; #router: Router; #actorCallbacks?: ActorCallbacks<TContextData>; #outboxCallbacks?: OutboxCallbacks<TContextData>; #outboxCallbacks?: CollectionCallbacks<Activity, TContextData>; #followersCallbacks?: CollectionCallbacks<Actor | URL, TContextData>; #inboxListeners: Map< new (...args: unknown[]) => Activity, InboxListener<TContextData, Activity> Loading Loading @@ -201,6 +202,13 @@ export class Federation<TContextData> { } return new URL(path, url); }, getFollowersUri: (handle: string): URL => { const path = this.#router.build("followers", { handle }); if (path == null) { throw new RouterError("No followers collection path registered."); } return new URL(path, url); }, getActorKey: async (handle: string): Promise<CryptographicKey | null> => { let keyPair = this.#actorCallbacks?.keyPairDispatcher?.( contextData, Loading Loading @@ -303,8 +311,8 @@ export class Federation<TContextData> { */ setOutboxDispatcher( path: string, dispatcher: OutboxDispatcher<TContextData>, ): OutboxCallbackSetters<TContextData> { dispatcher: CollectionDispatcher<Activity, TContextData>, ): CollectionCallbackSetters<TContextData> { if (this.#router.has("outbox")) { throw new RouterError("Outbox dispatcher already set."); } Loading @@ -314,18 +322,63 @@ export class Federation<TContextData> { "Path for outbox dispatcher must have one variable: {handle}", ); } const callbacks: OutboxCallbacks<TContextData> = { dispatcher }; const callbacks: CollectionCallbacks<Activity, TContextData> = { dispatcher, }; this.#outboxCallbacks = callbacks; const setters: OutboxCallbackSetters<TContextData> = { setCounter(counter: OutboxCounter<TContextData>) { const setters: CollectionCallbackSetters<TContextData> = { setCounter(counter: CollectionCounter<TContextData>) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor: CollectionCursor<TContextData>) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor: CollectionCursor<TContextData>) { callbacks.lastCursor = cursor; return setters; }, }; return setters; } /** * Registers an followers collection dispatcher. * @param path The URI path pattern for the followers collection. The syntax * is based on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). The path * must have one variable: `{handle}`. * @param dispatcher An outbox collection callback to register. * @throws {@link RouterError} Thrown if the path pattern is invalid. */ setFollowersDispatcher( path: string, dispatcher: CollectionDispatcher<Actor | URL, TContextData>, ): CollectionCallbackSetters<TContextData> { if (this.#router.has("followers")) { throw new RouterError("Followers collection dispatcher already set."); } const variables = this.#router.add(path, "followers"); if (variables.size !== 1 || !variables.has("handle")) { throw new RouterError( "Path for followers collection dispatcher must have one variable: {handle}", ); } const callbacks: CollectionCallbacks<Actor | URL, TContextData> = { dispatcher, }; this.#followersCallbacks = callbacks; const setters: CollectionCallbackSetters<TContextData> = { setCounter(counter: CollectionCounter<TContextData>) { callbacks.counter = counter; return setters; }, setFirstCursor(cursor: OutboxCursor<TContextData>) { setFirstCursor(cursor: CollectionCursor<TContextData>) { callbacks.firstCursor = cursor; return setters; }, setLastCursor(cursor: OutboxCursor<TContextData>) { setLastCursor(cursor: CollectionCursor<TContextData>) { callbacks.lastCursor = cursor; return setters; }, Loading Loading @@ -439,14 +492,14 @@ export class Federation<TContextData> { onNotAcceptable, }); case "outbox": return await handleOutbox(request, { return await handleCollection(request, { handle: route.values.handle, context, documentLoader: this.#documentLoader, outboxDispatcher: this.#outboxCallbacks?.dispatcher, outboxCounter: this.#outboxCallbacks?.counter, outboxFirstCursor: this.#outboxCallbacks?.firstCursor, outboxLastCursor: this.#outboxCallbacks?.lastCursor, collectionDispatcher: this.#outboxCallbacks?.dispatcher, collectionCounter: this.#outboxCallbacks?.counter, collectionFirstCursor: this.#outboxCallbacks?.firstCursor, collectionLastCursor: this.#outboxCallbacks?.lastCursor, onNotFound, onNotAcceptable, }); Loading @@ -462,6 +515,18 @@ export class Federation<TContextData> { inboxErrorHandler: this.#inboxErrorHandler, onNotFound, }); case "followers": return await handleCollection(request, { handle: route.values.handle, context, documentLoader: this.#documentLoader, collectionDispatcher: this.#followersCallbacks?.dispatcher, collectionCounter: this.#followersCallbacks?.counter, collectionFirstCursor: this.#followersCallbacks?.firstCursor, collectionLastCursor: this.#followersCallbacks?.lastCursor, onNotFound, onNotAcceptable, }); default: { const response = onNotFound(request); return response instanceof Promise ? await response : response; Loading Loading @@ -505,28 +570,28 @@ export interface ActorCallbackSetters<TContextData> { ): ActorCallbackSetters<TContextData>; } interface OutboxCallbacks<TContextData> { dispatcher: OutboxDispatcher<TContextData>; counter?: OutboxCounter<TContextData>; firstCursor?: OutboxCursor<TContextData>; lastCursor?: OutboxCursor<TContextData>; interface CollectionCallbacks<TItem, TContextData> { dispatcher: CollectionDispatcher<TItem, TContextData>; counter?: CollectionCounter<TContextData>; firstCursor?: CollectionCursor<TContextData>; lastCursor?: CollectionCursor<TContextData>; } /** * Additional settings for the outbox dispatcher. * Additional settings for a collection dispatcher. */ export interface OutboxCallbackSetters<TContextData> { export interface CollectionCallbackSetters<TContextData> { setCounter( counter: OutboxCounter<TContextData>, ): OutboxCallbackSetters<TContextData>; counter: CollectionCounter<TContextData>, ): CollectionCallbackSetters<TContextData>; setFirstCursor( cursor: OutboxCursor<TContextData>, ): OutboxCallbackSetters<TContextData>; cursor: CollectionCursor<TContextData>, ): CollectionCallbackSetters<TContextData>; setLastCursor( cursor: OutboxCursor<TContextData>, ): OutboxCallbackSetters<TContextData>; cursor: CollectionCursor<TContextData>, ): CollectionCallbackSetters<TContextData>; } /** Loading