mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(federation): ✨ Allow objects to be fetched via their ID
This commit is contained in:
parent
895826a5f8
commit
47040ad273
|
|
@ -11,7 +11,7 @@ export type Like = InferSelectModel<typeof like>;
|
||||||
/**
|
/**
|
||||||
* Represents a Like entity in the database.
|
* Represents a Like entity in the database.
|
||||||
*/
|
*/
|
||||||
export const toLysand = (like: Like): Lysand.Like => {
|
export const likeToLysand = (like: Like): Lysand.Like => {
|
||||||
return {
|
return {
|
||||||
id: like.id,
|
id: like.id,
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { jsonResponse } from "@response";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
ratelimits: {
|
|
||||||
duration: 60,
|
|
||||||
max: 500,
|
|
||||||
},
|
|
||||||
route: "/object/:id",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default apiRoute(() => {
|
|
||||||
return jsonResponse({});
|
|
||||||
});
|
|
||||||
58
server/api/objects/[uuid]/index.ts
Normal file
58
server/api/objects/[uuid]/index.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { apiRoute, applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { likeToLysand, type Like } from "~database/entities/Like";
|
||||||
|
import {
|
||||||
|
findFirstStatuses,
|
||||||
|
statusToLysand,
|
||||||
|
type StatusWithRelations,
|
||||||
|
} from "~database/entities/Status";
|
||||||
|
import { db } from "~drizzle/db";
|
||||||
|
import type * as Lysand from "lysand-types";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["GET"],
|
||||||
|
auth: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
ratelimits: {
|
||||||
|
duration: 60,
|
||||||
|
max: 500,
|
||||||
|
},
|
||||||
|
route: "/objects/:id",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default apiRoute(async (req, matchedRoute) => {
|
||||||
|
const uuid = matchedRoute.params.uuid;
|
||||||
|
|
||||||
|
let foundObject: StatusWithRelations | Like | null = null;
|
||||||
|
let apiObject: Lysand.Entity | null = null;
|
||||||
|
|
||||||
|
foundObject =
|
||||||
|
(await findFirstStatuses({
|
||||||
|
where: (status, { eq, and, inArray }) =>
|
||||||
|
and(
|
||||||
|
eq(status.id, uuid),
|
||||||
|
inArray(status.visibility, ["public", "unlisted"]),
|
||||||
|
),
|
||||||
|
})) ?? null;
|
||||||
|
apiObject = foundObject ? statusToLysand(foundObject) : null;
|
||||||
|
|
||||||
|
if (!foundObject) {
|
||||||
|
foundObject =
|
||||||
|
(await db.query.like.findFirst({
|
||||||
|
where: (like, { eq, and }) =>
|
||||||
|
and(
|
||||||
|
eq(like.id, uuid),
|
||||||
|
sql`EXISTS (SELECT 1 FROM statuses WHERE statuses.id = ${like.likedId} AND statuses.visibility IN ('public', 'unlisted'))`,
|
||||||
|
),
|
||||||
|
})) ?? null;
|
||||||
|
apiObject = foundObject ? likeToLysand(foundObject) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundObject) {
|
||||||
|
return errorResponse("Object not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse(foundObject);
|
||||||
|
});
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
import { apiRoute, applyConfig } from "@api";
|
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
|
||||||
import { findFirstStatuses, statusToLysand } from "~database/entities/Status";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
ratelimits: {
|
|
||||||
duration: 60,
|
|
||||||
max: 500,
|
|
||||||
},
|
|
||||||
route: "/objects/note/:uuid",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default apiRoute(async (req, matchedRoute, extraData) => {
|
|
||||||
const uuid = matchedRoute.params.uuid;
|
|
||||||
|
|
||||||
const status = await findFirstStatuses({
|
|
||||||
where: (status, { eq }) => eq(status.id, uuid),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
return errorResponse("Note not found", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await extraData.configManager.getConfig();
|
|
||||||
|
|
||||||
const output = statusToLysand(status);
|
|
||||||
|
|
||||||
const privateKey = await crypto.subtle.importKey(
|
|
||||||
"pkcs8",
|
|
||||||
Uint8Array.from(atob(status.author.privateKey ?? ""), (c) =>
|
|
||||||
c.charCodeAt(0),
|
|
||||||
),
|
|
||||||
"Ed25519",
|
|
||||||
false,
|
|
||||||
["sign"],
|
|
||||||
);
|
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
|
||||||
"SHA-256",
|
|
||||||
new TextEncoder().encode(JSON.stringify(output)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const userInbox = new URL(
|
|
||||||
"http://lysand.localhost:8080/users/018ec11c-c6cb-7a67-bd20-a4c81bf42912/inbox",
|
|
||||||
);
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
const signature = await crypto.subtle.sign(
|
|
||||||
"Ed25519",
|
|
||||||
privateKey,
|
|
||||||
new TextEncoder().encode(
|
|
||||||
`(request-target): post ${userInbox.pathname}\n` +
|
|
||||||
`host: ${userInbox.host}\n` +
|
|
||||||
`date: ${date.toISOString()}\n` +
|
|
||||||
`digest: SHA-256=${btoa(
|
|
||||||
String.fromCharCode(...new Uint8Array(digest)),
|
|
||||||
)}\n`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const signatureBase64 = btoa(
|
|
||||||
String.fromCharCode(...new Uint8Array(signature)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return jsonResponse({
|
|
||||||
Date: date.toISOString(),
|
|
||||||
Origin: "example.com",
|
|
||||||
Signature: `keyId="https://example.com/users/${status.author.id}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
|
||||||
post: output,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue