113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import { cdnUrl } from "@/lib/cdn-images";
|
|
import { ensureDiscordUser } from "@/lib/auth/discord-user";
|
|
import { getCanonicalUrl, getShareBotSecret } from "@/lib/config/server";
|
|
import { getDiscordSenderProfile } from "@/lib/discord/discord";
|
|
import {
|
|
buildBotShareResponse,
|
|
hasValidShareBotBearer,
|
|
rejectSuppliedBotProfile,
|
|
resolveBotSenderProfile,
|
|
} from "@/lib/share/bot";
|
|
import { createShare } from "@/lib/share/create";
|
|
import { ShareHttpError, shareErrorResponse } from "@/lib/share/http-error";
|
|
import { enforceShareRateLimit } from "@/lib/share/rate-limit";
|
|
import {
|
|
validateOptionalImage,
|
|
validateShareDescription,
|
|
validateTextFile,
|
|
} from "@/lib/share/validation";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
function hasValidBearerToken(request: Request) {
|
|
const authorization = request.headers.get("authorization");
|
|
let expected: string;
|
|
try {
|
|
expected = getShareBotSecret();
|
|
} catch {
|
|
throw new ShareHttpError("Share bot API is not configured", 503);
|
|
}
|
|
return hasValidShareBotBearer(authorization, expected);
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
if (!hasValidBearerToken(request)) {
|
|
throw new ShareHttpError("Unauthorized", 401);
|
|
}
|
|
const baseUrl = getCanonicalUrl();
|
|
|
|
await enforceShareRateLimit({
|
|
key: "bot:global",
|
|
limit: 60,
|
|
windowSeconds: 60 * 60,
|
|
});
|
|
|
|
let formData: FormData;
|
|
try {
|
|
formData = await request.formData();
|
|
} catch {
|
|
throw new ShareHttpError("Malformed multipart form data", 400);
|
|
}
|
|
rejectSuppliedBotProfile(formData);
|
|
const file = formData.get("file");
|
|
const senderDiscordId = formData.get("senderDiscordId");
|
|
if (!(file instanceof File)) {
|
|
throw new ShareHttpError("A .txt file is required", 400);
|
|
}
|
|
if (
|
|
typeof senderDiscordId !== "string" ||
|
|
!/^\d{15,22}$/.test(senderDiscordId)
|
|
) {
|
|
throw new ShareHttpError("A valid senderDiscordId is required", 400);
|
|
}
|
|
|
|
const [content, image] = await Promise.all([
|
|
validateTextFile(file),
|
|
validateOptionalImage(formData.get("image")),
|
|
]);
|
|
const description = validateShareDescription(formData.get("description"));
|
|
const profile = await resolveBotSenderProfile(
|
|
senderDiscordId,
|
|
getDiscordSenderProfile,
|
|
);
|
|
|
|
await enforceShareRateLimit({
|
|
key: `sender:${senderDiscordId}`,
|
|
limit: 10,
|
|
windowSeconds: 60 * 60,
|
|
});
|
|
|
|
const displayName = profile.globalName?.trim() || profile.username;
|
|
const user = await ensureDiscordUser({
|
|
discordId: senderDiscordId,
|
|
displayName,
|
|
avatarUrl: profile.avatarUrl,
|
|
});
|
|
const share = await createShare({
|
|
content,
|
|
description,
|
|
image,
|
|
source: "bot",
|
|
author: {
|
|
userId: user.id,
|
|
discordId: senderDiscordId,
|
|
displayName,
|
|
avatarUrl: profile.avatarUrl,
|
|
},
|
|
});
|
|
return Response.json(
|
|
buildBotShareResponse({
|
|
baseUrl,
|
|
share,
|
|
displayName,
|
|
avatarUrl: profile.avatarUrl,
|
|
imagePath: share.imageCdnId ? cdnUrl(share.imageCdnId) : null,
|
|
}),
|
|
{ status: 201 }
|
|
);
|
|
} catch (error) {
|
|
return shareErrorResponse(error);
|
|
}
|
|
}
|