Files
erika/lib/share/bot.ts
T

83 lines
2.0 KiB
TypeScript

import { timingSafeEqual } from "node:crypto";
import { ShareHttpError } from "@/lib/share/http-error";
export type BotSenderProfile = {
id: string;
username: string;
globalName: string | null;
avatarUrl: string | null;
};
const FORBIDDEN_PROFILE_FIELDS = [
"displayName",
"avatarUrl",
"authorName",
"authorAvatarUrl",
] as const;
export function hasValidShareBotBearer(
authorization: string | null,
expected: string,
) {
if (!authorization?.startsWith("Bearer ")) return false;
const suppliedBytes = Buffer.from(authorization.slice("Bearer ".length));
const expectedBytes = Buffer.from(expected);
return suppliedBytes.length === expectedBytes.length &&
timingSafeEqual(suppliedBytes, expectedBytes);
}
export function rejectSuppliedBotProfile(formData: FormData) {
for (const field of FORBIDDEN_PROFILE_FIELDS) {
if (formData.has(field)) {
throw new ShareHttpError(
`Profile field ${field} must not be supplied`,
400,
);
}
}
}
export async function resolveBotSenderProfile(
discordId: string,
lookup: (discordId: string) => Promise<BotSenderProfile | null>,
) {
let profile: BotSenderProfile | null;
try {
profile = await lookup(discordId);
} catch {
throw new ShareHttpError("Discord profile lookup is unavailable", 503);
}
if (!profile) {
throw new ShareHttpError(
"Discord sender was not found in the configured guild",
422,
);
}
return profile;
}
export function buildBotShareResponse({
baseUrl,
share,
displayName,
avatarUrl,
imagePath,
}: {
baseUrl: string;
share: { id: string; createdAt: Date };
displayName: string;
avatarUrl: string | null;
imagePath: string | null;
}) {
return {
success: true as const,
share: {
id: share.id,
url: `${baseUrl}/share/${share.id}`,
author: { displayName, avatarUrl },
imageUrl: imagePath ? `${baseUrl}${imagePath}` : null,
createdAt: share.createdAt.toISOString(),
},
};
}