server/server/api/users/[uuid]/inbox/index.ts
2024-04-09 22:37:58 -10:00

262 lines
7.4 KiB
TypeScript

import { apiRoute, applyConfig } from "@api";
import { errorResponse, response } from "@response";
import { client } from "~database/datasource";
import { userRelations } from "~database/entities/relations";
import type * as Lysand from "lysand-types";
import { createNewStatus, resolveStatus } from "~database/entities/Status";
import type { APIStatus } from "~types/entities/status";
import {
followAcceptToLysand,
getRelationshipToOtherUser,
resolveUser,
sendFollowAccept,
} from "~database/entities/User";
import { objectToInboxRequest } from "~database/entities/Federation";
export const meta = applyConfig({
allowedMethods: ["POST"],
auth: {
required: false,
},
ratelimits: {
duration: 60,
max: 500,
},
route: "/users/:uuid",
});
export default apiRoute(async (req, matchedRoute, extraData) => {
const uuid = matchedRoute.params.uuid;
const user = await client.user.findUnique({
where: {
id: uuid,
},
include: userRelations,
});
if (!user) {
return errorResponse("User not found", 404);
}
const config = await extraData.configManager.getConfig();
// Process incoming request
const body = extraData.parsedRequest as Lysand.Entity;
// 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");
const date = req.headers.get("Date");
if (!signatureHeader) {
return errorResponse("Missing Signature header", 400);
}
if (!origin) {
return errorResponse("Missing Origin header", 400);
}
if (!date) {
return errorResponse("Missing Date header", 400);
}
const signature = signatureHeader
.split("signature=")[1]
.replace(/"/g, "");
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(JSON.stringify(body)),
);
const keyId = signatureHeader
.split("keyId=")[1]
.split(",")[0]
.replace(/"/g, "");
console.log(`Resolving keyId ${keyId}`);
const sender = await resolveUser(keyId);
if (!sender) {
return errorResponse("Invalid keyId", 400);
}
const public_key = await crypto.subtle.importKey(
"spki",
Uint8Array.from(atob(sender.publicKey), (c) => c.charCodeAt(0)),
"Ed25519",
false,
["verify"],
);
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`;
// Check if signed string is valid
const isValid = await crypto.subtle.verify(
"Ed25519",
public_key,
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
new TextEncoder().encode(expectedSignedString),
);
if (!isValid) {
return errorResponse("Invalid signature", 400);
}
}
console.log(body);
// Add sent data to database
switch (body.type) {
case "Note": {
const note = body as Lysand.Note;
const account = await resolveUser(note.author);
if (!account) {
return errorResponse("Author not found", 400);
}
const newStatus = await resolveStatus(undefined, note).catch(
(e) => {
console.error(e);
return null;
},
);
if (!newStatus) {
return errorResponse("Failed to add status", 500);
}
return response("Note created", 201);
}
case "Follow": {
const follow = body as Lysand.Follow;
const account = await resolveUser(follow.author);
if (!account) {
return errorResponse("Author not found", 400);
}
const relationship = await getRelationshipToOtherUser(
account,
user,
);
// Check if already following
if (relationship.following) {
return response("Already following", 200);
}
await client.relationship.update({
where: { id: relationship.id },
data: {
following: !user.isLocked,
requested: user.isLocked,
showingReblogs: true,
notifying: true,
languages: [],
},
});
await client.notification.create({
data: {
accountId: account.id,
type: user.isLocked ? "follow_request" : "follow",
notifiedId: user.id,
},
});
if (!user.isLocked) {
// Federate FollowAccept
await sendFollowAccept(account, user);
}
return response("Follow request sent", 200);
}
case "FollowAccept": {
const followAccept = body as Lysand.FollowAccept;
console.log(followAccept);
const account = await resolveUser(followAccept.author);
if (!account) {
return errorResponse("Author not found", 400);
}
console.log(account);
const relationship = await getRelationshipToOtherUser(
user,
account,
);
console.log(relationship);
if (!relationship.requested) {
return response("There is no follow request to accept", 200);
}
await client.relationship.update({
where: { id: relationship.id },
data: {
following: true,
requested: false,
},
});
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);
}
const relationship = await getRelationshipToOtherUser(
user,
account,
);
if (!relationship.requested) {
return response("There is no follow request to reject", 200);
}
await client.relationship.update({
where: { id: relationship.id },
data: {
requested: false,
following: false,
},
});
return response("Follow request rejected", 200);
}
default: {
return errorResponse("Unknown object type", 400);
}
}
//return jsonResponse(userToLysand(user));
});