Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

18 changed files with 340 additions and 368 deletions

View file

@ -1,24 +0,0 @@
name: Code quality checks
on:
push:
pull_request:
jobs:
biome:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Bun
run: |
curl -fsSL https://bun.sh/install | bash
export BUN_INSTALL="$HOME/.bun"
echo "$BUN_INSTALL/bin" >> $GITHUB_PATH
- name: Install Dependencies
run: bun install
- name: Run Biome with verbose output
run: bunx biome ci . --verbose

View file

@ -1,4 +1,5 @@
{ {
"cSpell.words": ["Booru"], "cSpell.words": [
"explorer.excludeGitIgnore": false "Booru"
]
} }

View file

@ -48,20 +48,21 @@
--- ---
> **Note** > **Note**
> To use the **e621 Booru route**, include the following **headers** in your request: > To use the **e621 API**, you must update the following environment variables in your `.env` file:
> >
> ```http > ```env
> e621UserAgent: YourApplication/1.0 (by username on e621) > E621_USER_AGENT=YourApplication/1.0 (by username on e621)
> e621Username: your-username > E621_USERNAME=your-username
> e621ApiKey: your-apikey > E621_API_KEY=your-apikey
> ``` > ```
> Replace `your-username` and `your-apikey` with your e621 account credentials. Update the `e621UserAgent` string to include your application name, version, and a contact method (e.g., your e621 username) to comply with e621's API guidelines. > Replace `your-username` and `your-apikey` with your e621 account credentials. Update the `User-Agent` string to include your application name, version, and a contact method (e.g., your e621 username) to comply with e621's API guidelines.
> >
> >
> To use the **Gelbooru Booru route**, include these **headers** in your request: > To use the **Gelbooru API**, you must also update the following:
> >
> ```http > ```env
> gelbooruApiKey: your-apikey > GELBOORU_API_KEY=your-apikey
> gelbooruUserId: your-user-id > GELBOORU_USER_ID=your-user-id
> ``` > ```
> You can find these credentials in your [Gelbooru account settings](https://gelbooru.com/index.php?page=account&s=options). These are required for authenticated API requests and higher rate limits. > You can find these credentials in your [Gelbooru account settings](https://gelbooru.com/index.php?page=account&s=options).
> These are required for authenticated API requests and higher rate limits.

View file

@ -1,35 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": true,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineEnding": "lf"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"indentStyle": "tab",
"lineEnding": "lf",
"jsxQuoteStyle": "double",
"semicolons": "always"
}
}
}

View file

@ -1,5 +1,7 @@
// cSpell:disable // cSpell:disable
import { gelBooruAUTH, getE621Auth } from "@config/environment";
const booruDefaults: IBooruDefaults = { const booruDefaults: IBooruDefaults = {
search: "index.php?page=dapi&s=post&q=index&json=1", search: "index.php?page=dapi&s=post&q=index&json=1",
random: "s", random: "s",
@ -66,6 +68,7 @@ export const booruConfig: IBooruConfigMap = {
random: "defaultRandom", random: "defaultRandom",
id: ["posts/", ".json"], id: ["posts/", ".json"],
}, },
auth: getE621Auth(),
}, },
"gelbooru.com": { "gelbooru.com": {
enabled: true, enabled: true,
@ -74,5 +77,6 @@ export const booruConfig: IBooruConfigMap = {
endpoint: "gelbooru.com", endpoint: "gelbooru.com",
autocomplete: "gelbooru.com/index.php?page=autocomplete&term=", autocomplete: "gelbooru.com/index.php?page=autocomplete&term=",
functions: booruDefaults, functions: booruDefaults,
auth: gelBooruAUTH(),
}, },
}; };

View file

