Files

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);
}
}