Loading examples/blog/.vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ "fedify", "fediverse", "preact", "unfollowing", "uuidv7" ] } examples/blog/federation/mod.ts +27 −9 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import { Accept, Activity, Create, Endpoints, Follow, Link, Person, Loading Loading @@ -40,10 +41,20 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { summary: blog.description, preferredUsername: handle, url: new URL("/", ctx.request.url), // A `Context<TContextData>` object has several purposes, and one of // them is to provide a way to generate URIs for the dispatchers and // the collections: outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), following: ctx.getFollowingUri(handle), followers: ctx.getFollowersUri(handle), // The `key` parameter is the public key of the actor, which is used // for the HTTP Signatures. Note that the `key` object is not a // `CryptoKey` instance, but a `CryptographicKey` instance which is // used for ActivityPub: publicKey: key, }); }) Loading Loading @@ -105,7 +116,11 @@ federation.setOutboxDispatcher( return ""; }); federation.setInboxListeners("/users/{handle}/inbox") // Registers the inbox listeners, which are responsible for handling // incoming activities in the inbox: federation.setInboxListeners("/users/{handle}/inbox", "/inbox") // The `Follow` activity is handled by adding the follower to the // follower list: .on(Follow, async (ctx, follow) => { const blog = await getBlog(); if (blog == null) return; Loading Loading @@ -136,20 +151,23 @@ federation.setInboxListeners("/users/{handle}/inbox") sharedInbox: recipient.endpoints?.sharedInbox?.href, typeName: getActorTypeName(recipient), }); // Note that if a server receives a `Follow` activity, it should reply // with either an `Accept` or a `Reject` activity. In this case, the // server automatically accepts the follow request: await ctx.sendActivity( { handle: blog.handle }, recipient, new Accept({ actor: actorUri, object: follow, }), new Accept({ actor: actorUri, object: follow }), ); }) // The `Undo` activity purposes to undo the previous activity. In this // project, we use the `Undo` activity to represent someone unfollowing // the blog: .on(Undo, async (ctx, undo) => { const object = await undo.getObject(ctx); if (object instanceof Follow) { if (object.id == null) return; await removeFollower(object.id.href); const activity = await undo.getObject(ctx); // An `Activity` to undo if (activity instanceof Follow) { if (activity.id == null) return; await removeFollower(activity.id.href); } else { console.debug(undo); } Loading federation/context.ts +6 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,12 @@ export interface Context<TContextData> { */ getOutboxUri(handle: string): URL; /** * Builds the URI of the shared inbox. * @returns The shared inbox URI. */ getInboxUri(): URL; /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. Loading federation/handler.ts +2 −2 Original line number Diff line number Diff line Loading @@ -211,7 +211,7 @@ export async function handleCollection< } export interface InboxHandlerParameters<TContextData> { handle: string; handle: string | null; context: RequestContext<TContextData>; kv: Deno.Kv; kvPrefix: Deno.KvKey; Loading Loading @@ -242,7 +242,7 @@ export async function handleInbox<TContextData>( if (actorDispatcher == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; } else { } else if (handle != null) { const key = await context.getActorKey(handle); const promise = actorDispatcher(context, handle, key); const actor = promise instanceof Promise ? await promise : promise; Loading federation/middleware.ts +31 −7 Original line number Diff line number Diff line Loading @@ -201,7 +201,14 @@ export class Federation<TContextData> { } return new URL(path, url); }, getInboxUri: (handle: string): URL => { getInboxUri: (handle?: string): URL => { if (handle == null) { const path = this.#router.build("sharedInbox", {}); if (path == null) { throw new RouterError("No shared inbox path registered."); } return new URL(path, url); } const path = this.#router.build("inbox", { handle }); if (path == null) { throw new RouterError("No inbox path registered."); Loading Loading @@ -449,22 +456,38 @@ export class Federation<TContextData> { /** * Assigns the URL patth for the inbox and starts setting inbox listeners. * @param path The URI path pattern for the inbox. The syntax is based on * URI Template ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * @param inboxPath The URI path pattern for the inbox. The syntax is based * on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * The path must have one variable: `{handle}`. * @param sharedInboxPath An optional URI path pattern for the shared inbox. * The syntax is based on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * The path must have no variables. * @returns An object to register inbox listeners. * @throws {RouteError} Thrown if the path pattern is invalid. */ setInboxListeners(path: string): InboxListenerSetter<TContextData> { setInboxListeners( inboxPath: string, sharedInboxPath?: string, ): InboxListenerSetter<TContextData> { if (this.#router.has("inbox")) { throw new RouterError("Inbox already set."); } const variables = this.#router.add(path, "inbox"); const variables = this.#router.add(inboxPath, "inbox"); if (variables.size !== 1 || !variables.has("handle")) { throw new RouterError( "Path for inbox must have one variable: {handle}", ); } if (sharedInboxPath != null) { const siVars = this.#router.add(sharedInboxPath, "sharedInbox"); if (siVars.size !== 0) { throw new RouterError( "Path for shared inbox must have no variables.", ); } } const listeners = this.#inboxListeners; const setter: InboxListenerSetter<TContextData> = { on<TActivity extends Activity>( Loading Loading @@ -571,8 +594,9 @@ export class Federation<TContextData> { onNotAcceptable, }); case "inbox": case "sharedInbox": return await handleInbox(request, { handle: route.values.handle, handle: route.values.handle ?? null, context, kv: this.#kv, kvPrefix: this.#kvPrefixes.activityIdempotence, Loading Loading
examples/blog/.vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ "fedify", "fediverse", "preact", "unfollowing", "uuidv7" ] }
examples/blog/federation/mod.ts +27 −9 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ import { Accept, Activity, Create, Endpoints, Follow, Link, Person, Loading Loading @@ -40,10 +41,20 @@ federation.setActorDispatcher("/users/{handle}", async (ctx, handle, key) => { summary: blog.description, preferredUsername: handle, url: new URL("/", ctx.request.url), // A `Context<TContextData>` object has several purposes, and one of // them is to provide a way to generate URIs for the dispatchers and // the collections: outbox: ctx.getOutboxUri(handle), inbox: ctx.getInboxUri(handle), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri(), }), following: ctx.getFollowingUri(handle), followers: ctx.getFollowersUri(handle), // The `key` parameter is the public key of the actor, which is used // for the HTTP Signatures. Note that the `key` object is not a // `CryptoKey` instance, but a `CryptographicKey` instance which is // used for ActivityPub: publicKey: key, }); }) Loading Loading @@ -105,7 +116,11 @@ federation.setOutboxDispatcher( return ""; }); federation.setInboxListeners("/users/{handle}/inbox") // Registers the inbox listeners, which are responsible for handling // incoming activities in the inbox: federation.setInboxListeners("/users/{handle}/inbox", "/inbox") // The `Follow` activity is handled by adding the follower to the // follower list: .on(Follow, async (ctx, follow) => { const blog = await getBlog(); if (blog == null) return; Loading Loading @@ -136,20 +151,23 @@ federation.setInboxListeners("/users/{handle}/inbox") sharedInbox: recipient.endpoints?.sharedInbox?.href, typeName: getActorTypeName(recipient), }); // Note that if a server receives a `Follow` activity, it should reply // with either an `Accept` or a `Reject` activity. In this case, the // server automatically accepts the follow request: await ctx.sendActivity( { handle: blog.handle }, recipient, new Accept({ actor: actorUri, object: follow, }), new Accept({ actor: actorUri, object: follow }), ); }) // The `Undo` activity purposes to undo the previous activity. In this // project, we use the `Undo` activity to represent someone unfollowing // the blog: .on(Undo, async (ctx, undo) => { const object = await undo.getObject(ctx); if (object instanceof Follow) { if (object.id == null) return; await removeFollower(object.id.href); const activity = await undo.getObject(ctx); // An `Activity` to undo if (activity instanceof Follow) { if (activity.id == null) return; await removeFollower(activity.id.href); } else { console.debug(undo); } Loading
federation/context.ts +6 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,12 @@ export interface Context<TContextData> { */ getOutboxUri(handle: string): URL; /** * Builds the URI of the shared inbox. * @returns The shared inbox URI. */ getInboxUri(): URL; /** * Builds the URI of an actor's inbox with the given handle. * @param handle The actor's handle. Loading
federation/handler.ts +2 −2 Original line number Diff line number Diff line Loading @@ -211,7 +211,7 @@ export async function handleCollection< } export interface InboxHandlerParameters<TContextData> { handle: string; handle: string | null; context: RequestContext<TContextData>; kv: Deno.Kv; kvPrefix: Deno.KvKey; Loading Loading @@ -242,7 +242,7 @@ export async function handleInbox<TContextData>( if (actorDispatcher == null) { const response = onNotFound(request); return response instanceof Promise ? await response : response; } else { } else if (handle != null) { const key = await context.getActorKey(handle); const promise = actorDispatcher(context, handle, key); const actor = promise instanceof Promise ? await promise : promise; Loading
federation/middleware.ts +31 −7 Original line number Diff line number Diff line Loading @@ -201,7 +201,14 @@ export class Federation<TContextData> { } return new URL(path, url); }, getInboxUri: (handle: string): URL => { getInboxUri: (handle?: string): URL => { if (handle == null) { const path = this.#router.build("sharedInbox", {}); if (path == null) { throw new RouterError("No shared inbox path registered."); } return new URL(path, url); } const path = this.#router.build("inbox", { handle }); if (path == null) { throw new RouterError("No inbox path registered."); Loading Loading @@ -449,22 +456,38 @@ export class Federation<TContextData> { /** * Assigns the URL patth for the inbox and starts setting inbox listeners. * @param path The URI path pattern for the inbox. The syntax is based on * URI Template ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * @param inboxPath The URI path pattern for the inbox. The syntax is based * on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * The path must have one variable: `{handle}`. * @param sharedInboxPath An optional URI path pattern for the shared inbox. * The syntax is based on URI Template * ([RFC 6570](https://tools.ietf.org/html/rfc6570)). * The path must have no variables. * @returns An object to register inbox listeners. * @throws {RouteError} Thrown if the path pattern is invalid. */ setInboxListeners(path: string): InboxListenerSetter<TContextData> { setInboxListeners( inboxPath: string, sharedInboxPath?: string, ): InboxListenerSetter<TContextData> { if (this.#router.has("inbox")) { throw new RouterError("Inbox already set."); } const variables = this.#router.add(path, "inbox"); const variables = this.#router.add(inboxPath, "inbox"); if (variables.size !== 1 || !variables.has("handle")) { throw new RouterError( "Path for inbox must have one variable: {handle}", ); } if (sharedInboxPath != null) { const siVars = this.#router.add(sharedInboxPath, "sharedInbox"); if (siVars.size !== 0) { throw new RouterError( "Path for shared inbox must have no variables.", ); } } const listeners = this.#inboxListeners; const setter: InboxListenerSetter<TContextData> = { on<TActivity extends Activity>( Loading Loading @@ -571,8 +594,9 @@ export class Federation<TContextData> { onNotAcceptable, }); case "inbox": case "sharedInbox": return await handleInbox(request, { handle: route.values.handle, handle: route.values.handle ?? null, context, kv: this.#kv, kvPrefix: this.#kvPrefixes.activityIdempotence, Loading