@ -1,6 +1,47 @@
import { logger } from "@/helpers/logger";
export const environment: Environment = { export const environment: Environment = {
port: Number.parseInt(process.env.PORT || "6600", 10), port: parseInt(process.env.PORT || "6600", 10),
host: process.env.HOST || "0.0.0.0", host: process.env.HOST || "0.0.0.0",
development: development:
process.env.NODE_ENV === "development" || process.argv.includes("--dev"), process.env.NODE_ENV === "development" ||
process.argv.includes("--dev"),
}; };
if (
!process.env.E621_USER_AGENT ||
!process.env.E621_USERNAME ||
!process.env.E621_API_KEY
) {
logger.error("Missing e621 credentials in .env file");
} else {
if (
process.env.E621_USERNAME === "username" ||
process.env.E621_API_KEY === "apikey"
) {
logger.error("Please update your e621 credentials in the .env file");
}
}
export function getE621Auth(): Record<string, string> {
const e621UserAgent: string | undefined = process.env.E621_USER_AGENT;
const e621Username: string | undefined = process.env.E621_USERNAME;
const e621ApiKey: string | undefined = process.env.E621_API_KEY;
return {
"User-Agent": e621UserAgent || "",
Authorization:
"Basic " + btoa(`${e621Username || ""}:${e621ApiKey || ""}`),
};
}
if (!process.env.GELBOORU_API_KEY || !process.env.GELBOORU_USER_ID) {
logger.error("Missing Gelbooru credentials in .env file");
}
export function gelBooruAUTH(): Record<string, string> {
return {
apiKey: process.env.GELBOORU_API_KEY || "",
userId: process.env.GELBOORU_USER_ID || "",
};
}

132
eslint.config.js Normal file
View file

@ -0,0 +1,132 @@
import pluginJs from "@eslint/js";
import tseslintPlugin from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import prettier from "eslint-plugin-prettier";
import promisePlugin from "eslint-plugin-promise";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unicorn from "eslint-plugin-unicorn";
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
{
files: ["**/*.{js,mjs,cjs}"],
languageOptions: {
globals: globals.node,
},
...pluginJs.configs.recommended,
plugins: {
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
promise: promisePlugin,
prettier: prettier,
unicorn: unicorn,
},
rules: {
"eol-last": ["error", "always"],
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"prettier/prettier": [
"error",
{
useTabs: true,
tabWidth: 4,
},
],
indent: ["error", "tab", { SwitchCase: 1 }],
"unicorn/filename-case": [
"error",
{
case: "camelCase",
},
],
},
},
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tsParser,
globals: globals.node,
},
plugins: {
"@typescript-eslint": tseslintPlugin,
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
promise: promisePlugin,
prettier: prettier,
unicorn: unicorn,
},
rules: {
...tseslintPlugin.configs.recommended.rules,
quotes: ["error", "double"],
"eol-last": ["error", "always"],
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"prettier/prettier": [
"error",
{
useTabs: true,
tabWidth: 4,
},
],
indent: ["error", "tab", { SwitchCase: 1 }],
"unicorn/filename-case": [
"error",
{
case: "camelCase",
},
],
"@typescript-eslint/explicit-function-return-type": ["error"],
"@typescript-eslint/explicit-module-boundary-types": ["error"],
"@typescript-eslint/typedef": [
"error",
{
arrowParameter: true,
variableDeclaration: true,
propertyDeclaration: true,
memberVariableDeclaration: true,
parameter: true,
},
],
},
},
];

View file

@ -2,20 +2,28 @@
"name": "booru-api", "name": "booru-api",
"module": "src/index.ts", "module": "src/index.ts",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@eslint/js": "^9.17.0",
"@eslint/js": "^9.24.0", "@types/bun": "^1.1.14",
"@types/bun": "^1.2.9", "@typescript-eslint/eslint-plugin": "^8.18.1",
"globals": "^16.0.0" "@typescript-eslint/parser": "^8.18.1",
"eslint": "^9.17.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^56.0.1",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^15.14.0",
"prettier": "^3.4.2"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.3" "typescript": "^5.7.2"
}, },
"scripts": { "scripts": {
"start": "bun run src/index.ts", "start": "bun run src/index.ts",
"dev": "bun run --hot src/index.ts --dev", "dev": "bun run --watch src/index.ts --dev",
"lint": "bunx biome check", "lint": "eslint",
"lint:fix": "bunx biome check --fix", "lint:fix": "bun lint --fix",
"cleanup": "rm -rf logs node_modules bun.lock" "cleanup": "rm -rf logs node_modules bun.lockb"
}, },
"type": "module" "type": "module"
} }

