From d75254fc7169bf19d00cea109a30190b22cc9e6d Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 28 Mar 2025 22:06:42 +0100 Subject: [PATCH] refactor(federation): :truck: Change Like path from /objects/{id} to /likes/{id} --- api/inbox/index.ts | 1 + api/likes/:uuid/index.ts | 79 +++++++++++++++++++ api/messaging/index.ts | 1 + api/notes/:uuid/index.ts | 1 + api/notes/:uuid/quotes.ts | 1 + api/notes/:uuid/replies.ts | 1 + api/objects/:id/index.ts | 110 --------------------------- api/users/:uuid/inbox/index.ts | 1 + api/users/:uuid/index.ts | 1 + api/users/:uuid/outbox/index.ts | 1 + api/well-known/host-meta/index.ts | 1 + api/well-known/nodeinfo/2.0/index.ts | 1 + api/well-known/nodeinfo/index.ts | 1 + api/well-known/versia.ts | 1 + api/well-known/webfinger/index.ts | 1 + biome.json | 2 +- classes/database/like.ts | 2 +- classes/errors/api-error.ts | 8 ++ 18 files changed, 102 insertions(+), 112 deletions(-) create mode 100644 api/likes/:uuid/index.ts delete mode 100644 api/objects/:id/index.ts diff --git a/api/inbox/index.ts b/api/inbox/index.ts index 524d787f..90760a6c 100644 --- a/api/inbox/index.ts +++ b/api/inbox/index.ts @@ -21,6 +21,7 @@ const route = createRoute({ method: "post", path: "/inbox", summary: "Instance federation inbox", + tags: ["Federation"], request: { headers: schemas.header, body: { diff --git a/api/likes/:uuid/index.ts b/api/likes/:uuid/index.ts new file mode 100644 index 00000000..3558002d --- /dev/null +++ b/api/likes/:uuid/index.ts @@ -0,0 +1,79 @@ +import { apiRoute } from "@/api"; +import { createRoute, z } from "@hono/zod-openapi"; +import { Status as StatusSchema } from "@versia/client/schemas"; +import { LikeExtension as LikeSchema } from "@versia/federation/schemas"; +import { Like, User } from "@versia/kit/db"; +import { Likes } from "@versia/kit/tables"; +import { and, eq, sql } from "drizzle-orm"; +import { ApiError } from "~/classes/errors/api-error"; +import { config } from "~/config.ts"; + +const route = createRoute({ + method: "get", + path: "/likes/{id}", + summary: "Retrieve the Versia representation of a like.", + request: { + params: z.object({ + id: StatusSchema.shape.id, + }), + }, + tags: ["Federation"], + responses: { + 200: { + description: "Like", + content: { + "application/json": { + schema: LikeSchema, + }, + }, + }, + 404: { + description: + "Entity not found, is remote, or the requester is not allowed to view it.", + content: { + "application/json": { + schema: ApiError.zodSchema, + }, + }, + }, + }, +}); + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { id } = context.req.valid("param"); + + // Don't fetch a like of a note that is not public or unlisted + // prevents leaking the existence of a private note + const like = await Like.fromSql( + and( + eq(Likes.id, id), + sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."id" = ${Likes.likedId} AND "Notes"."visibility" IN ('public', 'unlisted'))`, + ), + ); + + if (!like) { + throw ApiError.likeNotFound(); + } + + const liker = await User.fromId(like.data.likerId); + + if (!liker || liker.isRemote()) { + throw ApiError.accountNotFound(); + } + + // If base_url uses https and request uses http, rewrite request to use https + // This fixes reverse proxy errors + const reqUrl = new URL(context.req.url); + if ( + config.http.base_url.protocol === "https:" && + reqUrl.protocol === "http:" + ) { + reqUrl.protocol = "https:"; + } + + const { headers } = await liker.sign(like.toVersia(), reqUrl, "GET"); + + return context.json(like.toVersia(), 200, headers.toJSON()); + }), +); diff --git a/api/messaging/index.ts b/api/messaging/index.ts index abee0998..6d9f7aab 100644 --- a/api/messaging/index.ts +++ b/api/messaging/index.ts @@ -8,6 +8,7 @@ const route = createRoute({ path: "/messaging", summary: "Endpoint for the Instance Messaging Versia Extension.", description: "https://versia.pub/extensions/instance-messaging.", + tags: ["Federation"], request: { body: { content: { diff --git a/api/notes/:uuid/index.ts b/api/notes/:uuid/index.ts index e1eb5973..e02911ab 100644 --- a/api/notes/:uuid/index.ts +++ b/api/notes/:uuid/index.ts @@ -12,6 +12,7 @@ const route = createRoute({ method: "get", path: "/notes/{id}", summary: "Retrieve the Versia representation of a note.", + tags: ["Federation"], request: { params: z.object({ id: StatusSchema.shape.id, diff --git a/api/notes/:uuid/quotes.ts b/api/notes/:uuid/quotes.ts index b281c879..6650b6bd 100644 --- a/api/notes/:uuid/quotes.ts +++ b/api/notes/:uuid/quotes.ts @@ -13,6 +13,7 @@ const route = createRoute({ method: "get", path: "/notes/{id}/quotes", summary: "Retrieve all quotes of a Versia Note.", + tags: ["Federation"], request: { params: z.object({ id: StatusSchema.shape.id, diff --git a/api/notes/:uuid/replies.ts b/api/notes/:uuid/replies.ts index 513c7c1f..490bb4d7 100644 --- a/api/notes/:uuid/replies.ts +++ b/api/notes/:uuid/replies.ts @@ -13,6 +13,7 @@ const route = createRoute({ method: "get", path: "/notes/{id}/replies", summary: "Retrieve all replies to a Versia Note.", + tags: ["Federation"], request: { params: z.object({ id: StatusSchema.shape.id, diff --git a/api/objects/:id/index.ts b/api/objects/:id/index.ts deleted file mode 100644 index b296910e..00000000 --- a/api/objects/:id/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { apiRoute } from "@/api"; -import { createRoute, z } from "@hono/zod-openapi"; -import { - LikeExtension as LikeSchema, - Note as NoteSchema, -} from "@versia/federation/schemas"; -import { Like, Note, User } from "@versia/kit/db"; -import { Likes, Notes } from "@versia/kit/tables"; -import { and, eq, inArray, sql } from "drizzle-orm"; -import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/config.ts"; -import type { KnownEntity } from "~/types/api"; - -const route = createRoute({ - method: "get", - path: "/objects/{id}", - summary: "Get object", - request: { - params: z.object({ - id: z.string().uuid(), - }), - }, - responses: { - 200: { - description: "Object", - content: { - "application/json": { - schema: NoteSchema.or(LikeSchema), - }, - }, - }, - 404: { - description: "Object not found", - content: { - "application/json": { - schema: ApiError.zodSchema, - }, - }, - }, - 403: { - description: "Cannot view objects from remote instances", - content: { - "application/json": { - schema: ApiError.zodSchema, - }, - }, - }, - }, -}); - -export default apiRoute((app) => - app.openapi(route, async (context) => { - const { id } = context.req.valid("param"); - - let foundObject: Note | Like | null = null; - let foundAuthor: User | null = null; - let apiObject: KnownEntity | null = null; - - foundObject = await Note.fromSql( - and( - eq(Notes.id, id), - inArray(Notes.visibility, ["public", "unlisted"]), - ), - ); - apiObject = foundObject ? foundObject.toVersia() : null; - foundAuthor = foundObject ? foundObject.author : null; - - if (foundObject) { - if (!(await foundObject.isViewableByUser(null))) { - throw new ApiError(404, "Object not found"); - } - } else { - foundObject = await Like.fromSql( - and( - eq(Likes.id, id), - sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."id" = ${Likes.likedId} AND "Notes"."visibility" IN ('public', 'unlisted'))`, - ), - ); - apiObject = foundObject ? foundObject.toVersia() : null; - foundAuthor = foundObject - ? await User.fromId(foundObject.data.likerId) - : null; - } - - if (!(foundObject && apiObject)) { - throw new ApiError(404, "Object not found"); - } - - if (!foundAuthor) { - throw new ApiError(404, "Author not found"); - } - - if (foundAuthor?.isRemote()) { - throw new ApiError(403, "Object is from a remote instance"); - } - // If base_url uses https and request uses http, rewrite request to use https - // This fixes reverse proxy errors - const reqUrl = new URL(context.req.url); - if ( - config.http.base_url.protocol === "https:" && - reqUrl.protocol === "http:" - ) { - reqUrl.protocol = "https:"; - } - - const { headers } = await foundAuthor.sign(apiObject, reqUrl, "GET"); - - return context.json(apiObject, 200, headers.toJSON()); - }), -); diff --git a/api/users/:uuid/inbox/index.ts b/api/users/:uuid/inbox/index.ts index 39ab31e5..2079927e 100644 --- a/api/users/:uuid/inbox/index.ts +++ b/api/users/:uuid/inbox/index.ts @@ -25,6 +25,7 @@ const route = createRoute({ method: "post", path: "/users/{uuid}/inbox", summary: "Receive federation inbox", + tags: ["Federation"], request: { params: schemas.param, headers: schemas.header, diff --git a/api/users/:uuid/index.ts b/api/users/:uuid/index.ts index 7c3845ff..5f335897 100644 --- a/api/users/:uuid/index.ts +++ b/api/users/:uuid/index.ts @@ -17,6 +17,7 @@ const route = createRoute({ request: { params: schemas.param, }, + tags: ["Federation"], responses: { 200: { description: "User data", diff --git a/api/users/:uuid/outbox/index.ts b/api/users/:uuid/outbox/index.ts index 395dbb9c..e4e5034e 100644 --- a/api/users/:uuid/outbox/index.ts +++ b/api/users/:uuid/outbox/index.ts @@ -27,6 +27,7 @@ const route = createRoute({ params: schemas.param, query: schemas.query, }, + tags: ["Federation"], responses: { 200: { description: "User outbox", diff --git a/api/well-known/host-meta/index.ts b/api/well-known/host-meta/index.ts index 9cea4aad..90b6667b 100644 --- a/api/well-known/host-meta/index.ts +++ b/api/well-known/host-meta/index.ts @@ -6,6 +6,7 @@ const route = createRoute({ method: "get", path: "/.well-known/host-meta", summary: "Well-known host-meta", + tags: ["Federation"], responses: { 200: { description: "Host-meta", diff --git a/api/well-known/nodeinfo/2.0/index.ts b/api/well-known/nodeinfo/2.0/index.ts index 18e8ef59..6bac532a 100644 --- a/api/well-known/nodeinfo/2.0/index.ts +++ b/api/well-known/nodeinfo/2.0/index.ts @@ -8,6 +8,7 @@ const route = createRoute({ method: "get", path: "/.well-known/nodeinfo/2.0", summary: "Well-known nodeinfo 2.0", + tags: ["Federation"], responses: { 200: { description: "Nodeinfo 2.0", diff --git a/api/well-known/nodeinfo/index.ts b/api/well-known/nodeinfo/index.ts index e7fa841f..1ba94b2e 100644 --- a/api/well-known/nodeinfo/index.ts +++ b/api/well-known/nodeinfo/index.ts @@ -6,6 +6,7 @@ const route = createRoute({ method: "get", path: "/.well-known/nodeinfo", summary: "Well-known nodeinfo", + tags: ["Federation"], responses: { 200: { description: "Nodeinfo links", diff --git a/api/well-known/versia.ts b/api/well-known/versia.ts index 0c21ec9b..ea48cfc5 100644 --- a/api/well-known/versia.ts +++ b/api/well-known/versia.ts @@ -12,6 +12,7 @@ const route = createRoute({ method: "get", path: "/.well-known/versia", summary: "Get instance metadata", + tags: ["Federation"], responses: { 200: { description: "Instance metadata", diff --git a/api/well-known/webfinger/index.ts b/api/well-known/webfinger/index.ts index b353d836..5087b517 100644 --- a/api/well-known/webfinger/index.ts +++ b/api/well-known/webfinger/index.ts @@ -36,6 +36,7 @@ const route = createRoute({ request: { query: schemas.query, }, + tags: ["Federation"], responses: { 200: { description: "User information", diff --git a/biome.json b/biome.json index e4641804..03c19ade 100644 --- a/biome.json +++ b/biome.json @@ -103,6 +103,6 @@ "globals": ["Bun", "HTMLRewriter", "BufferEncoding"] }, "files": { - "ignore": ["node_modules", "dist", "cache"] + "ignore": ["node_modules", "dist", "cache", "build"] } } diff --git a/classes/database/like.ts b/classes/database/like.ts index e7a46b11..df473e24 100644 --- a/classes/database/like.ts +++ b/classes/database/like.ts @@ -146,7 +146,7 @@ export class Like extends BaseInterface { } public getUri(): URL { - return new URL(`/objects/${this.data.id}`, config.http.base_url); + return new URL(`/likes/${this.data.id}`, config.http.base_url); } public toVersia(): LikeExtension { diff --git a/classes/errors/api-error.ts b/classes/errors/api-error.ts index 1df8c4f3..a34a971b 100644 --- a/classes/errors/api-error.ts +++ b/classes/errors/api-error.ts @@ -100,6 +100,14 @@ export class ApiError extends Error { ); } + public static likeNotFound(): ApiError { + return new ApiError( + 404, + "Like not found", + "The requested like could not be found.", + ); + } + public static pushSubscriptionNotFound(): ApiError { return new ApiError( 404,