gallery.badmanners.xyz/src/pages/api/export-story/[...id].ts
2024-12-05 21:43:04 -03:00

170 lines
6 KiB
TypeScript

import type { APIRoute, GetStaticPaths } from "astro";
import { getCollection, type CollectionEntry, getEntries } from "astro:content";
import type { PostWebsite } from "src/content.config";
import { t } from "@i18n";
import { formatCopyrightedCharacters } from "@utils/format_copyrighted_characters";
import { markdownToBbcode } from "@utils/markdown_to_bbcode";
import { getUsernameForLang } from "@utils/get_username_for_lang";
import { isAnonymousUser } from "@utils/is_anonymous_user";
import { qualifyLocalURLsInMarkdown } from "@utils/qualify_local_urls_in_markdown";
import { getWebsiteLinkForUser } from "@utils/get_website_link_for_user";
import { toPlainMarkdown } from "@utils/to_plain_markdown";
interface ExportWebsiteInfo {
website: PostWebsite;
exportFormat: "bbcode" | "markdown";
}
const WEBSITE_LIST = [
{ website: "eka", exportFormat: "bbcode" },
{ website: "furaffinity", exportFormat: "bbcode" },
{ website: "inkbunny", exportFormat: "bbcode" },
{ website: "sofurry", exportFormat: "bbcode" },
{ website: "weasyl", exportFormat: "markdown" },
] as const satisfies ExportWebsiteInfo[];
type ExportWebsiteName = typeof WEBSITE_LIST extends ReadonlyArray<{ website: infer K }> ? K : never;
export type ExportStoryResponse = {
story: string;
description: Record<string, string>;
thumbnail: string | null;
};
type Props = {
story: CollectionEntry<"stories">;
};
type Params = {
id: CollectionEntry<"stories">["id"];
};
export const getStaticPaths: GetStaticPaths = async () => {
if (import.meta.env.PROD) {
return [];
}
return (await getCollection("stories")).map((story) => ({
params: { id: story.id } satisfies Params,
props: { story } satisfies Props,
}));
};
export const GET: APIRoute<Props, Params> = async ({ props: { story }, site }) => {
try {
const { lang } = story.data;
if (!story.body) {
throw new Error("Story body cannot be empty");
}
const copyrightedCharacters = await formatCopyrightedCharacters(story.data.copyrightedCharacters);
const authorsList = await getEntries(story.data.authors);
const commissionersList = story.data.commissioners && (await getEntries(story.data.commissioners));
const requestersList = story.data.requesters && (await getEntries(story.data.requesters));
const description = await Promise.all(
WEBSITE_LIST.map(async ({ website, exportFormat }) => {
const exportWebsite: ExportWebsiteName = website;
const u = (user: CollectionEntry<"users">) =>
isAnonymousUser(user)
? getUsernameForLang(user, lang)
: getWebsiteLinkForUser(user, exportWebsite, (user) => getUsernameForLang(user, lang));
const storyDescription = await [
story.data.description,
`*${t(lang, "story/warnings", story.data.wordCount, story.data.contentWarning)}*`,
t(
lang,
"export_story/authors",
authorsList.map((author) => u(author)),
),
requestersList &&
t(
lang,
"export_story/request_for",
requestersList.map((requester) => u(requester)),
),
commissionersList &&
t(
lang,
"export_story/commissioned_by",
commissionersList.map((commissioner) => u(commissioner)),
),
...copyrightedCharacters.map(({ user, characters }) =>
t(lang, "characters/characters_are_copyrighted_by", u(user), characters[0] === "" ? [] : characters),
),
].reduce(async (promise, data) => {
if (!data) {
return promise;
}
const acc = await promise;
const newData = await qualifyLocalURLsInMarkdown(data, lang, site, exportWebsite);
return `${acc}\n\n${newData}`;
}, Promise.resolve(""));
switch (exportFormat) {
case "bbcode":
return {
descriptionFilename: `description_${exportWebsite}.txt`,
descriptionText: markdownToBbcode(storyDescription).replaceAll(/\n\n\n+/g, "\n\n"),
};
case "markdown":
return {
descriptionFilename: `description_${exportWebsite}.md`,
descriptionText: toPlainMarkdown(storyDescription)
.replaceAll(/\n\n\n+/g, "\n\n")
.trim(),
};
default:
const unknown: never = exportFormat;
throw new Error(`Unknown export format "${unknown}"`);
}
}),
);
const storyHeader =
`${story.data.title}\n` +
`${t(
lang,
"story/authors",
authorsList.map((author) => getUsernameForLang(author, lang)),
)}\n` +
(requestersList
? `${t(
lang,
"story/requested_by",
requestersList.map((requester) => getUsernameForLang(requester, lang)),
)}\n`
: "") +
(commissionersList
? `${t(
lang,
"story/commissioned_by",
commissionersList.map((commissioner) => getUsernameForLang(commissioner, lang)),
)}\n`
: "");
const storyText = `${storyHeader}\n===\n\n${story.body.replaceAll(/\\([=*])/g, "$1")}`
.replaceAll(/\n\n\n+/g, "\n\n")
.trim();
return new Response(
JSON.stringify({
story: storyText,
description: description.reduce(
(acc, item) => {
acc[item.descriptionFilename] = item.descriptionText;
return acc;
},
{} as Record<string, string>,
),
thumbnail: story.data.thumbnail ? story.data.thumbnail.src : null,
} satisfies ExportStoryResponse),
{ headers: { "Content-Type": "application/json; charset=utf-8" } },
);
} catch (e) {
return new Response(
JSON.stringify({
message: (e as Error).message ?? null,
stack: (e as Error).stack ?? null,
}),
{ status: 500, headers: { "Content-Type": "application/json; charset=utf-8" } },
);
}
};