View file

@ -4,15 +4,15 @@ import { booruConfig } from "@config/booru";
export function timestampToReadable(timestamp?: number): string { export function timestampToReadable(timestamp?: number): string {
const date: Date = const date: Date =
timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date(); timestamp && !isNaN(timestamp) ? new Date(timestamp) : new Date();
if (Number.isNaN(date.getTime())) return "Invalid Date"; if (isNaN(date.getTime())) return "Invalid Date";
return date.toISOString().replace("T", " ").replace("Z", ""); return date.toISOString().replace("T", " ").replace("Z", "");
} }
export function tagsToExpectedFormat( export function tagsToExpectedFormat(
tags: string[] | string | Record<string, string[]>, tags: string[] | string | Record<string, string[]>,
minus = false, minus: boolean = false,
onlyMinus = false, onlyMinus: boolean = false,
): string { ): string {
const delimiter: string = minus ? (onlyMinus ? "-" : "+-") : "+"; const delimiter: string = minus ? (onlyMinus ? "-" : "+-") : "+";
@ -76,7 +76,7 @@ export function determineBooru(
export function postExpectedFormat( export function postExpectedFormat(
booru: IBooruConfig, booru: IBooruConfig,
posts: BooruPost[] | BooruPost, posts: BooruPost[] | BooruPost,
tag_format = "string", tag_format: string = "string",
): { posts: BooruPost[] } | null { ): { posts: BooruPost[] } | null {
if (!posts) return null; if (!posts) return null;
@ -90,64 +90,40 @@ export function postExpectedFormat(
...post, ...post,
file_url: post.file.url ?? null, file_url: post.file.url ?? null,
post_url: post_url:
post.post_url ?? `https://${booru.endpoint}/posts/${post.id}`, post.post_url ??
`https://${booru.endpoint}/posts/${post.id}`,
tags: tags:
tag_format === "unformatted" tag_format === "unformatted"
? post.tags ? post.tags
: Object.values(post.tags || {}) : Object.values(post.tags || {})
.flat() .flat()
.join(" "), .join(" "),
}; };
}), }),
}; };
} }
const fixedDomain: string = booru.endpoint.replace(/^api\./, ""); const fixedDomain: string = booru.endpoint.replace(/^api\./, "");
const formattedPosts: BooruPost[] = normalizedPosts.map((post: BooruPost) => { const formattedPosts: BooruPost[] = normalizedPosts.map(
const postUrl: string = (post: BooruPost) => {
post.post_url ?? const postUrl: string =
`https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`; post.post_url ??
const imageExtension: string = `https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`;
post.image?.substring(post.image.lastIndexOf(".") + 1) ?? ""; const imageExtension: string =
const fileUrl: string | null = post.image?.substring(post.image.lastIndexOf(".") + 1) ?? "";
post.file_url ?? const fileUrl: string | null =
(post.directory && post.hash && imageExtension post.file_url ??
? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}` (post.directory && post.hash && imageExtension
: null); ? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`
: null);
return { return {
...post, ...post,
file_url: fileUrl, file_url: fileUrl,
post_url: postUrl, post_url: postUrl,
}; };
}); },
);
return { posts: formattedPosts }; return { posts: formattedPosts };
} }
export function getE621Auth(headers: Headers): Record<string, string> | null {
const userAgent = headers.get("e621UserAgent") ?? "";
const username = headers.get("e621Username");
const apiKey = headers.get("e621ApiKey");
if (!userAgent || !username || !apiKey) return null;
return {
"User-Agent": userAgent,
Authorization: `Basic ${btoa(`${username}:${apiKey}`)}`,
};
}
export function getGelBooruAuth(
headers: Headers,
): Record<string, string> | null {
const apiKey = headers.get("gelbooruApiKey");
const userId = headers.get("gelbooruUserId");
if (!apiKey || !userId) return null;
return {
apiKey,
userId,
};
}

