add random, add autocomplete, more smaller changes

This commit is contained in:
creations 2025-01-05 20:03:30 -05:00
parent 5fe235194b
commit 608f4d5e8d
Signed by: creations
GPG key ID: 8F553AA4320FC711
10 changed files with 1039 additions and 66 deletions

View file

@ -15,13 +15,15 @@ export const booruConfig: IBooruConfigMap = {
aliases: ["rule34", "r34", "rule34xxx"],
endpoint: "api.rule34.xxx",
functions: booruDefaults,
autocomplete: "ac.rule34.xxx/autocomplete.php?q=",
},
"realbooru.com": {
enabled: false,
enabled: true,
name: "realbooru.com",
aliases: ["realbooru", "rb", "real34"],
endpoint: "realbooru.com",
functions: booruDefaults,
autocomplete: "realbooru.com/index.php?page=autocomplete&term=",
},
"safebooru.org": {
enabled: true,
@ -29,6 +31,7 @@ export const booruConfig: IBooruConfigMap = {
aliases: ["safebooru", "sb", "s34"],
endpoint: "safebooru.org",
functions: booruDefaults,
autocomplete: "safebooru.org/autocomplete.php?q=",
},
"tbib.org": {
enabled: true,
@ -36,6 +39,7 @@ export const booruConfig: IBooruConfigMap = {
aliases: ["tbib", "tb", "tbiborg"],
endpoint: "tbib.org",
functions: booruDefaults,
autocomplete: "tbib.org/autocomplete.php?q=",
},
"hypnohub.net": {
enabled: true,
@ -43,6 +47,7 @@ export const booruConfig: IBooruConfigMap = {
aliases: ["hypnohub", "hh", "hypnohubnet"],
endpoint: "hypnohub.net",
functions: booruDefaults,
autocomplete: "hypnohub.net/autocomplete.php?q=",
},
"xbooru.com": {
enabled: true,
@ -50,12 +55,14 @@ export const booruConfig: IBooruConfigMap = {
aliases: ["xbooru", "xb", "xboorucom"],
endpoint: "xbooru.com",
functions: booruDefaults,
autocomplete: "xbooru.com/autocomplete.php?q=",
},
"e621.net": {
enabled: true,
name: "e621.net",
aliases: ["e621", "e6", "e621net"],
endpoint: "e621.net",
autocomplete: "e621.net/tags/autocomplete.json?search[name_matches]=",
functions: {
search: "posts.json",
random: "defaultRandom",

View file

@ -103,8 +103,8 @@ class RedisJson {
type: "JSON" | "STRING",
key: string,
value: unknown,
path?: string,
expiresInSeconds?: number,
path?: string,
): Promise<void> {
if (!this.client) {
logger.error("Redis client is not initialized.");

View file

@ -43,7 +43,7 @@ export function tagsToExpectedFormat(
.join(delimiter);
}
export function shufflePosts<T>(posts: T[]): T[] {
export function shufflePosts<BooruPost>(posts: BooruPost[]): BooruPost[] {
for (let i: number = posts.length - 1; i > 0; i--) {
const j: number = Math.floor(Math.random() * (i + 1));
[posts[i], posts[j]] = [posts[j], posts[i]];
@ -51,6 +51,13 @@ export function shufflePosts<T>(posts: T[]): T[] {
return posts;
}
export function minPosts<BooruPost>(
posts: BooruPost[],
min: number,
): BooruPost[] {
return posts.slice(0, min);
}
export function determineBooru(
booruName: string,
): IBooruConfigMap[keyof IBooruConfigMap] | null {
@ -66,40 +73,49 @@ export function determineBooru(
export function postExpectedFormat(
booru: IBooruConfig,
posts: BooruPost[],
posts: BooruPost[] | BooruPost,
): { posts: BooruPost[] } | null {
if (!posts) return null;
posts = Array.isArray(posts) ? posts : [posts];
if (posts.length === 0) return null;
const normalizedPosts: BooruPost[] = Array.isArray(posts) ? posts : [posts];
if (normalizedPosts.length === 0) return null;
if (booru.name === "e621.net")
if (booru.name === "e621.net") {
return {
posts: normalizedPosts.map((post: BooruPost) => {
return {
posts: posts.map(
(post: BooruPost): BooruPost => ({
...post,
file_url: post.file_url,
post_url: `https://${booru.endpoint}/posts/${post.id}`,
tags: Object.keys(post.tags)
.flatMap(
(key: string) =>
post.tags[key as keyof typeof post.tags],
)
file_url: post.file.url ?? null,
post_url:
post.post_url ??
`https://${booru.endpoint}/posts/${post.id}`,
tags: Object.values(post.tags || {})
.flat()
.join(" "),
}),
),
};
}),
};
}
const fixedDomain: string = booru.endpoint.replace(/^api\./, "");
const formattedPosts: BooruPost[] = posts.map(
(post: BooruPost): BooruPost => {
const postUrl: string = `https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`;
const imageExtension: string = post.image.substring(
post.image.lastIndexOf(".") + 1,
);
const fileUrl: string = `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`;
const formattedPosts: BooruPost[] = normalizedPosts.map(
(post: BooruPost) => {
const postUrl: string =
post.post_url ??
`https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`;
const imageExtension: string =
post.image?.substring(post.image.lastIndexOf(".") + 1) ?? "";
const fileUrl: string | null =
post.file_url ??
(post.directory && post.hash && imageExtension
? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`
: null);
return { ...post, file_url: fileUrl, post_url: postUrl };
return {
...post,
file_url: fileUrl,
post_url: postUrl,
};
},
);

View file

@ -0,0 +1,259 @@
import { determineBooru } from "@helpers/char";
import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "application/json",
};
async function handler(
_request: Request,
_server: BunServer,
_requestBody: unknown,
query: Query,
params: Params,
): Promise<Response> {
const { force } = query as { force: string };
const { booru, tag } = params as { booru: string; tag: string };
if (!booru) {
return Response.json(
{
success: false,
code: 400,
error: "Missing booru",
},
{
status: 400,
},
);
}
if (!tag) {
return Response.json(
{
success: false,
code: 400,
error: "Missing tag",
},
{
status: 400,
},
);
}
const booruConfig: IBooruConfig | null = determineBooru(booru);
if (!booruConfig) {
return Response.json(
{
success: false,
code: 404,
error: "Booru not found",
},
{
status: 404,
},
);
}
if (!booruConfig.enabled) {
return Response.json(
{
success: false,
code: 403,
error: "Booru is disabled",
},
{
status: 403,
},
);
}
const isE621: boolean = booruConfig.name === "e621.net";
if (isE621 && tag.length < 3) {
return Response.json(
{
success: false,
code: 400,
error: "Tag must be at least 3 characters long for e621",
},
{
status: 400,
},
);
}
let editedTag: string = tag;
if (editedTag.startsWith("-")) {
editedTag = editedTag.slice(1);
}
editedTag = editedTag.replace(/\s/g, "_");
const cacheKey: string = `nsfw:${booru}:autocomplete:${editedTag}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
const dataAsType: { count: number; data: unknown } = cacheData as {
count: number;
data: unknown;
};
if (dataAsType.count === 0) {
return Response.json(
{
success: false,
code: 404,
error: "No results found",
},
{
status: 404,
},
);
}
return Response.json(
{
success: true,
code: 200,
cache: true,
count: dataAsType.count,
data: dataAsType.data,
},
{
status: 200,
},
);
}
}
if (!booruConfig.autocomplete) {
return Response.json(
{
success: false,
code: 501,
error: `${booruConfig.name} does not support autocomplete (yet)`,
},
{
status: 501,
},
);
}
const url: string = `https://${booruConfig.autocomplete}${editedTag}`;
try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
: undefined;
const response: Response = await fetch(url, {
headers,
});
if (!response.ok) {
logger.error([
"Failed to fetch post",
`Booru: ${booru}`,
`Status: ${response.status}`,
`Status Text: ${response.statusText}`,
]);
return Response.json(
{
success: false,
code: response.status || 500,
error: response.statusText || "Could not reach booru",
},
{
status: response.status || 500,
},
);
}
const data: unknown = await response.json();
if (!data) {
logger.error([
"No data returned",
`Booru: ${booru}`,
`Tag: ${editedTag}`,
]);
return Response.json(
{
success: false,
code: 404,
error: "No data was returned",
},
{
status: 404,
},
);
}
const resultCount: number = (data as unknown[]).length;
if (resultCount === 0) {
await redis
.getInstance()
.set("JSON", cacheKey, { count: 0, data }, 60 * 60 * 2); // 2 hours
return Response.json(
{
success: false,
code: 404,
error: "No results found",
},
{
status: 404,
},
);
}
await redis
.getInstance()
.set("JSON", cacheKey, { count: resultCount, data }, 60 * 60 * 24); // 24 hours
return Response.json(
{
success: true,
code: 200,
cache: false,
count: resultCount,
data,
},
{
status: 200,
},
);
} catch (error) {
logger.error([
"Failed to fetch post",
`Booru: ${booru}`,
`Tag: ${editedTag}`,
`Error: ${error}`,
]);
return Response.json(
{
success: false,
code: 500,
error: "Could not reach booru",
},
{
status: 500,
},
);
}
}
export { handler, routeDef };

View file

@ -2,6 +2,7 @@ import { determineBooru, postExpectedFormat } from "@helpers/char";
import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger";
const routeDef: RouteDef = {
method: "GET",
@ -10,12 +11,13 @@ const routeDef: RouteDef = {
};
async function handler(
request: Request,
server: BunServer,
requestBody: unknown,
_request: Request,
_server: BunServer,
_requestBody: unknown,
query: Query,
params: Params,
): Promise<Response> {
const { force } = query as { force: string };
const { booru, id } = params as { booru: string; id: string };
if (!booru || !id) {
@ -68,8 +70,11 @@ async function handler(
url = `https://${booruConfig.endpoint}/${start}${id}${end}`;
}
const cacheKey: string = `${booru}:${id}`;
const cacheData: unknown = await redis.getInstance().get("JSON", cacheKey);
const cacheKey: string = `nsfw:${booru}:${id}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
return Response.json(
@ -77,18 +82,34 @@ async function handler(
success: true,
code: 200,
cache: true,
data: cacheData,
post:
(cacheData as { posts: BooruPost[] }).posts[0] || null,
},
{
status: 200,
},
);
}
}
try {
const response: Response = await fetch(url);
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
: undefined;
const response: Response = await fetch(url, {
headers,
});
if (!response.ok) {
logger.error([
"Failed to fetch post",
`Booru: ${booru}`,
`ID: ${id}`,
`Status: ${response.status}`,
`Status Text: ${response.statusText}`,
]);
return Response.json(
{
success: false,
@ -104,11 +125,12 @@ async function handler(
const data: unknown = await response.json();
if (!data) {
logger.error(["No data returned", `Booru: ${booru}`, `ID: ${id}`]);
return Response.json(
{
success: false,
code: 404,
error: "Post not found",
error: "Post not found, or no json data was returned",
},
{
status: 404,
@ -128,6 +150,7 @@ async function handler(
}
if (posts.length === 0) {
logger.error(["No posts found", `Booru: ${booru}`, `ID: ${id}`]);
return Response.json(
{
success: false,
@ -145,6 +168,27 @@ async function handler(
posts,
);
if (!expectedData) {
logger.error([
"Unexpected data format",
`Booru: ${booru}`,
`ID: ${id}`,
`Data: ${JSON.stringify(data)}`,
]);
return Response.json(
{
success: false,
code: 500,
error: "Unexpected data format",
},
{
status: 500,
},
);
}
await redis.getInstance().set("JSON", cacheKey, expectedData, 60 * 30); // 30 minutes
return Response.json(
{
success: true,

View file

@ -0,0 +1,346 @@
import {
determineBooru,
minPosts,
postExpectedFormat,
shufflePosts,
tagsToExpectedFormat,
} from "@helpers/char";
import { fetch } from "bun";
import { redis } from "@/database/redis";
import { logger } from "@/helpers/logger";
const routeDef: RouteDef = {
method: "POST",
accepts: "*/*",
returns: "application/json",
needsBody: "json",
};
async function handler(
_request: Request,
_server: BunServer,
requestBody: unknown,
query: Query,
params: Params,
): Promise<Response> {
const { force } = query as { force: string };
const { booru } = params as { booru: string };
const {
tags,
results = 5,
excludeTags,
} = requestBody as {
tags: string[];
results: number;
excludeTags: string[];
};
if (!booru) {
return Response.json(
{
success: false,
code: 400,
error: "Missing booru",
},
{
status: 400,
},
);
}
if (tags && !(typeof tags === "string" || Array.isArray(tags))) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid tags, must be a string or array of strings",
},
{
status: 400,
},
);
}
if (
excludeTags &&
!(typeof excludeTags === "string" || Array.isArray(excludeTags))
) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid excludeTags, must be a string or array of strings",
},
{
status: 400,
},
);
}
if (results && (typeof results !== "number" || results <= 0)) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid results, must be a number greater than 0",
},
{
status: 400,
},
);
}
const booruConfig: IBooruConfig | null = determineBooru(booru);
if (!booruConfig) {
return Response.json(
{
success: false,
code: 404,
error: "Booru not found",
},
{
status: 404,
},
);
}
if (!booruConfig.enabled) {
return Response.json(
{
success: false,
code: 403,
error: "Booru is disabled",
},
{
status: 403,
},
);
}
const isE621: boolean = booruConfig.name === "e621.net";
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags
? tagsToExpectedFormat(excludeTags, true, isE621)
: "";
const tagsString: () => string = (): string => {
if (formattedTags && formattedExcludeTags) {
return `tags=${formattedTags}+-${formattedExcludeTags}`;
} else if (formattedTags) {
return `tags=${formattedTags}`;
} else if (formattedExcludeTags) {
return `tags=-${formattedExcludeTags}`;
}
return "";
};
const pageString: (page: string | number) => string = (
page: string | number,
): string => {
if (isE621) {
return `page=${page}`;
}
return `pid=${page}`;
};
const resultsString: string = `limit=${isE621 ? 320 : 1000}`;
const getUrl: (pageString: string, resultsString: string) => string = (
page: string,
resultsString: string,
): string => {
const parts: string[] = [
`https://${booruConfig.endpoint}/${booruConfig.functions.search}`,
];
if (isE621) {
parts.push("?");
} else {
parts.push("&");
}
const queryParams: string = [tagsString(), page, resultsString]
.filter(Boolean)
.join("&");
parts.push(queryParams);
return parts.join("");
};
const noResultsCacheKey: string = `nsfw:${booru}:random:noResults:${tagsString()}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", noResultsCacheKey);
if (cacheData) {
return Response.json(
{
success: false,
code: 404,
cache: true,
error: "No posts found with the given tags",
},
{
status: 404,
},
);
}
}
const config: { maxPage: number; maxTries: number } = {
maxPage: 12,
maxTries: 6,
};
let state: { tries: number; page: number } = { tries: 0, page: 16 };
while (state.tries < config.maxTries) {
if (state.tries === config.maxTries) {
state.page = 0;
} else {
const oldPage: number = state.page;
do {
state.page = Math.floor(Math.random() * config.maxPage);
} while (state.page === oldPage);
}
const cacheKey: string = `nsfw:${booru}:random:${tagsString()}:${results}:${state.page}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
const minimizedPosts: BooruPost[] = minPosts(
(cacheData as { posts: BooruPost[] }).posts,
results,
);
const shuffledPosts: BooruPost[] = shufflePosts(minimizedPosts);
return Response.json(
{
success: true,
code: 200,
cache: true,
posts: shuffledPosts,
},
{
status: 200,
},
);
}
}
const url: string = getUrl(pageString(state.page), resultsString);
try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
: undefined;
const response: Response = await fetch(url, {
headers,
});
if (!response.ok) {
return Response.json(
{
success: false,
code: response.status || 500,
error:
response.statusText ||
`Could not reach ${booruConfig.name}`,
},
{
status: response.status || 500,
},
);
}
const data: unknown = await response.json();
if (!data) {
return Response.json(
{
success: false,
code: 500,
error: `No data returned from ${booruConfig.name}`,
},
{
status: 500,
},
);
}
const parsedData: Data = data as Data;
let posts: BooruPost[] = [];
if (parsedData.post) {
posts = [parsedData.post];
} else if (parsedData.posts) {
posts = parsedData.posts;
} else {
posts = Array.isArray(data) ? (data as BooruPost[]) : [];
}
if (posts.length === 0) continue;
let expectedData: { posts: BooruPost[] } | null =
postExpectedFormat(booruConfig, posts);
if (!expectedData) continue;
expectedData.posts = minPosts(expectedData.posts, results);
expectedData.posts = shufflePosts(expectedData.posts);
await redis
.getInstance()
.set("JSON", cacheKey, expectedData, 60 * 60 * 1); // 1 hours
return Response.json(
{
success: true,
code: 200,
cache: false,
posts: expectedData.posts,
},
{
status: 200,
},
);
} catch {
continue;
} finally {
state.tries++;
}
}
await redis.getInstance().set("JSON", noResultsCacheKey, true, 60 * 30); // 30 minutes
logger.error([
"No posts found",
`Booru: ${booru}`,
`Tags: ${tagsString()}`,
`Exclude tags: ${formattedExcludeTags}`,
`Tries: ${state.tries}`,
]);
return Response.json(
{
success: false,
code: 404,
error: "No posts found with the given tags",
},
{
status: 404,
},
);
}
export { handler, routeDef };

View file

@ -0,0 +1,301 @@
import {
determineBooru,
postExpectedFormat,
tagsToExpectedFormat,
} from "@helpers/char";
import { fetch } from "bun";
import { redis } from "@/database/redis";
const routeDef: RouteDef = {
method: "POST",
accepts: "*/*",
returns: "application/json",
needsBody: "json",
};
async function handler(
_request: Request,
_server: BunServer,
requestBody: unknown,
query: Query,
params: Params,
): Promise<Response> {
const { force } = query as { force: string };
const { booru } = params as { booru: string };
const {
page = 0,
tags,
results = 5,
excludeTags,
} = requestBody as {
page: 0;
tags: string[] | string;
results: number;
excludeTags: string[] | string;
};
if (!booru) {
return Response.json(
{
success: false,
code: 400,
error: "Missing booru",
},
{
status: 400,
},
);
}
if (tags && !(typeof tags === "string" || Array.isArray(tags))) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid tags, must be a string or array of strings",
},
{
status: 400,
},
);
}
if (
excludeTags &&
!(typeof excludeTags === "string" || Array.isArray(excludeTags))
) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid excludeTags, must be a string or array of strings",
},
{
status: 400,
},
);
}
if (results && (typeof results !== "number" || results < 1)) {
return Response.json(
{
success: false,
code: 400,
error: "Invalid results, must be a number greater than 0",
},
{
status: 400,
},
);
}
const booruConfig: IBooruConfig | null = determineBooru(booru);
if (!booruConfig) {
return Response.json(
{
success: false,
code: 404,
error: "Booru not found",
},
{
status: 404,
},
);
}
if (!booruConfig.enabled) {
return Response.json(
{
success: false,
code: 403,
error: "Booru is disabled",
},
{
status: 403,
},
);
}
const isE621: boolean = booruConfig.name === "e621.net";
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags
? tagsToExpectedFormat(excludeTags, true, isE621)
: "";
const safePage: string | number = Number.isSafeInteger(page) ? page : 0;
const safeResults: string | number = Number.isSafeInteger(results)
? results
: 5;
const tagsString: () => string = (): string => {
if (formattedTags && formattedExcludeTags) {
return `tags=${formattedTags}+-${formattedExcludeTags}`;
} else if (formattedTags) {
return `tags=${formattedTags}`;
} else if (formattedExcludeTags) {
return `tags=-${formattedExcludeTags}`;
}
return "";
};
const pageString: () => string = (): string => {
if (isE621) {
return `page=${safePage}`;
}
return `pid=${safePage}`;
};
const resultsString: string = `limit=${safeResults}`;
const url: () => string = (): string => {
const parts: string[] = [
`https://${booruConfig.endpoint}/${booruConfig.functions.search}`,
];
if (isE621) {
parts.push("?");
} else {
parts.push("&");
}
const queryParams: string = [tagsString(), pageString(), resultsString]
.filter(Boolean)
.join("&");
parts.push(queryParams);
return parts.join("");
};
const cacheKey: string = `nsfw:${booru}:${formattedTags}:${formattedExcludeTags}:${safePage}:${safeResults}`;
if (!force) {
const cacheData: unknown = await redis
.getInstance()
.get("JSON", cacheKey);
if (cacheData) {
return Response.json(
{
success: true,
code: 200,
cache: true,
posts: (cacheData as { posts: BooruPost[] }).posts,
},
{
status: 200,
},
);
}
}
try {
const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
: undefined;
const response: Response = await fetch(url(), {
headers,
});
if (!response.ok) {
return Response.json(
{
success: false,
code: response.status || 500,
error:
response.statusText ||
`Could not reach ${booruConfig.name}`,
},
{
status: response.status || 500,
},
);
}
const data: unknown = await response.json();
if (!data) {
return Response.json(
{
success: false,
code: 500,
error: `No data returned from ${booruConfig.name}`,
},
{
status: 500,
},
);
}
const parsedData: Data = data as Data;
let posts: BooruPost[] = [];
if (parsedData.post) {
posts = [parsedData.post];
} else if (parsedData.posts) {
posts = parsedData.posts;
} else {
posts = Array.isArray(data) ? (data as BooruPost[]) : [];
}
if (posts.length === 0) {
return Response.json(
{
success: false,
code: 404,
error: "No posts found",
},
{
status: 404,
},
);
}
const expectedData: { posts: BooruPost[] } | null = postExpectedFormat(
booruConfig,
posts,
);
if (!expectedData) {
return Response.json(
{
success: false,
code: 500,
error: "Unexpected data format",
},
{
status: 500,
},
);
}
await redis.getInstance().set("JSON", cacheKey, expectedData, 60 * 30); // 30 minutes
return Response.json(
{
success: true,
code: 200,
cache: false,
posts: expectedData.posts,
},
{
status: 200,
},
);
} catch (error) {
return Response.json(
{
success: false,
code: 500,
error: (error as Error).message || "Unknown error",
},
{
status: 500,
},
);
}
}
export { handler, routeDef };

View file

@ -149,6 +149,13 @@ class ServerHandler {
);
}
logger.info([
`[${request.method}]`,
request.url,
`${response.status}`,
server.requestIP(request)?.address || "unknown",
]);
return response;
}
}

View file

@ -4,32 +4,23 @@ type Data = {
[key: string]: unknown;
};
interface Rule34Post {
preview_url: string;
sample_url: string;
file_url: string;
interface DefaultPost {
directory: number;
hash: string;
width: number;
height: number;
id: number;
image: string;
change: number;
owner: string;
parent_id: number;
rating: string;
sample: boolean;
sample_height: number;
sample_width: number;
score: number;
tags: string;
source: string;
status: string;
has_notes: boolean;
comment_count: number;
}
type BooruPost = Rule34Post & {
post_url: string;
file_url: string;
type E621Post = {
id: number;
file: {
url: string;
};
tags: string;
};
type BooruPost = {
file_url?: string | null;
post_url?: string;
} & (DefaultPost | e621Post);

2
types/config.d.ts vendored
View file

@ -24,6 +24,7 @@ type IBooruConfigMap = {
aliases: string[];
endpoint: string;
functions: IBooruDefaults;
autocomplete?: string;
auth?: Record<string, string>;
};
};
@ -34,5 +35,6 @@ type IBooruConfig = {
aliases: string[];
endpoint: string;
functions: IBooruDefaults;
autocomplete?: string;
auth?: Record<string, string>;
};