170 lines
6 KiB
TypeScript
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" } },
|
|
);
|
|
}
|
|
};
|