2024-05-06 09:16:33 +02:00
|
|
|
import { applyConfig, handleZodError } from "@api";
|
|
|
|
|
import { zValidator } from "@hono/zod-validator";
|
|
|
|
|
import { dualLogger } from "@loggers";
|
2024-05-17 21:38:38 +02:00
|
|
|
import {
|
|
|
|
|
EntityValidator,
|
|
|
|
|
type HttpVerb,
|
|
|
|
|
SignatureValidator,
|
|
|
|
|
} from "@lysand-org/federation";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { errorResponse, jsonResponse, response } from "@response";
|
2024-05-17 19:56:13 +02:00
|
|
|
import type { SocketAddress } from "bun";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { eq } from "drizzle-orm";
|
|
|
|
|
import type { Hono } from "hono";
|
2024-05-17 19:56:13 +02:00
|
|
|
import { matches } from "ip-matching";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { z } from "zod";
|
|
|
|
|
import { isValidationError } from "zod-validation-error";
|
|
|
|
|
import { resolveNote } from "~database/entities/Status";
|
|
|
|
|
import {
|
|
|
|
|
getRelationshipToOtherUser,
|
|
|
|
|
sendFollowAccept,
|
|
|
|
|
} from "~database/entities/User";
|
|
|
|
|
import { db } from "~drizzle/db";
|
|
|
|
|
import { Notifications, Relationships } from "~drizzle/schema";
|
2024-05-17 19:56:13 +02:00
|
|
|
import { config } from "~packages/config-manager";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { User } from "~packages/database-interface/user";
|
|
|
|
|
import { LogLevel } from "~packages/log-manager";
|
|
|
|
|
|
|
|
|
|
export const meta = applyConfig({
|
|
|
|
|
allowedMethods: ["POST"],
|
|
|
|
|
auth: {
|
|
|
|
|
required: false,
|
|
|
|
|
},
|
|
|
|
|
ratelimits: {
|
|
|
|
|
duration: 60,
|
|
|
|
|
max: 500,
|
|
|
|
|
},
|
2024-05-17 09:51:49 +02:00
|
|
|
route: "/users/:uuid/inbox",
|
2024-05-06 09:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const schemas = {
|
|
|
|
|
param: z.object({
|
|
|
|
|
uuid: z.string().uuid(),
|
|
|
|
|
}),
|
|
|
|
|
header: z.object({
|
|
|
|
|
signature: z.string(),
|
|
|
|
|
date: z.string(),
|
|
|
|
|
}),
|
2024-05-17 10:37:06 +02:00
|
|
|
body: z.any(),
|
2024-05-06 09:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default (app: Hono) =>
|
|
|
|
|
app.on(
|
|
|
|
|
meta.allowedMethods,
|
|
|
|
|
meta.route,
|
|
|
|
|
zValidator("param", schemas.param, handleZodError),
|
|
|
|
|
zValidator("header", schemas.header, handleZodError),
|
2024-05-17 10:37:06 +02:00
|
|
|
zValidator("json", schemas.body, handleZodError),
|
2024-05-06 09:16:33 +02:00
|
|
|
async (context) => {
|
|
|
|
|
const { uuid } = context.req.valid("param");
|
|
|
|
|
const { signature, date } = context.req.valid("header");
|
2024-05-17 21:38:38 +02:00
|
|
|
const body: typeof EntityValidator.$Entity =
|
|
|
|
|
await context.req.valid("json");
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
const user = await User.fromId(uuid);
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
return errorResponse("User not found", 404);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-17 19:56:13 +02:00
|
|
|
// @ts-expect-error IP attribute is not in types
|
|
|
|
|
const request_ip = context.env?.ip as
|
|
|
|
|
| SocketAddress
|
|
|
|
|
| undefined
|
|
|
|
|
| null;
|
|
|
|
|
|
|
|
|
|
let checkSignature = true;
|
|
|
|
|
|
|
|
|
|
if (request_ip?.address && config.federation.bridge.enabled) {
|
|
|
|
|
for (const ip of config.federation.bridge.allowed_ips) {
|
|
|
|
|
if (matches(ip, request_ip?.address)) {
|
|
|
|
|
checkSignature = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 09:16:33 +02:00
|
|
|
// Verify request signature
|
|
|
|
|
// TODO: Check if instance is defederated
|
|
|
|
|
// TODO: Reverse DNS lookup with Origin header
|
2024-05-17 19:56:13 +02:00
|
|
|
if (checkSignature) {
|
2024-05-06 09:16:33 +02:00
|
|
|
if (!signature) {
|
|
|
|
|
return errorResponse("Missing Signature header", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!date) {
|
|
|
|
|
return errorResponse("Missing Date header", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const keyId = signature
|
|
|
|
|
.split("keyId=")[1]
|
|
|
|
|
.split(",")[0]
|
|
|
|
|
.replace(/"/g, "");
|
|
|
|
|
|
|
|
|
|
const sender = await User.resolve(keyId);
|
|
|
|
|
|
|
|
|
|
if (!sender) {
|
|
|
|
|
return errorResponse("Could not resolve keyId", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validator = await SignatureValidator.fromStringKey(
|
|
|
|
|
sender.getUser().publicKey,
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-17 23:42:42 +02:00
|
|
|
// 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 (
|
|
|
|
|
new URL(config.http.base_url).protocol === "https:" &&
|
|
|
|
|
reqUrl.protocol === "http:"
|
|
|
|
|
) {
|
|
|
|
|
reqUrl.protocol = "https:";
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-15 02:35:13 +02:00
|
|
|
const isValid = await validator
|
2024-05-17 21:38:38 +02:00
|
|
|
.validate(
|
|
|
|
|
signature,
|
|
|
|
|
new Date(Date.parse(date)),
|
|
|
|
|
context.req.method as HttpVerb,
|
2024-05-17 23:42:42 +02:00
|
|
|
reqUrl,
|
2024-05-17 21:38:38 +02:00
|
|
|
await context.req.text(),
|
|
|
|
|
)
|
2024-05-15 02:35:13 +02:00
|
|
|
.catch((e) => {
|
|
|
|
|
dualLogger.logError(
|
|
|
|
|
LogLevel.ERROR,
|
|
|
|
|
"Inbox.Signature",
|
|
|
|
|
e as Error,
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
});
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
return errorResponse("Invalid signature", 400);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-15 02:35:13 +02:00
|
|
|
const validator = new EntityValidator();
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Add sent data to database
|
2024-05-15 02:35:13 +02:00
|
|
|
switch (body.type) {
|
2024-05-06 09:16:33 +02:00
|
|
|
case "Note": {
|
2024-05-15 02:35:13 +02:00
|
|
|
const note = await validator.Note(body);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
const account = await User.resolve(note.author);
|
|
|
|
|
|
|
|
|
|
if (!account) {
|
|
|
|
|
return errorResponse("Author not found", 404);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newStatus = await resolveNote(
|
|
|
|
|
undefined,
|
|
|
|
|
note,
|
|
|
|
|
).catch((e) => {
|
|
|
|
|
dualLogger.logError(
|
|
|
|
|
LogLevel.ERROR,
|
|
|
|
|
"Inbox.NoteResolve",
|
|
|
|
|
e as Error,
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!newStatus) {
|
|
|
|
|
return errorResponse("Failed to add status", 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response("Note created", 201);
|
|
|
|
|
}
|
|
|
|
|
case "Follow": {
|
2024-05-15 02:35:13 +02:00
|
|
|
const follow = await validator.Follow(body);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
const account = await User.resolve(follow.author);
|
|
|
|
|
|
|
|
|
|
if (!account) {
|
|
|
|
|
return errorResponse("Author not found", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const foundRelationship =
|
|
|
|
|
await getRelationshipToOtherUser(account, user);
|
|
|
|
|
|
|
|
|
|
// Check if already following
|
|
|
|
|
if (foundRelationship.following) {
|
|
|
|
|
return response("Already following", 200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await db
|
|
|
|
|
.update(Relationships)
|
|
|
|
|
.set({
|
|
|
|
|
following: !user.getUser().isLocked,
|
|
|
|
|
requested: user.getUser().isLocked,
|
|
|
|
|
showingReblogs: true,
|
|
|
|
|
notifying: true,
|
|
|
|
|
languages: [],
|
|
|
|
|
})
|
|
|
|
|
.where(eq(Relationships.id, foundRelationship.id));
|
|
|
|
|
|
|
|
|
|
await db.insert(Notifications).values({
|
|
|
|
|
accountId: account.id,
|
|
|
|
|
type: user.getUser().isLocked
|
|
|
|
|
? "follow_request"
|
|
|
|
|
: "follow",
|
|
|
|
|
notifiedId: user.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user.getUser().isLocked) {
|
|
|
|
|
// Federate FollowAccept
|
|
|
|
|
await sendFollowAccept(account, user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response("Follow request sent", 200);
|
|
|
|
|
}
|
|
|
|
|
case "FollowAccept": {
|
2024-05-15 02:35:13 +02:00
|
|
|
const followAccept = await validator.FollowAccept(body);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
console.log(followAccept);
|
|
|
|
|
|
|
|
|
|
const account = await User.resolve(followAccept.author);
|
|
|
|
|
|
|
|
|
|
if (!account) {
|
|
|
|
|
return errorResponse("Author not found", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(account);
|
|
|
|
|
|
|
|
|
|
const foundRelationship =
|
|
|
|
|
await getRelationshipToOtherUser(user, account);
|
|
|
|
|
|
|
|
|
|
console.log(foundRelationship);
|
|
|
|
|
|
|
|
|
|
if (!foundRelationship.requested) {
|
|
|
|
|
return response(
|
|
|
|
|
"There is no follow request to accept",
|
|
|
|
|
200,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await db
|
|
|
|
|
.update(Relationships)
|
|
|
|
|
.set({
|
|
|
|
|
following: true,
|
|
|
|
|
requested: false,
|
|
|
|
|
})
|
|
|
|
|
.where(eq(Relationships.id, foundRelationship.id));
|
|
|
|
|
|
|
|
|
|
return response("Follow request accepted", 200);
|
|
|
|
|
}
|
|
|
|
|
case "FollowReject": {
|
2024-05-15 02:35:13 +02:00
|
|
|
const followReject = await validator.FollowReject(body);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
|
|
|
|
const account = await User.resolve(followReject.author);
|
|
|
|
|
|
|
|
|
|
if (!account) {
|
|
|
|
|
return errorResponse("Author not found", 400);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const foundRelationship =
|
|
|
|
|
await getRelationshipToOtherUser(user, account);
|
|
|
|
|
|
|
|
|
|
if (!foundRelationship.requested) {
|
|
|
|
|
return response(
|
|
|
|
|
"There is no follow request to reject",
|
|
|
|
|
200,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await db
|
|
|
|
|
.update(Relationships)
|
|
|
|
|
.set({
|
|
|
|
|
requested: false,
|
|
|
|
|
following: false,
|
|
|
|
|
})
|
|
|
|
|
.where(eq(Relationships.id, foundRelationship.id));
|
|
|
|
|
|
|
|
|
|
return response("Follow request rejected", 200);
|
|
|
|
|
}
|
|
|
|
|
default: {
|
|
|
|
|
return errorResponse(
|
|
|
|
|
"Object has not been implemented",
|
|
|
|
|
400,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (isValidationError(e)) {
|
|
|
|
|
return errorResponse(e.message, 400);
|
|
|
|
|
}
|
|
|
|
|
dualLogger.logError(LogLevel.ERROR, "Inbox", e as Error);
|
|
|
|
|
return jsonResponse(
|
|
|
|
|
{
|
|
|
|
|
error: "Failed to process request",
|
|
|
|
|
message: (e as Error).message,
|
|
|
|
|
},
|
|
|
|
|
500,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|