View file

@ -1,14 +1,14 @@
import type { Stats } from "node:fs"; import { environment } from "@config/environment";
import type { Stats } from "fs";
import { import {
type WriteStream,
createWriteStream, createWriteStream,
existsSync, existsSync,
mkdirSync, mkdirSync,
statSync, statSync,
} from "node:fs"; WriteStream,
import { EOL } from "node:os"; } from "fs";
import { basename, join } from "node:path"; import { EOL } from "os";
import { environment } from "@config/environment"; import { basename, join } from "path";
import { timestampToReadable } from "./char"; import { timestampToReadable } from "./char";
@ -38,7 +38,7 @@ class Logger {
mkdirSync(logDir, { recursive: true }); mkdirSync(logDir, { recursive: true });
} }
let addSeparator = false; let addSeparator: boolean = false;
if (existsSync(logFile)) { if (existsSync(logFile)) {
const fileStats: Stats = statSync(logFile); const fileStats: Stats = statSync(logFile);
@ -67,9 +67,9 @@ class Logger {
private extractFileName(stack: string): string { private extractFileName(stack: string): string {
const stackLines: string[] = stack.split("\n"); const stackLines: string[] = stack.split("\n");
let callerFile = ""; let callerFile: string = "";
for (let i = 2; i < stackLines.length; i++) { for (let i: number = 2; i < stackLines.length; i++) {
const line: string = stackLines[i].trim(); const line: string = stackLines[i].trim();
if (line && !line.includes("Logger.") && line.includes("(")) { if (line && !line.includes("Logger.") && line.includes("(")) {
callerFile = line.split("(")[1]?.split(")")[0] || ""; callerFile = line.split("(")[1]?.split(")")[0] || "";
@ -92,7 +92,7 @@ class Logger {
return { filename, timestamp: readableTimestamp }; return { filename, timestamp: readableTimestamp };
} }
public info(message: string | string[], breakLine = false): void { public info(message: string | string[], breakLine: boolean = false): void {
const stack: string = new Error().stack || ""; const stack: string = new Error().stack || "";
const { filename, timestamp } = this.getCallerInfo(stack); const { filename, timestamp } = this.getCallerInfo(stack);
@ -111,7 +111,7 @@ class Logger {
this.writeConsoleMessageColored(logMessageParts, breakLine); this.writeConsoleMessageColored(logMessageParts, breakLine);
} }
public warn(message: string | string[], breakLine = false): void { public warn(message: string | string[], breakLine: boolean = false): void {
const stack: string = new Error().stack || ""; const stack: string = new Error().stack || "";
const { filename, timestamp } = this.getCallerInfo(stack); const { filename, timestamp } = this.getCallerInfo(stack);
@ -132,7 +132,7 @@ class Logger {
public error( public error(
message: string | string[] | Error | Error[], message: string | string[] | Error | Error[],
breakLine = false, breakLine: boolean = false,
): void { ): void {
const stack: string = new Error().stack || ""; const stack: string = new Error().stack || "";
const { filename, timestamp } = this.getCallerInfo(stack); const { filename, timestamp } = this.getCallerInfo(stack);
@ -159,7 +159,7 @@ class Logger {
private writeConsoleMessageColored( private writeConsoleMessageColored(
logMessageParts: ILogMessageParts, logMessageParts: ILogMessageParts,
breakLine = false, breakLine: boolean = false,
): void { ): void {
const logMessage: string = Object.keys(logMessageParts) const logMessage: string = Object.keys(logMessageParts)
.map((key: string) => { .map((key: string) => {

View file

@ -3,7 +3,11 @@ import { logger } from "@helpers/logger";
import { serverHandler } from "./server"; import { serverHandler } from "./server";
async function main(): Promise<void> { async function main(): Promise<void> {
serverHandler.initialize(); try {
serverHandler.initialize();
} catch (error) {
throw error;
}
} }
main().catch((error: Error) => { main().catch((error: Error) => {

View file

@ -1,7 +1,7 @@
import { determineBooru, getE621Auth, getGelBooruAuth } from "@helpers/char"; import { determineBooru } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { logger } from "@helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "GET", method: "GET",
@ -10,7 +10,7 @@ const routeDef: RouteDef = {
}; };
async function handler( async function handler(
request: Request, _request: Request,
_server: BunServer, _server: BunServer,
_requestBody: unknown, _requestBody: unknown,
query: Query, query: Query,
@ -47,22 +47,6 @@ async function handler(
const booruConfig: IBooruConfig | null = determineBooru(booru); const booruConfig: IBooruConfig | null = determineBooru(booru);
const isE621: boolean = booruConfig?.name === "e621.net"; const isE621: boolean = booruConfig?.name === "e621.net";
const isGelbooru: boolean = booruConfig?.name === "gelbooru.com"; const isGelbooru: boolean = booruConfig?.name === "gelbooru.com";
const gelbooruAuth: Record<string, string> | null = getGelBooruAuth(
request.headers,
);
if (isGelbooru && !gelbooruAuth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing Gelbooru authentication headers",
},
{
status: 401,
},
);
}
if (!booruConfig) { if (!booruConfig) {
return Response.json( return Response.json(
@ -122,37 +106,15 @@ async function handler(
); );
} }
let url = `https://${booruConfig.autocomplete}${editedTag}`; let url: string = `https://${booruConfig.autocomplete}${editedTag}`;
if (isGelbooru && gelbooruAuth) { if (isGelbooru) {
url += `?api_key=${gelbooruAuth.api_key}&user_id=${gelbooruAuth.user_id}`; url += `&api_key=${booruConfig.auth?.api_key}&user_id=${booruConfig.auth?.user_id}`;
} }
try { try {
let headers: Record<string, string> | undefined; const headers: IBooruConfig["auth"] | undefined =
booruConfig.auth && isE621 ? booruConfig.auth : undefined;
if (isE621) {
const e621Auth: Record<string, string> | null = getE621Auth(
request.headers,
);
if (!e621Auth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing E621 authentication headers",
},
{
status: 401,
},
);
}
headers = {
...e621Auth,
};
}
const response: Response = await fetch(url, { const response: Response = await fetch(url, {
headers, headers,

View file

@ -1,12 +1,7 @@
import { import { determineBooru, postExpectedFormat } from "@helpers/char";
determineBooru,
getE621Auth,
getGelBooruAuth,
postExpectedFormat,
} from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { logger } from "@helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "GET", method: "GET",
@ -15,7 +10,7 @@ const routeDef: RouteDef = {
}; };
async function handler( async function handler(
request: Request, _request: Request,
_server: BunServer, _server: BunServer,
_requestBody: unknown, _requestBody: unknown,
query: Query, query: Query,
@ -42,22 +37,6 @@ async function handler(
const booruConfig: IBooruConfig | null = determineBooru(booru); const booruConfig: IBooruConfig | null = determineBooru(booru);
const isE621: boolean = booruConfig?.name === "e621.net"; const isE621: boolean = booruConfig?.name === "e621.net";
const isGelbooru: boolean = booruConfig?.name === "gelbooru.com"; const isGelbooru: boolean = booruConfig?.name === "gelbooru.com";
const gelbooruAuth: Record<string, string> | null = getGelBooruAuth(
request.headers,
);
if (isGelbooru && !gelbooruAuth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing Gelbooru authentication headers",
},
{
status: 401,
},
);
}
if (!booruConfig) { if (!booruConfig) {
return Response.json( return Response.json(
@ -86,10 +65,10 @@ async function handler(
} }
const funcString: string | [string, string] = booruConfig.functions.id; const funcString: string | [string, string] = booruConfig.functions.id;
let url = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`; let url: string = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`;
if (isGelbooru && gelbooruAuth) { if (isGelbooru) {
url += `?api_key=${gelbooruAuth.api_key}&user_id=${gelbooruAuth.user_id}`; url += `&api_key=${booruConfig.auth?.api_key}&user_id=${booruConfig.auth?.user_id}`;
} }
if (Array.isArray(funcString)) { if (Array.isArray(funcString)) {
@ -99,30 +78,8 @@ async function handler(
} }
try { try {
let headers: Record<string, string> | undefined; const headers: IBooruConfig["auth"] | undefined =
booruConfig.auth && isE621 ? booruConfig.auth : undefined;
if (isE621) {
const e621Auth: Record<string, string> | null = getE621Auth(
request.headers,
);
if (!e621Auth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing E621 authentication headers",
},
{
status: 401,
},
);
}
headers = {
...e621Auth,
};
}
const response: Response = await fetch(url, { const response: Response = await fetch(url, {
headers, headers,

View file

@ -1,7 +1,5 @@
import { import {
determineBooru, determineBooru,
getE621Auth,
getGelBooruAuth,
minPosts, minPosts,
postExpectedFormat, postExpectedFormat,
shufflePosts, shufflePosts,
@ -9,7 +7,7 @@ import {
} from "@helpers/char"; } from "@helpers/char";
import { fetch } from "bun"; import { fetch } from "bun";
import { logger } from "@helpers/logger"; import { logger } from "@/helpers/logger";
const routeDef: RouteDef = { const routeDef: RouteDef = {
method: "POST", method: "POST",
@ -19,7 +17,7 @@ const routeDef: RouteDef = {
}; };
async function handler( async function handler(
request: Request, _request: Request,
_server: BunServer, _server: BunServer,
requestBody: unknown, requestBody: unknown,
query: Query, query: Query,
@ -123,22 +121,6 @@ async function handler(
const isE621: boolean = booruConfig.name === "e621.net"; const isE621: boolean = booruConfig.name === "e621.net";
const isGelbooru: boolean = booruConfig.name === "gelbooru.com"; const isGelbooru: boolean = booruConfig.name === "gelbooru.com";
const gelbooruAuth: Record<string, string> | null = getGelBooruAuth(
request.headers,
);
if (isGelbooru && !gelbooruAuth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing Gelbooru authentication headers",
},
{
status: 401,
},
);
}
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : ""; const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags const formattedExcludeTags: string = excludeTags
@ -148,11 +130,9 @@ async function handler(
const tagsString: () => string = (): string => { const tagsString: () => string = (): string => {
if (formattedTags && formattedExcludeTags) { if (formattedTags && formattedExcludeTags) {
return `tags=${formattedTags}+-${formattedExcludeTags}`; return `tags=${formattedTags}+-${formattedExcludeTags}`;
} } else if (formattedTags) {
if (formattedTags) {
return `tags=${formattedTags}`; return `tags=${formattedTags}`;
} } else if (formattedExcludeTags) {
if (formattedExcludeTags) {
return `tags=-${formattedExcludeTags}`; return `tags=-${formattedExcludeTags}`;
} }
@ -184,12 +164,12 @@ async function handler(
parts.push("&"); parts.push("&");
} }
if (isGelbooru && gelbooruAuth) { if (isGelbooru) {
parts.push("api_key"); parts.push("api_key");
parts.push(gelbooruAuth.apiKey); parts.push(booruConfig.auth?.apiKey || "");
parts.push("&"); parts.push("&");
parts.push("user_id"); parts.push("user_id");
parts.push(gelbooruAuth.userId); parts.push(booruConfig.auth?.userId || "");
parts.push("&"); parts.push("&");
} }
@ -198,6 +178,7 @@ async function handler(
.join("&"); .join("&");
parts.push(queryParams); parts.push(queryParams);
console.log("URL", parts.join(""));
return parts.join(""); return parts.join("");
}; };
@ -205,37 +186,15 @@ async function handler(
maxPage: 12, maxPage: 12,
maxTries: 6, maxTries: 6,
}; };
const state: { tries: number; page: number } = { tries: 0, page: 16 }; let state: { tries: number; page: number } = { tries: 0, page: 16 };
while (state.tries < config.maxTries) { while (state.tries < config.maxTries) {
const url: string = getUrl(pageString(state.page), resultsString); const url: string = getUrl(pageString(state.page), resultsString);
try { try {
let headers: Record<string, string> | undefined; const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
if (isE621) { : undefined;
const e621Auth: Record<string, string> | null = getE621Auth(
request.headers,
);
if (!e621Auth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing E621 authentication headers",
},
{
status: 401,
},
);
}
headers = {
...e621Auth,
};
}
const response: Response = await fetch(url, { const response: Response = await fetch(url, {
headers, headers,
}); });
@ -245,7 +204,9 @@ async function handler(
{ {
success: false, success: false,
code: response.status || 500, code: response.status || 500,
error: response.statusText || `Could not reach ${booruConfig.name}`, error:
response.statusText ||
`Could not reach ${booruConfig.name}`,
}, },
{ {
status: response.status || 500, status: response.status || 500,
@ -285,11 +246,8 @@ async function handler(
if (posts.length === 0) continue; if (posts.length === 0) continue;
const expectedData: { posts: BooruPost[] } | null = postExpectedFormat( let expectedData: { posts: BooruPost[] } | null =
booruConfig, postExpectedFormat(booruConfig, posts, tag_format);
posts,
tag_format,
);
if (!expectedData) continue; if (!expectedData) continue;
@ -307,6 +265,7 @@ async function handler(
}, },
); );
} catch { } catch {
continue;
} finally { } finally {
state.tries++; state.tries++;

View file

@ -1,7 +1,5 @@
import { import {
determineBooru, determineBooru,
getE621Auth,
getGelBooruAuth,
postExpectedFormat, postExpectedFormat,
tagsToExpectedFormat, tagsToExpectedFormat,
} from "@helpers/char"; } from "@helpers/char";
@ -15,7 +13,7 @@ const routeDef: RouteDef = {
}; };
async function handler( async function handler(
request: Request, _request: Request,
_server: BunServer, _server: BunServer,
requestBody: unknown, requestBody: unknown,
query: Query, query: Query,
@ -121,22 +119,6 @@ async function handler(
const isE621: boolean = booruConfig.name === "e621.net"; const isE621: boolean = booruConfig.name === "e621.net";
const isGelbooru: boolean = booruConfig.name === "gelbooru.com"; const isGelbooru: boolean = booruConfig.name === "gelbooru.com";
const gelbooruAuth: Record<string, string> | null = getGelBooruAuth(
request.headers,
);
if (isGelbooru && !gelbooruAuth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing Gelbooru authentication headers",
},
{
status: 401,
},
);
}
const formattedTags: string = tags ? tagsToExpectedFormat(tags) : ""; const formattedTags: string = tags ? tagsToExpectedFormat(tags) : "";
const formattedExcludeTags: string = excludeTags const formattedExcludeTags: string = excludeTags
@ -151,11 +133,9 @@ async function handler(
const tagsString: () => string = (): string => { const tagsString: () => string = (): string => {
if (formattedTags && formattedExcludeTags) { if (formattedTags && formattedExcludeTags) {
return `tags=${formattedTags}+-${formattedExcludeTags}`; return `tags=${formattedTags}+-${formattedExcludeTags}`;
} } else if (formattedTags) {
if (formattedTags) {
return `tags=${formattedTags}`; return `tags=${formattedTags}`;
} } else if (formattedExcludeTags) {
if (formattedExcludeTags) {
return `tags=-${formattedExcludeTags}`; return `tags=-${formattedExcludeTags}`;
} }
@ -182,12 +162,12 @@ async function handler(
parts.push("&"); parts.push("&");
} }
if (isGelbooru && gelbooruAuth) { if (isGelbooru) {
parts.push("api_key"); parts.push("api_key");
parts.push(gelbooruAuth.apiKey); parts.push(booruConfig.auth?.apiKey || "");
parts.push("&"); parts.push("&");
parts.push("user_id"); parts.push("user_id");
parts.push(gelbooruAuth.userId); parts.push(booruConfig.auth?.userId || "");
parts.push("&"); parts.push("&");
} }
@ -200,31 +180,9 @@ async function handler(
}; };
try { try {
let headers: Record<string, string> | undefined; const headers: IBooruConfig["auth"] | undefined = booruConfig.auth
? booruConfig.auth
if (isE621) { : undefined;
const e621Auth: Record<string, string> | null = getE621Auth(
request.headers,
);
if (!e621Auth) {
return Response.json(
{
success: false,
code: 401,
error: "Missing E621 authentication headers",
},
{
status: 401,
},
);
}
headers = {
...e621Auth,
};
}
const response: Response = await fetch(url(), { const response: Response = await fetch(url(), {
headers, headers,
}); });
@ -234,7 +192,9 @@ async function handler(
{ {
success: false, success: false,
code: response.status || 500, code: response.status || 500,
error: response.statusText || `Could not reach ${booruConfig.name}`, error:
response.statusText ||
`Could not reach ${booruConfig.name}`,
}, },
{ {
status: response.status || 500, status: response.status || 500,

View file

@ -58,7 +58,8 @@ class ServerHandler {
try { try {
const routeModule: RouteModule = await import(filePath); const routeModule: RouteModule = await import(filePath);
const contentType: string | null = request.headers.get("Content-Type"); const contentType: string | null =
request.headers.get("Content-Type");
const actualContentType: string | null = contentType const actualContentType: string | null = contentType
? contentType.split(";")[0].trim() ? contentType.split(";")[0].trim()
: null; : null;
@ -118,7 +119,10 @@ class ServerHandler {
params, params,
); );
response.headers.set("Content-Type", routeModule.routeDef.returns); response.headers.set(
"Content-Type",
routeModule.routeDef.returns,
);
} }
} }
} catch (error: unknown) { } catch (error: unknown) {

View file

@ -2,15 +2,31 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": [
"@config/*": ["config/*"], "src/*"
"@types/*": ["types/*"], ],
"@helpers/*": ["src/helpers/*"], "@config/*": [
"@database/*": ["src/database/*"] "config/*"
],
"@types/*": [
"types/*"
],
"@helpers/*": [
"src/helpers/*"
],
"@database/*": [
"src/database/*"
],
}, },
"typeRoots": ["./src/types", "./node_modules/@types"], "typeRoots": [
"./src/types",
"./node_modules/@types"
],
// Enable latest features // Enable latest features
"lib": ["ESNext", "DOM"], "lib": [
"ESNext",
"DOM"
],
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleDetection": "force", "moduleDetection": "force",
@ -28,7 +44,11 @@
// Some stricter flags (disabled by default) // Some stricter flags (disabled by default)
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false,
}, },
"include": ["src", "types", "config"] "include": [
"src",
"types",
"config"
],
} }

2
types/config.d.ts vendored
View file

@ -18,6 +18,7 @@ type IBooruConfigMap = {
endpoint: string; endpoint: string;
functions: IBooruDefaults; functions: IBooruDefaults;
autocomplete?: string; autocomplete?: string;
auth?: Record<string, string>;
}; };
}; };
@ -28,4 +29,5 @@ type IBooruConfig = {
endpoint: string; endpoint: string;
functions: IBooruDefaults; functions: IBooruDefaults;
autocomplete?: string; autocomplete?: string;
auth?: Record<string, string>;
}; };