Loading CHANGES.md +6 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,12 @@ Version 0.3.0 To be released. - Utility functions for responding with an ActivityPub object. - Added `respondWithObject()` function. - Added `respondWithObjectIfAcceptable()` function. - Added `RespondWithObjectOptions` interface. Version 0.2.0 ------------- Loading examples/blog/routes/posts/[uuid].tsx +6 −25 Original line number Diff line number Diff line import { Handler, PageProps } from "$fresh/server.ts"; import { Head } from "$fresh/runtime.ts"; import { accepts } from "$std/http/mod.ts"; import { respondWithObjectIfAcceptable } from "fedify/federation"; import Comment from "../../components/Comment.tsx"; import Post from "../../components/Post.tsx"; import { federation } from "../../federation/mod.ts"; Loading Loading @@ -30,30 +30,11 @@ export const handler: Handler<PostPageData> = async (req, ctx) => { const post = await getPost(ctx.params.uuid); if (post == null) return await ctx.renderNotFound(); const comments = await getComments(post.uuid); const accept = accepts( req, "application/activity+json", "application/ld+json", "application/json", "text/html", "application/xhtml+xml", ); if ( accept === "application/activity+json" || accept === "application/ld+json" || accept === "application/json" ) { const fedCtx = federation.createContext(req); const article = toArticle(fedCtx, blog, post, comments); const jsonLd = await article.toJsonLd(fedCtx); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", Link: `<${article.id}>; rel="alternate"; type="application/activity+json"`, Vary: "Accept", }, }); } const response = await respondWithObjectIfAcceptable(article, req, fedCtx); if (response != null) return response; const followers = await countFollowers(); const data: PostPageData = { blog, Loading federation/handler.test.ts +69 −2 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "jsr:@std/assert@^0.218.2"; import { createRequestContext } from "../testing/context.ts"; import { Activity, Create, Person } from "../vocab/vocab.ts"; import { Activity, Create, Note, Person } from "../vocab/vocab.ts"; import { ActorDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, } from "./callback.ts"; import { acceptsJsonLd, handleActor, handleCollection } from "./handler.ts"; import { acceptsJsonLd, handleActor, handleCollection, respondWithObject, respondWithObjectIfAcceptable, } from "./handler.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; Deno.test("acceptsJsonLd()", () => { assert(acceptsJsonLd( Loading Loading @@ -390,3 +397,63 @@ Deno.test("handleCollection()", async () => { assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, null); }); Deno.test("respondWithObject()", async () => { const response = await respondWithObject( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), { documentLoader: mockDocumentLoader }, ); assert(response.ok); assertEquals( response.headers.get("Content-Type"), "application/activity+json", ); assertEquals(await response.json(), { "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/notes/1", type: "Note", content: "Hello, world!", }); }); Deno.test("respondWithObjectIfAcceptable", async () => { let request = new Request("https://example.com/", { headers: { Accept: "application/activity+json" }, }); let response = await respondWithObjectIfAcceptable( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), request, { documentLoader: mockDocumentLoader }, ); assert(response != null); assert(response.ok); assertEquals( response.headers.get("Content-Type"), "application/activity+json", ); assertEquals(await response.json(), { "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/notes/1", type: "Note", content: "Hello, world!", }); request = new Request("https://example.com/", { headers: { Accept: "text/html" }, }); response = await respondWithObjectIfAcceptable( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), request, { documentLoader: mockDocumentLoader }, ); assertEquals(response, null); }); federation/handler.ts +49 −0 Original line number Diff line number Diff line import { accepts } from "jsr:@std/http@^0.218.2"; import { doesActorOwnKey, verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Activity, Link, Loading Loading @@ -336,3 +337,51 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } /** * Options for the {@link respondWithObject} and * {@link respondWithObjectIfAcceptable} functions. */ export interface RespondWithObjectOptions { /** * The document loader to use for compacting JSON-LD. */ documentLoader: DocumentLoader; } /** * Responds with the given object in JSON-LD format. * * @param object The object to respond with. * @param options Options. */ export async function respondWithObject( object: Object, options?: RespondWithObjectOptions, ): Promise<Response> { const jsonLd = await object.toJsonLd(options); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", }, }); } /** * Responds with the given object in JSON-LD format if the request accepts * JSON-LD. * * @param object The object to respond with. * @param request The request to check for JSON-LD acceptability. * @param options Options. */ export async function respondWithObjectIfAcceptable( object: Object, request: Request, options?: RespondWithObjectOptions, ): Promise<Response | null> { if (!acceptsJsonLd(request)) return null; const response = await respondWithObject(object, options); response.headers.set("Vary", "Accept"); return response; } federation/mod.ts +5 −0 Original line number Diff line number Diff line Loading @@ -6,5 +6,10 @@ export * from "./callback.ts"; export * from "./collection.ts"; export * from "./context.ts"; export { respondWithObject, respondWithObjectIfAcceptable, type RespondWithObjectOptions, } from "./handler.ts"; export * from "./middleware.ts"; export * from "./router.ts"; Loading
CHANGES.md +6 −0 Original line number Diff line number Diff line Loading @@ -13,6 +13,12 @@ Version 0.3.0 To be released. - Utility functions for responding with an ActivityPub object. - Added `respondWithObject()` function. - Added `respondWithObjectIfAcceptable()` function. - Added `RespondWithObjectOptions` interface. Version 0.2.0 ------------- Loading
examples/blog/routes/posts/[uuid].tsx +6 −25 Original line number Diff line number Diff line import { Handler, PageProps } from "$fresh/server.ts"; import { Head } from "$fresh/runtime.ts"; import { accepts } from "$std/http/mod.ts"; import { respondWithObjectIfAcceptable } from "fedify/federation"; import Comment from "../../components/Comment.tsx"; import Post from "../../components/Post.tsx"; import { federation } from "../../federation/mod.ts"; Loading Loading @@ -30,30 +30,11 @@ export const handler: Handler<PostPageData> = async (req, ctx) => { const post = await getPost(ctx.params.uuid); if (post == null) return await ctx.renderNotFound(); const comments = await getComments(post.uuid); const accept = accepts( req, "application/activity+json", "application/ld+json", "application/json", "text/html", "application/xhtml+xml", ); if ( accept === "application/activity+json" || accept === "application/ld+json" || accept === "application/json" ) { const fedCtx = federation.createContext(req); const article = toArticle(fedCtx, blog, post, comments); const jsonLd = await article.toJsonLd(fedCtx); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", Link: `<${article.id}>; rel="alternate"; type="application/activity+json"`, Vary: "Accept", }, }); } const response = await respondWithObjectIfAcceptable(article, req, fedCtx); if (response != null) return response; const followers = await countFollowers(); const data: PostPageData = { blog, Loading
federation/handler.test.ts +69 −2 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "jsr:@std/assert@^0.218.2"; import { createRequestContext } from "../testing/context.ts"; import { Activity, Create, Person } from "../vocab/vocab.ts"; import { Activity, Create, Note, Person } from "../vocab/vocab.ts"; import { ActorDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, } from "./callback.ts"; import { acceptsJsonLd, handleActor, handleCollection } from "./handler.ts"; import { acceptsJsonLd, handleActor, handleCollection, respondWithObject, respondWithObjectIfAcceptable, } from "./handler.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; Deno.test("acceptsJsonLd()", () => { assert(acceptsJsonLd( Loading Loading @@ -390,3 +397,63 @@ Deno.test("handleCollection()", async () => { assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, null); }); Deno.test("respondWithObject()", async () => { const response = await respondWithObject( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), { documentLoader: mockDocumentLoader }, ); assert(response.ok); assertEquals( response.headers.get("Content-Type"), "application/activity+json", ); assertEquals(await response.json(), { "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/notes/1", type: "Note", content: "Hello, world!", }); }); Deno.test("respondWithObjectIfAcceptable", async () => { let request = new Request("https://example.com/", { headers: { Accept: "application/activity+json" }, }); let response = await respondWithObjectIfAcceptable( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), request, { documentLoader: mockDocumentLoader }, ); assert(response != null); assert(response.ok); assertEquals( response.headers.get("Content-Type"), "application/activity+json", ); assertEquals(await response.json(), { "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/notes/1", type: "Note", content: "Hello, world!", }); request = new Request("https://example.com/", { headers: { Accept: "text/html" }, }); response = await respondWithObjectIfAcceptable( new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!", }), request, { documentLoader: mockDocumentLoader }, ); assertEquals(response, null); });
federation/handler.ts +49 −0 Original line number Diff line number Diff line import { accepts } from "jsr:@std/http@^0.218.2"; import { doesActorOwnKey, verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Activity, Link, Loading Loading @@ -336,3 +337,51 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } /** * Options for the {@link respondWithObject} and * {@link respondWithObjectIfAcceptable} functions. */ export interface RespondWithObjectOptions { /** * The document loader to use for compacting JSON-LD. */ documentLoader: DocumentLoader; } /** * Responds with the given object in JSON-LD format. * * @param object The object to respond with. * @param options Options. */ export async function respondWithObject( object: Object, options?: RespondWithObjectOptions, ): Promise<Response> { const jsonLd = await object.toJsonLd(options); return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json", }, }); } /** * Responds with the given object in JSON-LD format if the request accepts * JSON-LD. * * @param object The object to respond with. * @param request The request to check for JSON-LD acceptability. * @param options Options. */ export async function respondWithObjectIfAcceptable( object: Object, request: Request, options?: RespondWithObjectOptions, ): Promise<Response | null> { if (!acceptsJsonLd(request)) return null; const response = await respondWithObject(object, options); response.headers.set("Vary", "Accept"); return response; }
federation/mod.ts +5 −0 Original line number Diff line number Diff line Loading @@ -6,5 +6,10 @@ export * from "./callback.ts"; export * from "./collection.ts"; export * from "./context.ts"; export { respondWithObject, respondWithObjectIfAcceptable, type RespondWithObjectOptions, } from "./handler.ts"; export * from "./middleware.ts"; export * from "./router.ts";