server/server/api/users/[uuid]/inbox/index.ts

252 lines
7.1 KiB
TypeScript
Raw Normal View History

import { apiRoute, applyConfig } from "@api";
2024-04-10 04:05:02 +02:00
import { errorResponse, response } from "@response";
2024-04-14 02:07:05 +02:00
import { eq } from "drizzle-orm";
2024-04-10 04:05:02 +02:00
import type * as Lysand from "lysand-types";
2024-04-14 02:07:05 +02:00
import { resolveStatus } from "~database/entities/Status";
2024-04-10 07:51:00 +02:00
import {
2024-04-14 02:07:05 +02:00
findFirstUser,
2024-04-10 07:51:00 +02:00
getRelationshipToOtherUser,
resolveUser,
sendFollowAccept,
2024-04-10 07:51:00 +02:00
} from "~database/entities/User";
2024-04-14 02:07:05 +02:00
import { db } from "~drizzle/db";
import { notification, relationship } from "~drizzle/schema";
export const meta = applyConfig({
2024-04-07 07:30:49 +02:00
allowedMethods: ["POST"],
auth: {
required: false,
},
ratelimits: {
duration: 60,
max: 500,
},
2024-04-10 04:05:02 +02:00
route: "/users/:uuid",
});
export default apiRoute(async (req, matchedRoute, extraData) => {
2024-04-10 04:05:02 +02:00
const uuid = matchedRoute.params.uuid;
2024-04-07 07:30:49 +02:00
2024-04-14 02:07:05 +02:00
const user = await findFirstUser({
where: (user, { eq }) => eq(user.id, uuid),
2024-04-07 07:30:49 +02:00
});
2024-04-10 04:05:02 +02:00
if (!user) {
return errorResponse("User not found", 404);
2024-04-07 07:30:49 +02:00
}
2024-04-10 04:05:02 +02:00
// Process incoming request
const body = extraData.parsedRequest as Lysand.Entity;
2024-04-07 07:30:49 +02:00
2024-04-10 04:05:02 +02:00
// Verify request signature
// TODO: Check if instance is defederated
// biome-ignore lint/correctness/noConstantCondition: Temporary
if (true) {
// request is a Request object containing the previous request
const signatureHeader = req.headers.get("Signature");
const origin = req.headers.get("Origin");
2024-04-07 07:30:49 +02:00
const date = req.headers.get("Date");
2024-04-10 04:05:02 +02:00
if (!signatureHeader) {
return errorResponse("Missing Signature header", 400);
2024-04-07 07:30:49 +02:00
}
2024-04-10 04:05:02 +02:00
if (!origin) {
return errorResponse("Missing Origin header", 400);
2024-04-07 07:30:49 +02:00
}
2024-04-10 04:05:02 +02:00
if (!date) {
return errorResponse("Missing Date header", 400);
2024-04-07 07:30:49 +02:00
}
const signature = signatureHeader
.split("signature=")[1]
.replace(/"/g, "");
const digest = await crypto.subtle.digest(
"SHA-256",
2024-04-10 04:05:02 +02:00
new TextEncoder().encode(JSON.stringify(body)),
2024-04-07 07:30:49 +02:00
);
2024-04-10 04:05:02 +02:00
const keyId = signatureHeader
.split("keyId=")[1]
.split(",")[0]
.replace(/"/g, "");
2024-04-10 09:04:46 +02:00
console.log(`Resolving keyId ${keyId}`);
const sender = await resolveUser(keyId);
2024-04-07 07:30:49 +02:00
2024-04-10 04:05:02 +02:00
if (!sender) {
return errorResponse("Invalid keyId", 400);
}
const public_key = await crypto.subtle.importKey(
2024-04-07 07:30:49 +02:00
"spki",
2024-04-10 04:05:02 +02:00
Uint8Array.from(atob(sender.publicKey), (c) => c.charCodeAt(0)),
2024-04-07 07:30:49 +02:00
"Ed25519",
false,
["verify"],
);
2024-04-10 04:05:02 +02:00
const expectedSignedString =
`(request-target): ${req.method.toLowerCase()} ${
new URL(req.url).pathname
}\n` +
`host: ${new URL(req.url).host}\n` +
`date: ${date}\n` +
`digest: SHA-256=${btoa(
String.fromCharCode(...new Uint8Array(digest)),
)}\n`;
2024-04-07 07:30:49 +02:00
// Check if signed string is valid
const isValid = await crypto.subtle.verify(
"Ed25519",
2024-04-10 04:05:02 +02:00
public_key,
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
2024-04-07 07:30:49 +02:00
new TextEncoder().encode(expectedSignedString),
);
if (!isValid) {
2024-04-10 04:05:02 +02:00
return errorResponse("Invalid signature", 400);
2024-04-07 07:30:49 +02:00
}
2024-04-10 04:05:02 +02:00
}
2024-04-07 07:30:49 +02:00
2024-04-10 04:05:02 +02:00
// Add sent data to database
switch (body.type) {
2024-04-07 07:30:49 +02:00
case "Note": {
2024-04-10 04:05:02 +02:00
const note = body as Lysand.Note;
2024-04-07 07:30:49 +02:00
2024-04-10 07:51:00 +02:00
const account = await resolveUser(note.author);
2024-04-07 07:30:49 +02:00
2024-04-10 04:05:02 +02:00
if (!account) {
return errorResponse("Author not found", 400);
2024-04-07 07:30:49 +02:00
}
const newStatus = await resolveStatus(undefined, note).catch(
(e) => {
console.error(e);
return null;
2024-04-07 07:30:49 +02:00
},
2024-04-10 04:05:02 +02:00
);
if (!newStatus) {
return errorResponse("Failed to add status", 500);
}
2024-04-10 04:05:02 +02:00
return response("Note created", 201);
2024-04-07 07:30:49 +02:00
}
2024-04-10 07:51:00 +02:00
case "Follow": {
const follow = body as Lysand.Follow;
const account = await resolveUser(follow.author);
if (!account) {
return errorResponse("Author not found", 400);
}
2024-04-14 02:07:05 +02:00
const foundRelationship = await getRelationshipToOtherUser(
2024-04-10 07:51:00 +02:00
account,
user,
);
// Check if already following
2024-04-14 02:07:05 +02:00
if (foundRelationship.following) {
2024-04-10 07:51:00 +02:00
return response("Already following", 200);
}
2024-04-14 02:07:05 +02:00
await db
.update(relationship)
.set({
2024-04-10 07:51:00 +02:00
following: !user.isLocked,
requested: user.isLocked,
showingReblogs: true,
notifying: true,
languages: [],
2024-04-14 02:07:05 +02:00
})
.where(eq(relationship.id, foundRelationship.id));
2024-04-10 07:51:00 +02:00
2024-04-14 02:07:05 +02:00
await db.insert(notification).values({
accountId: account.id,
type: user.isLocked ? "follow_request" : "follow",
notifiedId: user.id,
2024-04-10 07:51:00 +02:00
});
if (!user.isLocked) {
// Federate FollowAccept
await sendFollowAccept(account, user);
2024-04-10 07:51:00 +02:00
}
return response("Follow request sent", 200);
}
2024-04-10 09:24:23 +02:00
case "FollowAccept": {
const followAccept = body as Lysand.FollowAccept;
2024-04-10 09:45:20 +02:00
console.log(followAccept);
2024-04-10 09:24:23 +02:00
const account = await resolveUser(followAccept.author);
if (!account) {
return errorResponse("Author not found", 400);
}
2024-04-10 09:45:20 +02:00
console.log(account);
2024-04-14 02:07:05 +02:00
const foundRelationship = await getRelationshipToOtherUser(
2024-04-10 09:24:23 +02:00
user,
account,
);
2024-04-14 02:07:05 +02:00
console.log(foundRelationship);
2024-04-10 09:45:20 +02:00
2024-04-14 02:07:05 +02:00
if (!foundRelationship.requested) {
2024-04-10 09:24:23 +02:00
return response("There is no follow request to accept", 200);
}
2024-04-14 02:07:05 +02:00
await db
.update(relationship)
.set({
2024-04-10 09:24:23 +02:00
following: true,
requested: false,
2024-04-14 02:07:05 +02:00
})
.where(eq(relationship.id, foundRelationship.id));
2024-04-10 09:24:23 +02:00
return response("Follow request accepted", 200);
}
case "FollowReject": {
const followReject = body as Lysand.FollowReject;
const account = await resolveUser(followReject.author);
if (!account) {
return errorResponse("Author not found", 400);
}
2024-04-14 02:07:05 +02:00
const foundRelationship = await getRelationshipToOtherUser(
user,
account,
);
2024-04-14 02:07:05 +02:00
if (!foundRelationship.requested) {
return response("There is no follow request to reject", 200);
}
2024-04-14 02:07:05 +02:00
await db
.update(relationship)
.set({
requested: false,
following: false,
2024-04-14 02:07:05 +02:00
})
.where(eq(relationship.id, foundRelationship.id));
return response("Follow request rejected", 200);
}
2024-04-07 07:30:49 +02:00
default: {
2024-04-10 04:05:02 +02:00
return errorResponse("Unknown object type", 400);
2024-04-07 07:30:49 +02:00
}
}
2024-04-10 04:05:02 +02:00
//return jsonResponse(userToLysand(user));
});