mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(federation): ♻️ Refactor user inbox API to reduce complexity
This commit is contained in:
parent
7638a094f4
commit
d06301ed72
|
|
@ -1,14 +1,25 @@
|
||||||
import { apiRoute, applyConfig, debugRequest } from "@/api";
|
import { apiRoute, applyConfig, debugRequest } from "@/api";
|
||||||
import { sentry } from "@/sentry";
|
import { sentry } from "@/sentry";
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { type Logger, getLogger } from "@logtape/logtape";
|
||||||
import {
|
import {
|
||||||
EntityValidator,
|
EntityValidator,
|
||||||
RequestParserHandler,
|
RequestParserHandler,
|
||||||
SignatureValidator,
|
SignatureValidator,
|
||||||
} from "@versia/federation";
|
} from "@versia/federation";
|
||||||
import type { Entity } from "@versia/federation/types";
|
import type {
|
||||||
|
Entity,
|
||||||
|
Delete as VersiaDelete,
|
||||||
|
Follow as VersiaFollow,
|
||||||
|
FollowAccept as VersiaFollowAccept,
|
||||||
|
FollowReject as VersiaFollowReject,
|
||||||
|
LikeExtension as VersiaLikeExtension,
|
||||||
|
Note as VersiaNote,
|
||||||
|
User as VersiaUser,
|
||||||
|
} from "@versia/federation/types";
|
||||||
|
import type { SocketAddress } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Context, TypedResponse } from "hono";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type ValidationError, isValidationError } from "zod-validation-error";
|
import { type ValidationError, isValidationError } from "zod-validation-error";
|
||||||
|
|
@ -151,47 +162,34 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestIp = context.env?.ip;
|
const requestIp = context.env?.ip ?? null;
|
||||||
|
|
||||||
let checkSignature = true;
|
let checkSignature = true;
|
||||||
|
|
||||||
if (config.federation.bridge.enabled) {
|
if (config.federation.bridge.enabled) {
|
||||||
const token = authorization?.split("Bearer ")[1];
|
const token = authorization?.split("Bearer ")[1];
|
||||||
if (token) {
|
if (token) {
|
||||||
// Request is bridge request
|
const bridgeResponse = await handleBridgeRequest(
|
||||||
if (token !== config.federation.bridge.token) {
|
token,
|
||||||
return context.json(
|
requestIp,
|
||||||
{
|
context,
|
||||||
error: "An invalid token was passed in the Authorization header. Please use the correct token, or remove the Authorization header.",
|
);
|
||||||
},
|
if (bridgeResponse) {
|
||||||
401,
|
return bridgeResponse;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestIp?.address) {
|
|
||||||
if (config.federation.bridge.allowed_ips.length > 0) {
|
|
||||||
checkSignature = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const ip of config.federation.bridge.allowed_ips) {
|
|
||||||
if (matches(ip, requestIp?.address)) {
|
|
||||||
checkSignature = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "Request IP address is not available",
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
checkSignature = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sender = await User.resolve(signedBy);
|
const sender = await User.resolve(signedBy);
|
||||||
|
|
||||||
|
if (!sender) {
|
||||||
|
return context.json(
|
||||||
|
{ error: `Couldn't resolve sender ${signedBy}` },
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (sender?.isLocal()) {
|
if (sender?.isLocal()) {
|
||||||
return context.json(
|
return context.json(
|
||||||
{ error: "Cannot send federation requests to local users" },
|
{ error: "Cannot send federation requests to local users" },
|
||||||
|
|
@ -202,53 +200,22 @@ export default apiRoute((app) =>
|
||||||
const hostname = sender?.data.instance?.baseUrl ?? "";
|
const hostname = sender?.data.instance?.baseUrl ?? "";
|
||||||
|
|
||||||
// Check if Origin is defederated
|
// Check if Origin is defederated
|
||||||
if (
|
if (isDefederated(hostname)) {
|
||||||
config.federation.blocked.find(
|
// Return 201 to not make the sender think there's an error
|
||||||
(blocked) =>
|
|
||||||
blocked.includes(hostname) || hostname.includes(blocked),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Pretend to accept request
|
|
||||||
return context.newResponse(null, 201);
|
return context.newResponse(null, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify request signature
|
// Verify request signature
|
||||||
if (checkSignature) {
|
if (checkSignature) {
|
||||||
if (!sender) {
|
const signatureResponse = await verifySignature(
|
||||||
return context.json({ error: "Could not resolve sender" }, 400);
|
sender,
|
||||||
}
|
signature,
|
||||||
|
nonce,
|
||||||
if (config.debug.federation) {
|
context,
|
||||||
// Log public key
|
logger,
|
||||||
logger.debug`Sender public key: ${sender.data.publicKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validator = await SignatureValidator.fromStringKey(
|
|
||||||
sender.data.publicKey,
|
|
||||||
);
|
);
|
||||||
|
if (signatureResponse) {
|
||||||
const isValid = await validator
|
return signatureResponse;
|
||||||
.validate(
|
|
||||||
new Request(context.req.url, {
|
|
||||||
method: context.req.method,
|
|
||||||
headers: {
|
|
||||||
"X-Signature": signature,
|
|
||||||
"X-Nonce": nonce,
|
|
||||||
},
|
|
||||||
body: await context.req.text(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error`${e}`;
|
|
||||||
sentry?.captureException(e);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
return context.json(
|
|
||||||
{ error: "Signature could not be verified" },
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,237 +224,505 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await handler.parseBody<Response>({
|
return await handler.parseBody<Response>({
|
||||||
note: async (note) => {
|
note: async (note) => handleNoteRequest(note, context, logger),
|
||||||
const account = await User.resolve(note.author);
|
follow: async (follow) =>
|
||||||
|
handleFollowRequest(follow, user, context),
|
||||||
if (!account) {
|
followAccept: async (followAccept) =>
|
||||||
return context.json({ error: "Author not found" }, 404);
|
handleFollowAcceptRequest(followAccept, user, context),
|
||||||
}
|
followReject: async (followReject) =>
|
||||||
|
handleFollowRejectRequest(followReject, user, context),
|
||||||
const newStatus = await Note.fromVersia(
|
"pub.versia:likes/Like": async (like) =>
|
||||||
note,
|
handleLikeRequest(like, context),
|
||||||
account,
|
delete: async (delete_) =>
|
||||||
).catch((e) => {
|
handleDeleteRequest(delete_, user, context),
|
||||||
logger.error`${e}`;
|
user: async (user) => handleUserRequest(user, context),
|
||||||
sentry?.captureException(e);
|
unknown: () =>
|
||||||
return null;
|
context.json({ error: "Unknown entity type" }, 400),
|
||||||
});
|
|
||||||
|
|
||||||
if (!newStatus) {
|
|
||||||
return context.json(
|
|
||||||
{ error: "Failed to add status" },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.text("Note created", 201);
|
|
||||||
},
|
|
||||||
follow: async (follow) => {
|
|
||||||
const account = await User.resolve(follow.author);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return context.json({ error: "Author not found" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundRelationship =
|
|
||||||
await Relationship.fromOwnerAndSubject(account, user);
|
|
||||||
|
|
||||||
if (foundRelationship.data.following) {
|
|
||||||
return context.text("Already following", 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
await foundRelationship.update({
|
|
||||||
following: !user.data.isLocked,
|
|
||||||
requested: user.data.isLocked,
|
|
||||||
showingReblogs: true,
|
|
||||||
notifying: true,
|
|
||||||
languages: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.insert(Notifications).values({
|
|
||||||
accountId: account.id,
|
|
||||||
type: user.data.isLocked ? "follow_request" : "follow",
|
|
||||||
notifiedId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user.data.isLocked) {
|
|
||||||
await sendFollowAccept(account, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.text("Follow request sent", 200);
|
|
||||||
},
|
|
||||||
followAccept: async (followAccept) => {
|
|
||||||
const account = await User.resolve(followAccept.author);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return context.json({ error: "Author not found" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundRelationship =
|
|
||||||
await Relationship.fromOwnerAndSubject(user, account);
|
|
||||||
|
|
||||||
if (!foundRelationship.data.requested) {
|
|
||||||
return context.text(
|
|
||||||
"There is no follow request to accept",
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await foundRelationship.update({
|
|
||||||
requested: false,
|
|
||||||
following: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return context.text("Follow request accepted", 200);
|
|
||||||
},
|
|
||||||
followReject: async (followReject) => {
|
|
||||||
const account = await User.resolve(followReject.author);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return context.json({ error: "Author not found" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundRelationship =
|
|
||||||
await Relationship.fromOwnerAndSubject(user, account);
|
|
||||||
|
|
||||||
if (!foundRelationship.data.requested) {
|
|
||||||
return context.text(
|
|
||||||
"There is no follow request to reject",
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await foundRelationship.update({
|
|
||||||
requested: false,
|
|
||||||
following: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return context.text("Follow request rejected", 200);
|
|
||||||
},
|
|
||||||
"pub.versia:likes/Like": async (like) => {
|
|
||||||
const author = await User.resolve(like.author);
|
|
||||||
|
|
||||||
if (!author) {
|
|
||||||
return context.json({ error: "Author not found" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const note = await Note.resolve(like.liked);
|
|
||||||
|
|
||||||
if (!note) {
|
|
||||||
return context.json({ error: "Note not found" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
await author.like(note, like.uri);
|
|
||||||
|
|
||||||
return context.text("Like added", 200);
|
|
||||||
},
|
|
||||||
// "delete" is a reserved keyword in JS
|
|
||||||
delete: async (delete_) => {
|
|
||||||
// Delete the specified object from database, if it exists and belongs to the user
|
|
||||||
const toDelete = delete_.deleted;
|
|
||||||
|
|
||||||
switch (delete_.deleted_type) {
|
|
||||||
case "Note": {
|
|
||||||
const note = await Note.fromSql(
|
|
||||||
eq(Notes.uri, toDelete),
|
|
||||||
eq(Notes.authorId, user.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (note) {
|
|
||||||
await note.delete();
|
|
||||||
return context.text("Note deleted", 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "User": {
|
|
||||||
const otherUser = await User.resolve(toDelete);
|
|
||||||
|
|
||||||
if (otherUser) {
|
|
||||||
if (otherUser.id === user.id) {
|
|
||||||
// Delete own account
|
|
||||||
await user.delete();
|
|
||||||
return context.text("Account deleted", 200);
|
|
||||||
}
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "Cannot delete other users than self",
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "pub.versia:likes/Like": {
|
|
||||||
const like = await Like.fromSql(
|
|
||||||
eq(Likes.uri, toDelete),
|
|
||||||
eq(Likes.likerId, user.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (like) {
|
|
||||||
await like.delete();
|
|
||||||
return context.text("Like deleted", 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "Like not found or not owned by user",
|
|
||||||
},
|
|
||||||
404,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: `Deletetion of object ${toDelete} not implemented`,
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
{ error: "Object not found or not owned by user" },
|
|
||||||
404,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
user: async (user) => {
|
|
||||||
// Refetch user to ensure we have the latest data
|
|
||||||
const updatedAccount = await User.saveFromRemote(user.uri);
|
|
||||||
|
|
||||||
if (!updatedAccount) {
|
|
||||||
return context.json(
|
|
||||||
{ error: "Failed to update user" },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.text("User refreshed", 200);
|
|
||||||
},
|
|
||||||
unknown: () => {
|
|
||||||
return context.json({ error: "Unknown entity type" }, 400);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isValidationError(e)) {
|
return handleError(e as Error, context, logger);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles bridge requests.
|
||||||
|
* @param {string} token - The authorization token.
|
||||||
|
* @param {SocketAddress | null} requestIp - The request IP address.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response | null>} - The response or null if no error.
|
||||||
|
*/
|
||||||
|
function handleBridgeRequest(
|
||||||
|
token: string,
|
||||||
|
requestIp: SocketAddress | null,
|
||||||
|
context: Context,
|
||||||
|
): (Response & TypedResponse<{ error: string }, 401 | 500, "json">) | null {
|
||||||
|
if (token !== config.federation.bridge.token) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "An invalid token was passed in the Authorization header. Please use the correct token, or remove the Authorization header.",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestIp?.address) {
|
||||||
|
if (config.federation.bridge.allowed_ips.length > 0) {
|
||||||
|
for (const ip of config.federation.bridge.allowed_ips) {
|
||||||
|
if (matches(ip, requestIp?.address)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Request IP address is not available",
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the hostname is defederated using glob matching.
|
||||||
|
* @param {string} hostname - The hostname to check. Can contain glob patterns.
|
||||||
|
* @returns {boolean} - True if defederated, false otherwise.
|
||||||
|
*/
|
||||||
|
function isDefederated(hostname: string): boolean {
|
||||||
|
const pattern = new Bun.Glob(hostname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
config.federation.blocked.find(
|
||||||
|
(blocked) => pattern.match(blocked) !== null,
|
||||||
|
) !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the request signature.
|
||||||
|
* @param {User} sender - The sender user.
|
||||||
|
* @param {string} signature - The request signature.
|
||||||
|
* @param {string} nonce - The request nonce.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @param {Logger} logger - LogTape logger.
|
||||||
|
* @returns {Promise<Response | null>} - The response or null if no error.
|
||||||
|
*/
|
||||||
|
async function verifySignature(
|
||||||
|
sender: User,
|
||||||
|
signature: string,
|
||||||
|
nonce: string,
|
||||||
|
context: Context,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<
|
||||||
|
(Response & TypedResponse<{ error: string }, 401 | 400, "json">) | null
|
||||||
|
> {
|
||||||
|
if (!sender) {
|
||||||
|
return context.json({ error: "Could not resolve sender" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.debug.federation) {
|
||||||
|
logger.debug`Sender public key: ${sender.data.publicKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = await SignatureValidator.fromStringKey(
|
||||||
|
sender.data.publicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValid = await validator
|
||||||
|
.validate(
|
||||||
|
new Request(context.req.url, {
|
||||||
|
method: context.req.method,
|
||||||
|
headers: {
|
||||||
|
"X-Signature": signature,
|
||||||
|
"X-Nonce": nonce,
|
||||||
|
},
|
||||||
|
body: await context.req.text(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error`${e}`;
|
||||||
|
sentry?.captureException(e);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return context.json({ error: "Signature could not be verified" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Note entity processing.
|
||||||
|
*
|
||||||
|
* @param {VersiaNote} note - Note entity to process.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @param {Logger} logger - LogTape logger.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleNoteRequest(
|
||||||
|
note: VersiaNote,
|
||||||
|
context: Context,
|
||||||
|
logger: Logger,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<
|
||||||
|
| {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
| string,
|
||||||
|
404 | 500 | 201,
|
||||||
|
"json" | "text"
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const account = await User.resolve(note.author);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return context.json({ error: "Author not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStatus = await Note.fromVersia(note, account).catch((e) => {
|
||||||
|
logger.error`${e}`;
|
||||||
|
sentry?.captureException(e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newStatus) {
|
||||||
|
return context.json({ error: "Failed to add status" }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.text("Note created", 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Follow entity processing.
|
||||||
|
*
|
||||||
|
* @param {VersiaFollow} follow - Follow entity to process.
|
||||||
|
* @param {User} user - Owner of this inbox.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleFollowRequest(
|
||||||
|
follow: VersiaFollow,
|
||||||
|
user: User,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<
|
||||||
|
| {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
| string,
|
||||||
|
200 | 400,
|
||||||
|
"text" | "json"
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const account = await User.resolve(follow.author);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return context.json({ error: "Author not found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||||
|
account,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundRelationship.data.following) {
|
||||||
|
return context.text("Already following", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
await foundRelationship.update({
|
||||||
|
following: !user.data.isLocked,
|
||||||
|
requested: user.data.isLocked,
|
||||||
|
showingReblogs: true,
|
||||||
|
notifying: true,
|
||||||
|
languages: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(Notifications).values({
|
||||||
|
accountId: account.id,
|
||||||
|
type: user.data.isLocked ? "follow_request" : "follow",
|
||||||
|
notifiedId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user.data.isLocked) {
|
||||||
|
await sendFollowAccept(account, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.text("Follow request sent", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles FollowAccept entity processing
|
||||||
|
*
|
||||||
|
* @param {VersiaFollowAccept} followAccept - FollowAccept entity to process.
|
||||||
|
* @param {User} user - Owner of this inbox.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleFollowAcceptRequest(
|
||||||
|
followAccept: VersiaFollowAccept,
|
||||||
|
user: User,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<{ error: string } | string, 200 | 400, "text" | "json">
|
||||||
|
> {
|
||||||
|
const account = await User.resolve(followAccept.author);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return context.json({ error: "Author not found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!foundRelationship.data.requested) {
|
||||||
|
return context.text("There is no follow request to accept", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
await foundRelationship.update({
|
||||||
|
requested: false,
|
||||||
|
following: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return context.text("Follow request accepted", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles FollowReject entity processing
|
||||||
|
*
|
||||||
|
* @param {VersiaFollowReject} followReject - FollowReject entity to process.
|
||||||
|
* @param {User} user - Owner of this inbox.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleFollowRejectRequest(
|
||||||
|
followReject: VersiaFollowReject,
|
||||||
|
user: User,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<{ error: string } | string, 200 | 400, "text" | "json">
|
||||||
|
> {
|
||||||
|
const account = await User.resolve(followReject.author);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return context.json({ error: "Author not found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!foundRelationship.data.requested) {
|
||||||
|
return context.text("There is no follow request to reject", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
await foundRelationship.update({
|
||||||
|
requested: false,
|
||||||
|
following: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return context.text("Follow request rejected", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Like entity processing.
|
||||||
|
*
|
||||||
|
* @param {VersiaLikeExtension} like - Like entity to process.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleLikeRequest(
|
||||||
|
like: VersiaLikeExtension,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<{ error: string } | string, 200 | 400, "text" | "json">
|
||||||
|
> {
|
||||||
|
const author = await User.resolve(like.author);
|
||||||
|
|
||||||
|
if (!author) {
|
||||||
|
return context.json({ error: "Author not found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await Note.resolve(like.liked);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
return context.json({ error: "Note not found" }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await author.like(note, like.uri);
|
||||||
|
|
||||||
|
return context.text("Like added", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Delete entity processing.
|
||||||
|
*
|
||||||
|
* @param {VersiaDelete} delete_ - Delete entity to process.
|
||||||
|
* @param {User} user - Owner of this inbox.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleDeleteRequest(
|
||||||
|
delete_: VersiaDelete,
|
||||||
|
user: User,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<
|
||||||
|
{ error: string } | string,
|
||||||
|
200 | 400 | 404,
|
||||||
|
"text" | "json"
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const toDelete = delete_.deleted;
|
||||||
|
|
||||||
|
switch (delete_.deleted_type) {
|
||||||
|
case "Note": {
|
||||||
|
const note = await Note.fromSql(
|
||||||
|
eq(Notes.uri, toDelete),
|
||||||
|
eq(Notes.authorId, user.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
await note.delete();
|
||||||
|
return context.text("Note deleted", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "User": {
|
||||||
|
const otherUser = await User.resolve(toDelete);
|
||||||
|
|
||||||
|
if (otherUser) {
|
||||||
|
if (otherUser.id === user.id) {
|
||||||
|
await user.delete();
|
||||||
|
return context.text("Account deleted", 200);
|
||||||
|
}
|
||||||
return context.json(
|
return context.json(
|
||||||
{
|
{
|
||||||
error: "Failed to process request",
|
error: "Cannot delete other users than self",
|
||||||
error_description: (e as ValidationError).message,
|
|
||||||
},
|
},
|
||||||
400,
|
400,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
logger.error`${e}`;
|
|
||||||
sentry?.captureException(e);
|
break;
|
||||||
|
}
|
||||||
|
case "pub.versia:likes/Like": {
|
||||||
|
const like = await Like.fromSql(
|
||||||
|
eq(Likes.uri, toDelete),
|
||||||
|
eq(Likes.likerId, user.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (like) {
|
||||||
|
await like.delete();
|
||||||
|
return context.text("Like deleted", 200);
|
||||||
|
}
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
{
|
{
|
||||||
error: "Failed to process request",
|
error: "Like not found or not owned by user",
|
||||||
message: (e as Error).message,
|
|
||||||
},
|
},
|
||||||
500,
|
404,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
default: {
|
||||||
);
|
return context.json(
|
||||||
|
{
|
||||||
|
error: `Deletion of object ${toDelete} not implemented`,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.json(
|
||||||
|
{ error: "Object not found or not owned by user" },
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles User entity processing (profile edits).
|
||||||
|
*
|
||||||
|
* @param {VersiaUser} user - User entity to process.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @returns {Promise<Response>} - The response.
|
||||||
|
*/
|
||||||
|
async function handleUserRequest(
|
||||||
|
user: VersiaUser,
|
||||||
|
context: Context,
|
||||||
|
): Promise<
|
||||||
|
Response &
|
||||||
|
TypedResponse<{ error: string } | string, 200 | 500, "text" | "json">
|
||||||
|
> {
|
||||||
|
const updatedAccount = await User.saveFromRemote(user.uri);
|
||||||
|
|
||||||
|
if (!updatedAccount) {
|
||||||
|
return context.json({ error: "Failed to update user" }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.text("User refreshed", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes Errors into the appropriate HTTP response.
|
||||||
|
*
|
||||||
|
* @param {Error} e - The error object.
|
||||||
|
* @param {Context} context - Hono request context.
|
||||||
|
* @param {any} logger - LogTape logger.
|
||||||
|
* @returns {Response} - The error response.
|
||||||
|
*/
|
||||||
|
function handleError(
|
||||||
|
e: Error,
|
||||||
|
context: Context,
|
||||||
|
logger: Logger,
|
||||||
|
):
|
||||||
|
| (Response &
|
||||||
|
TypedResponse<
|
||||||
|
{
|
||||||
|
error: string;
|
||||||
|
error_description: string;
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"json"
|
||||||
|
>)
|
||||||
|
| (Response &
|
||||||
|
TypedResponse<
|
||||||
|
{
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
"json"
|
||||||
|
>) {
|
||||||
|
if (isValidationError(e)) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Failed to process request",
|
||||||
|
error_description: (e as ValidationError).message,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.error`${e}`;
|
||||||
|
sentry?.captureException(e);
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Failed to process request",
|
||||||
|
message: (e as Error).message,
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue