refactor: improve code structure and add better logging
All checks were successful
Code quality checks / biome (push) Successful in 10s
All checks were successful
Code quality checks / biome (push) Successful in 10s
- Replace custom logger with @atums/echo library - Restructure imports using # path aliases - Add request performance tracking and better request logging - Improve type definitions and error handling - Add custom file serving capability - Update biome configuration with stricter linting rules - Add comprehensive API documentation (DOCS.md)
This commit is contained in:
parent
82c9d72619
commit
5f0bdb885b
24 changed files with 666 additions and 392 deletions
246
DOCS.md
Normal file
246
DOCS.md
Normal file
|
@ -0,0 +1,246 @@
|
|||
# Booru API Documentation
|
||||
|
||||
A unified API for accessing multiple booru image boards.
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:6600
|
||||
```
|
||||
|
||||
## Supported Boorus
|
||||
|
||||
| Booru | Aliases | Status |
|
||||
|-------|---------|--------|
|
||||
| rule34.xxx | `rule34`, `r34`, `rule34xxx` | ✅ Enabled |
|
||||
| safebooru.org | `safebooru`, `sb`, `s34` | ✅ Enabled |
|
||||
| tbib.org | `tbib`, `tb`, `tbiborg` | ✅ Enabled |
|
||||
| hypnohub.net | `hypnohub`, `hh`, `hypnohubnet` | ✅ Enabled |
|
||||
| xbooru.com | `xbooru`, `xb`, `xboorucom` | ✅ Enabled |
|
||||
| e621.net | `e621`, `e6`, `e621net` | ✅ Enabled |
|
||||
| gelbooru.com | `gelbooru`, `gb`, `gelboorucom` | ✅ Enabled |
|
||||
| realbooru.com | `realbooru`, `rb`, `real34`, `realb` | ❌ Disabled |
|
||||
|
||||
## Authentication
|
||||
|
||||
### E621
|
||||
Required headers for e621 requests:
|
||||
```http
|
||||
e621UserAgent: YourApplication/1.0 (by username on e621)
|
||||
e621Username: your-username
|
||||
e621ApiKey: your-apikey
|
||||
```
|
||||
|
||||
### Gelbooru
|
||||
Required headers for Gelbooru requests:
|
||||
```http
|
||||
gelbooruApiKey: your-apikey
|
||||
gelbooruUserId: your-user-id
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Search Posts
|
||||
Search for posts with specific tags.
|
||||
|
||||
```http
|
||||
POST /{booru}/search
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"tags": ["tag1", "tag2"],
|
||||
"excludeTags": ["unwanted_tag"],
|
||||
"page": 0,
|
||||
"results": 10,
|
||||
"tag_format": "formatted"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `tags` (string|array): Tags to search for
|
||||
- `excludeTags` (string|array): Tags to exclude
|
||||
- `page` (number): Page number (default: 0)
|
||||
- `results` (number): Number of results (default: 5)
|
||||
- `tag_format` (string): Format of tags in response (`"formatted"` or `"unformatted"`)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:6600/rule34/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tags": ["cat", "cute"],
|
||||
"results": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### 2. Random Posts
|
||||
Get random posts with optional tag filtering.
|
||||
|
||||
```http
|
||||
POST /{booru}/random
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"tags": ["tag1", "tag2"],
|
||||
"excludeTags": ["unwanted_tag"],
|
||||
"results": 5,
|
||||
"tag_format": "formatted"
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:6600/safebooru/random" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tags": ["anime"],
|
||||
"results": 3
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Get Post by ID
|
||||
Retrieve a specific post by its ID.
|
||||
|
||||
```http
|
||||
GET /{booru}/id/{id}?tag_format=formatted
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id` (string): Post ID
|
||||
- `tag_format` (query): Format of tags in response
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl "http://localhost:6600/rule34/id/123456?tag_format=formatted"
|
||||
```
|
||||
|
||||
### 4. Tag Autocomplete
|
||||
Get tag suggestions for autocomplete.
|
||||
|
||||
```http
|
||||
GET /{booru}/autocomplete/{tag}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `tag` (string): Partial tag name (minimum 3 characters for e621)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl "http://localhost:6600/safebooru/autocomplete/anim"
|
||||
```
|
||||
|
||||
### 5. API Info
|
||||
Get basic API information.
|
||||
|
||||
```http
|
||||
GET /
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl "http://localhost:6600/"
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Success Response
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"posts": [
|
||||
{
|
||||
"id": 123456,
|
||||
"file_url": "https://example.com/image.jpg",
|
||||
"post_url": "https://example.com/post/123456",
|
||||
"tags": "tag1 tag2 tag3",
|
||||
"directory": 1234,
|
||||
"hash": "abcdef123456"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 400,
|
||||
"error": "Missing booru parameter"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 400 | Bad Request - Missing or invalid parameters |
|
||||
| 401 | Unauthorized - Missing authentication headers |
|
||||
| 403 | Forbidden - Booru is disabled |
|
||||
| 404 | Not Found - No results found or booru not found |
|
||||
| 405 | Method Not Allowed - Wrong HTTP method |
|
||||
| 406 | Not Acceptable - Wrong content type |
|
||||
| 500 | Internal Server Error |
|
||||
| 501 | Not Implemented - Feature not supported |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Search for anime posts on Safebooru
|
||||
```bash
|
||||
curl -X POST "http://localhost:6600/safebooru/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tags": ["anime", "girl"],
|
||||
"results": 10,
|
||||
"page": 0
|
||||
}'
|
||||
```
|
||||
|
||||
### Get random posts from Rule34
|
||||
```bash
|
||||
curl -X POST "http://localhost:6600/rule34/random" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"tags": ["pokemon"],
|
||||
"excludeTags": ["gore"],
|
||||
"results": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### Search E621 with authentication
|
||||
```bash
|
||||
curl -X POST "http://localhost:6600/e621/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "e621UserAgent: MyApp/1.0 (by myusername on e621)" \
|
||||
-H "e621Username: myusername" \
|
||||
-H "e621ApiKey: myapikey" \
|
||||
-d '{
|
||||
"tags": ["canine"],
|
||||
"results": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### Get tag suggestions
|
||||
```bash
|
||||
curl "http://localhost:6600/gelbooru/autocomplete/anim" \
|
||||
-H "gelbooruApiKey: your-api-key" \
|
||||
-H "gelbooruUserId: your-user-id"
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
This API respects the rate limits of the underlying booru services. Please be mindful of your request frequency to avoid being blocked by the source APIs.
|
||||
|
||||
## Notes
|
||||
|
||||
- All POST requests require `Content-Type: application/json` header
|
||||
- Some boorus may have different response formats
|
||||
- E621 requires authentication for all requests
|
||||
- Gelbooru requires authentication for better rate limits
|
||||
- Tag formats may vary between boorus
|
||||
- The `tag_format` parameter allows you to choose between formatted strings or raw objects
|
25
biome.json
25
biome.json
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"ignore": []
|
||||
"ignore": ["dist"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
|
@ -17,11 +17,30 @@
|
|||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"css": {
|
||||
"formatter": {
|
||||
"indentStyle": "tab",
|
||||
"lineEnding": "lf"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedImports": "error",
|
||||
"noUnusedVariables": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"noVar": "error",
|
||||
"useImportType": "error"
|
||||
}
|
||||
},
|
||||
"ignore": ["types"]
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
// cSpell:disable
|
||||
|
||||
const reqLoggerIgnores = {
|
||||
ignoredStartsWith: ["/public"],
|
||||
ignoredPaths: ["/favicon.ico"],
|
||||
};
|
||||
|
||||
import type { IBooruConfigMap, IBooruDefaults } from "#types/config";
|
||||
|
||||
const booruDefaults: IBooruDefaults = {
|
||||
search: "index.php?page=dapi&s=post&q=index&json=1",
|
||||
random: "s",
|
||||
id: "index.php?page=dapi&s=post&q=index&json=1&id=",
|
||||
};
|
||||
|
||||
export const booruConfig: IBooruConfigMap = {
|
||||
const booruConfig: IBooruConfigMap = {
|
||||
"rule34.xxx": {
|
||||
enabled: true,
|
||||
name: "rule34.xxx",
|
||||
|
@ -76,3 +83,5 @@ export const booruConfig: IBooruConfigMap = {
|
|||
functions: booruDefaults,
|
||||
},
|
||||
};
|
||||
|
||||
export { reqLoggerIgnores, booruConfig, booruDefaults };
|
|
@ -1,6 +0,0 @@
|
|||
export const environment: Environment = {
|
||||
port: Number.parseInt(process.env.PORT || "6600", 10),
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
development:
|
||||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
};
|
30
config/index.ts
Normal file
30
config/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { echo } from "@atums/echo";
|
||||
|
||||
import type { Environment } from "#types/config";
|
||||
|
||||
const environment: Environment = {
|
||||
port: Number.parseInt(process.env.PORT || "8080", 10),
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
development:
|
||||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
};
|
||||
|
||||
function verifyRequiredVariables(): void {
|
||||
const requiredVariables = ["HOST", "PORT"];
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (const key of requiredVariables) {
|
||||
const value = process.env[key];
|
||||
if (value === undefined || value.trim() === "") {
|
||||
echo.error(`Missing or empty environment variable: ${key}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export { environment, verifyRequiredVariables };
|
40
logger.json
Normal file
40
logger.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"directory": "logs",
|
||||
"level": "debug",
|
||||
"disableFile": false,
|
||||
|
||||
"rotate": true,
|
||||
"maxFiles": 3,
|
||||
"fileNameFormat": "yyyy-MM-dd",
|
||||
|
||||
"console": true,
|
||||
"consoleColor": true,
|
||||
|
||||
"dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
"timezone": "local",
|
||||
|
||||
"silent": false,
|
||||
|
||||
"pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}",
|
||||
"levelColor": {
|
||||
"debug": "blue",
|
||||
"info": "green",
|
||||
"warn": "yellow",
|
||||
"error": "red",
|
||||
"fatal": "red"
|
||||
},
|
||||
|
||||
"customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
|
||||
"customColors": {
|
||||
"GET": "green",
|
||||
"POST": "blue",
|
||||
"PUT": "yellow",
|
||||
"DELETE": "red",
|
||||
"PATCH": "cyan",
|
||||
"HEAD": "magenta",
|
||||
"OPTIONS": "white",
|
||||
"TRACE": "gray"
|
||||
},
|
||||
|
||||
"prettyPrint": true
|
||||
}
|
14
package.json
14
package.json
|
@ -2,13 +2,8 @@
|
|||
"name": "booru-api",
|
||||
"module": "src/index.ts",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@types/bun": "^1.2.9",
|
||||
"globals": "^16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
"@biomejs/biome": "latest",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
|
@ -17,5 +12,8 @@
|
|||
"lint:fix": "bunx biome check --fix",
|
||||
"cleanup": "rm -rf logs node_modules bun.lock"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@atums/echo": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
import type { Stats } from "node:fs";
|
||||
import {
|
||||
type WriteStream,
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { EOL } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { environment } from "@config/environment";
|
||||
|
||||
import { timestampToReadable } from "./char";
|
||||
|
||||
class Logger {
|
||||
private static instance: Logger;
|
||||
private static log: string = join(__dirname, "../../logs");
|
||||
|
||||
public static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger();
|
||||
}
|
||||
|
||||
return Logger.instance;
|
||||
}
|
||||
|
||||
private writeToLog(logMessage: string): void {
|
||||
if (environment.development) return;
|
||||
|
||||
const date: Date = new Date();
|
||||
const logDir: string = Logger.log;
|
||||
const logFile: string = join(
|
||||
logDir,
|
||||
`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.log`,
|
||||
);
|
||||
|
||||
if (!existsSync(logDir)) {
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
let addSeparator = false;
|
||||
|
||||
if (existsSync(logFile)) {
|
||||
const fileStats: Stats = statSync(logFile);
|
||||
if (fileStats.size > 0) {
|
||||
const lastModified: Date = new Date(fileStats.mtime);
|
||||
if (
|
||||
lastModified.getFullYear() === date.getFullYear() &&
|
||||
lastModified.getMonth() === date.getMonth() &&
|
||||
lastModified.getDate() === date.getDate() &&
|
||||
lastModified.getHours() !== date.getHours()
|
||||
) {
|
||||
addSeparator = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stream: WriteStream = createWriteStream(logFile, { flags: "a" });
|
||||
|
||||
if (addSeparator) {
|
||||
stream.write(`${EOL}${date.toISOString()}${EOL}`);
|
||||
}
|
||||
|
||||
stream.write(`${logMessage}${EOL}`);
|
||||
stream.close();
|
||||
}
|
||||
|
||||
private extractFileName(stack: string): string {
|
||||
const stackLines: string[] = stack.split("\n");
|
||||
let callerFile = "";
|
||||
|
||||
for (let i = 2; i < stackLines.length; i++) {
|
||||
const line: string = stackLines[i].trim();
|
||||
if (line && !line.includes("Logger.") && line.includes("(")) {
|
||||
callerFile = line.split("(")[1]?.split(")")[0] || "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return basename(callerFile);
|
||||
}
|
||||
|
||||
private getCallerInfo(stack: unknown): {
|
||||
filename: string;
|
||||
timestamp: string;
|
||||
} {
|
||||
const filename: string =
|
||||
typeof stack === "string" ? this.extractFileName(stack) : "unknown";
|
||||
|
||||
const readableTimestamp: string = timestampToReadable();
|
||||
|
||||
return { filename, timestamp: readableTimestamp };
|
||||
}
|
||||
|
||||
public info(message: string | string[], breakLine = false): void {
|
||||
const stack: string = new Error().stack || "";
|
||||
const { filename, timestamp } = this.getCallerInfo(stack);
|
||||
|
||||
const joinedMessage: string = Array.isArray(message)
|
||||
? message.join(" ")
|
||||
: message;
|
||||
|
||||
const logMessageParts: ILogMessageParts = {
|
||||
readableTimestamp: { value: timestamp, color: "90" },
|
||||
level: { value: "[INFO]", color: "32" },
|
||||
filename: { value: `(${filename})`, color: "36" },
|
||||
message: { value: joinedMessage, color: "0" },
|
||||
};
|
||||
|
||||
this.writeToLog(`${timestamp} [INFO] (${filename}) ${joinedMessage}`);
|
||||
this.writeConsoleMessageColored(logMessageParts, breakLine);
|
||||
}
|
||||
|
||||
public warn(message: string | string[], breakLine = false): void {
|
||||
const stack: string = new Error().stack || "";
|
||||
const { filename, timestamp } = this.getCallerInfo(stack);
|
||||
|
||||
const joinedMessage: string = Array.isArray(message)
|
||||
? message.join(" ")
|
||||
: message;
|
||||
|
||||
const logMessageParts: ILogMessageParts = {
|
||||
readableTimestamp: { value: timestamp, color: "90" },
|
||||
level: { value: "[WARN]", color: "33" },
|
||||
filename: { value: `(${filename})`, color: "36" },
|
||||
message: { value: joinedMessage, color: "0" },
|
||||
};
|
||||
|
||||
this.writeToLog(`${timestamp} [WARN] (${filename}) ${joinedMessage}`);
|
||||
this.writeConsoleMessageColored(logMessageParts, breakLine);
|
||||
}
|
||||
|
||||
public error(
|
||||
message: string | string[] | Error | Error[],
|
||||
breakLine = false,
|
||||
): void {
|
||||
const stack: string = new Error().stack || "";
|
||||
const { filename, timestamp } = this.getCallerInfo(stack);
|
||||
|
||||
const messages: (string | Error)[] = Array.isArray(message)
|
||||
? message
|
||||
: [message];
|
||||
const joinedMessage: string = messages
|
||||
.map((msg: string | Error): string =>
|
||||
typeof msg === "string" ? msg : msg.message,
|
||||
)
|
||||
.join(" ");
|
||||
|
||||
const logMessageParts: ILogMessageParts = {
|
||||
readableTimestamp: { value: timestamp, color: "90" },
|
||||
level: { value: "[ERROR]", color: "31" },
|
||||
filename: { value: `(${filename})`, color: "36" },
|
||||
message: { value: joinedMessage, color: "0" },
|
||||
};
|
||||
|
||||
this.writeToLog(`${timestamp} [ERROR] (${filename}) ${joinedMessage}`);
|
||||
this.writeConsoleMessageColored(logMessageParts, breakLine);
|
||||
}
|
||||
|
||||
private writeConsoleMessageColored(
|
||||
logMessageParts: ILogMessageParts,
|
||||
breakLine = false,
|
||||
): void {
|
||||
const logMessage: string = Object.keys(logMessageParts)
|
||||
.map((key: string) => {
|
||||
const part: ILogMessagePart = logMessageParts[key];
|
||||
return `\x1b[${part.color}m${part.value}\x1b[0m`;
|
||||
})
|
||||
.join(" ");
|
||||
console.log(logMessage + (breakLine ? EOL : ""));
|
||||
}
|
||||
}
|
||||
|
||||
const logger: Logger = Logger.getInstance();
|
||||
export { logger };
|
11
src/index.ts
11
src/index.ts
|
@ -1,13 +1,16 @@
|
|||
import { logger } from "@helpers/logger";
|
||||
|
||||
import { Echo } from "@atums/echo";
|
||||
import { serverHandler } from "./server";
|
||||
|
||||
export const noFileLog = new Echo({
|
||||
disableFile: true,
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
serverHandler.initialize();
|
||||
}
|
||||
|
||||
main().catch((error: Error) => {
|
||||
logger.error("Error initializing the server:");
|
||||
logger.error(error as Error);
|
||||
noFileLog.error("Error initializing the server:");
|
||||
noFileLog.error(error as Error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/* eslint-disable prettier/prettier */
|
||||
import { booruConfig } from "#environment/constants";
|
||||
|
||||
import { booruConfig } from "@config/booru";
|
||||
import type { BooruPost } from "#types/booruResponses";
|
||||
import type { IBooruConfig, IBooruConfigMap } from "#types/config";
|
||||
|
||||
export function timestampToReadable(timestamp?: number): string {
|
||||
const date: Date =
|
||||
timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date();
|
||||
timestamp && !Number.isNaN(timestamp)
|
||||
? new Date(timestamp * 1000)
|
||||
: new Date();
|
||||
if (Number.isNaN(date.getTime())) return "Invalid Date";
|
||||
return date.toISOString().replace("T", " ").replace("Z", "");
|
||||
}
|
||||
|
@ -18,57 +21,63 @@ export function tagsToExpectedFormat(
|
|||
|
||||
if (!tags) return "";
|
||||
|
||||
const processTag: (tag: string) => string | null = (tag: string) => {
|
||||
const trimmed: string | null = tag.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
const processTag = (tag: string): string | null => {
|
||||
const trimmed = tag.trim();
|
||||
return trimmed || null;
|
||||
};
|
||||
|
||||
if (typeof tags === "string") {
|
||||
return tags
|
||||
.split(/\s+|,/)
|
||||
.map(processTag)
|
||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
||||
.filter((tag): tag is string => Boolean(tag))
|
||||
.join(delimiter);
|
||||
}
|
||||
|
||||
if (Array.isArray(tags)) {
|
||||
return tags
|
||||
.map(processTag)
|
||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
||||
.filter((tag): tag is string => Boolean(tag))
|
||||
.join(delimiter);
|
||||
}
|
||||
|
||||
const allTags: string[] = Object.values(tags).flat();
|
||||
return allTags
|
||||
.map(processTag)
|
||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
||||
.filter((tag): tag is string => Boolean(tag))
|
||||
.join(delimiter);
|
||||
}
|
||||
|
||||
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]];
|
||||
export function shufflePosts<T extends BooruPost>(posts: T[]): T[] {
|
||||
if (posts.length <= 1) return posts;
|
||||
|
||||
const shuffled = [...posts];
|
||||
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const itemI = shuffled[i];
|
||||
const itemJ = shuffled[j];
|
||||
|
||||
if (itemI !== undefined && itemJ !== undefined) {
|
||||
shuffled[i] = itemJ;
|
||||
shuffled[j] = itemI;
|
||||
}
|
||||
}
|
||||
return posts;
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
export function minPosts<BooruPost>(
|
||||
posts: BooruPost[],
|
||||
min: number,
|
||||
): BooruPost[] {
|
||||
export function minPosts<T extends BooruPost>(posts: T[], min: number): T[] {
|
||||
return posts.slice(0, min);
|
||||
}
|
||||
|
||||
export function determineBooru(
|
||||
booruName: string,
|
||||
): IBooruConfigMap[keyof IBooruConfigMap] | null {
|
||||
const booru: IBooruConfigMap[keyof IBooruConfigMap] | undefined =
|
||||
Object.values(booruConfig).find(
|
||||
(booru: IBooruConfigMap[keyof IBooruConfigMap]) =>
|
||||
booru.name === booruName ||
|
||||
booru.aliases.includes(booruName.toLowerCase()),
|
||||
);
|
||||
const booru = Object.values(booruConfig).find(
|
||||
(booru) =>
|
||||
booru.name === booruName ||
|
||||
booru.aliases.includes(booruName.toLowerCase()),
|
||||
);
|
||||
|
||||
return booru || null;
|
||||
}
|
||||
|
@ -85,33 +94,47 @@ export function postExpectedFormat(
|
|||
|
||||
if (booru.name === "e621.net") {
|
||||
return {
|
||||
posts: normalizedPosts.map((post: BooruPost) => {
|
||||
posts: normalizedPosts.map((post) => {
|
||||
const hasE621Structure =
|
||||
"file" in post &&
|
||||
post.file &&
|
||||
typeof post.file === "object" &&
|
||||
"url" in post.file;
|
||||
const fileUrl = hasE621Structure ? post.file.url : null;
|
||||
|
||||
return {
|
||||
...post,
|
||||
file_url: post.file.url ?? null,
|
||||
file_url: fileUrl ?? null,
|
||||
post_url:
|
||||
post.post_url ?? `https://${booru.endpoint}/posts/${post.id}`,
|
||||
tags:
|
||||
tag_format === "unformatted"
|
||||
? post.tags
|
||||
: Object.values(post.tags || {})
|
||||
.flat()
|
||||
.join(" "),
|
||||
: typeof post.tags === "object" && post.tags !== null
|
||||
? Object.values(post.tags).flat().join(" ")
|
||||
: String(post.tags || ""),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const fixedDomain: string = booru.endpoint.replace(/^api\./, "");
|
||||
const formattedPosts: BooruPost[] = normalizedPosts.map((post: BooruPost) => {
|
||||
const formattedPosts: BooruPost[] = normalizedPosts.map((post) => {
|
||||
const postUrl: string =
|
||||
post.post_url ??
|
||||
`https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`;
|
||||
|
||||
const hasDefaultStructure =
|
||||
"directory" in post && "hash" in post && "image" in post;
|
||||
|
||||
const imageExtension: string =
|
||||
post.image?.substring(post.image.lastIndexOf(".") + 1) ?? "";
|
||||
hasDefaultStructure && post.image
|
||||
? post.image.substring(post.image.lastIndexOf(".") + 1)
|
||||
: "";
|
||||
|
||||
const fileUrl: string | null =
|
||||
post.file_url ??
|
||||
(post.directory && post.hash && imageExtension
|
||||
(hasDefaultStructure && post.directory && post.hash && imageExtension
|
||||
? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`
|
||||
: null);
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import { determineBooru, getE621Auth, getGelBooruAuth } from "@helpers/char";
|
||||
import { echo } from "@atums/echo";
|
||||
import { fetch } from "bun";
|
||||
import { determineBooru, getE621Auth, getGelBooruAuth } from "#lib/char";
|
||||
|
||||
import { logger } from "@helpers/logger";
|
||||
import type { ExtendedRequest } from "#types/bun";
|
||||
import type { IBooruConfig } from "#types/config";
|
||||
import type { RouteDef } from "#types/routes";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
|
@ -9,14 +12,8 @@ const routeDef: RouteDef = {
|
|||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
_server: BunServer,
|
||||
_requestBody: unknown,
|
||||
query: Query,
|
||||
params: Params,
|
||||
): Promise<Response> {
|
||||
const { booru, tag } = params as { booru: string; tag: string };
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
const { booru, tag } = request.params as { booru: string; tag: string };
|
||||
|
||||
if (!booru) {
|
||||
return Response.json(
|
||||
|
@ -125,11 +122,11 @@ async function handler(
|
|||
let url = `https://${booruConfig.autocomplete}${editedTag}`;
|
||||
|
||||
if (isGelbooru && gelbooruAuth) {
|
||||
url += `?api_key=${gelbooruAuth.api_key}&user_id=${gelbooruAuth.user_id}`;
|
||||
url += `?api_key=${gelbooruAuth.apiKey}&user_id=${gelbooruAuth.userId}`;
|
||||
}
|
||||
|
||||
try {
|
||||
let headers: Record<string, string> | undefined;
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (isE621) {
|
||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||
|
@ -159,7 +156,7 @@ async function handler(
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error([
|
||||
echo.error([
|
||||
"Failed to fetch post",
|
||||
`Booru: ${booru}`,
|
||||
`Status: ${response.status}`,
|
||||
|
@ -181,11 +178,7 @@ async function handler(
|
|||
const data: unknown = await response.json();
|
||||
|
||||
if (!data) {
|
||||
logger.error([
|
||||
"No data returned",
|
||||
`Booru: ${booru}`,
|
||||
`Tag: ${editedTag}`,
|
||||
]);
|
||||
echo.error(["No data returned", `Booru: ${booru}`, `Tag: ${editedTag}`]);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
|
@ -225,7 +218,7 @@ async function handler(
|
|||
},
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
echo.error([
|
||||
"Failed to fetch post",
|
||||
`Booru: ${booru}`,
|
||||
`Tag: ${editedTag}`,
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { fetch } from "bun";
|
||||
import {
|
||||
determineBooru,
|
||||
getE621Auth,
|
||||
getGelBooruAuth,
|
||||
postExpectedFormat,
|
||||
} from "@helpers/char";
|
||||
import { fetch } from "bun";
|
||||
} from "#lib/char";
|
||||
|
||||
import { logger } from "@helpers/logger";
|
||||
import type { BooruPost, Data } from "#types/booruResponses";
|
||||
import type { ExtendedRequest } from "#types/bun";
|
||||
import type { IBooruConfig } from "#types/config";
|
||||
import type { RouteDef } from "#types/routes";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
|
@ -14,17 +18,11 @@ const routeDef: RouteDef = {
|
|||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
_server: BunServer,
|
||||
_requestBody: unknown,
|
||||
query: Query,
|
||||
params: Params,
|
||||
): Promise<Response> {
|
||||
const { tag_format } = query as {
|
||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||
const { tag_format } = request.query as {
|
||||
tag_format: string;
|
||||
};
|
||||
const { booru, id } = params as { booru: string; id: string };
|
||||
const { booru, id } = request.params as { booru: string; id: string };
|
||||
|
||||
if (!booru || !id) {
|
||||
return Response.json(
|
||||
|
@ -89,7 +87,7 @@ async function handler(
|
|||
let url = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`;
|
||||
|
||||
if (isGelbooru && gelbooruAuth) {
|
||||
url += `?api_key=${gelbooruAuth.api_key}&user_id=${gelbooruAuth.user_id}`;
|
||||
url += `?api_key=${gelbooruAuth.apiKey}&user_id=${gelbooruAuth.userId}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(funcString)) {
|
||||
|
@ -99,7 +97,7 @@ async function handler(
|
|||
}
|
||||
|
||||
try {
|
||||
let headers: Record<string, string> | undefined;
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (isE621) {
|
||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||
|
@ -129,7 +127,7 @@ async function handler(
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error([
|
||||
echo.error([
|
||||
"Failed to fetch post",
|
||||
`Booru: ${booru}`,
|
||||
`ID: ${id}`,
|
||||
|
@ -152,7 +150,7 @@ async function handler(
|
|||
const data: unknown = await response.json();
|
||||
|
||||
if (!data) {
|
||||
logger.error(["No data returned", `Booru: ${booru}`, `ID: ${id}`]);
|
||||
echo.error(["No data returned", `Booru: ${booru}`, `ID: ${id}`]);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
|
@ -177,7 +175,7 @@ async function handler(
|
|||
}
|
||||
|
||||
if (posts.length === 0) {
|
||||
logger.error(["No posts found", `Booru: ${booru}`, `ID: ${id}`]);
|
||||
echo.error(["No posts found", `Booru: ${booru}`, `ID: ${id}`]);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
|
@ -197,7 +195,7 @@ async function handler(
|
|||
);
|
||||
|
||||
if (!expectedData) {
|
||||
logger.error([
|
||||
echo.error([
|
||||
"Unexpected data format",
|
||||
`Booru: ${booru}`,
|
||||
`ID: ${id}`,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { type Server, fetch } from "bun";
|
||||
import {
|
||||
determineBooru,
|
||||
getE621Auth,
|
||||
|
@ -6,10 +8,12 @@ import {
|
|||
postExpectedFormat,
|
||||
shufflePosts,
|
||||
tagsToExpectedFormat,
|
||||
} from "@helpers/char";
|
||||
import { fetch } from "bun";
|
||||
} from "#lib/char";
|
||||
|
||||
import { logger } from "@helpers/logger";
|
||||
import type { BooruPost, Data } from "#types/booruResponses";
|
||||
import type { ExtendedRequest } from "#types/bun";
|
||||
import type { IBooruConfig } from "#types/config";
|
||||
import type { RouteDef } from "#types/routes";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
|
@ -19,13 +23,11 @@ const routeDef: RouteDef = {
|
|||
};
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
_server: BunServer,
|
||||
request: ExtendedRequest,
|
||||
_server: Server,
|
||||
requestBody: unknown,
|
||||
query: Query,
|
||||
params: Params,
|
||||
): Promise<Response> {
|
||||
const { booru } = params as { booru: string };
|
||||
const { booru } = request.params as { booru: string };
|
||||
const {
|
||||
tags,
|
||||
results = 5,
|
||||
|
@ -184,7 +186,12 @@ async function handler(
|
|||
parts.push("&");
|
||||
}
|
||||
|
||||
if (isGelbooru && gelbooruAuth) {
|
||||
if (
|
||||
isGelbooru &&
|
||||
gelbooruAuth &&
|
||||
gelbooruAuth.apiKey &&
|
||||
gelbooruAuth.userId
|
||||
) {
|
||||
parts.push("api_key");
|
||||
parts.push(gelbooruAuth.apiKey);
|
||||
parts.push("&");
|
||||
|
@ -211,7 +218,7 @@ async function handler(
|
|||
const url: string = getUrl(pageString(state.page), resultsString);
|
||||
|
||||
try {
|
||||
let headers: Record<string, string> | undefined;
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (isE621) {
|
||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||
|
@ -272,7 +279,11 @@ async function handler(
|
|||
|
||||
let posts: BooruPost[] = [];
|
||||
if (booruConfig.name === "realbooru.com" || isGelbooru) {
|
||||
posts = parsedData.post || [];
|
||||
if (parsedData.post) {
|
||||
posts = Array.isArray(parsedData.post)
|
||||
? parsedData.post
|
||||
: [parsedData.post];
|
||||
}
|
||||
} else {
|
||||
if (parsedData.post) {
|
||||
posts = [parsedData.post];
|
||||
|
@ -321,7 +332,7 @@ async function handler(
|
|||
}
|
||||
}
|
||||
|
||||
logger.error([
|
||||
echo.error([
|
||||
"No posts found",
|
||||
`Booru: ${booru}`,
|
||||
`Tags: ${tagsString()}`,
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { type Server, fetch } from "bun";
|
||||
import {
|
||||
determineBooru,
|
||||
getE621Auth,
|
||||
getGelBooruAuth,
|
||||
postExpectedFormat,
|
||||
tagsToExpectedFormat,
|
||||
} from "@helpers/char";
|
||||
import { fetch } from "bun";
|
||||
} from "#lib/char";
|
||||
|
||||
import type { BooruPost, Data } from "#types/booruResponses";
|
||||
import type { ExtendedRequest } from "#types/bun";
|
||||
import type { IBooruConfig } from "#types/config";
|
||||
import type { RouteDef } from "#types/routes";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "POST",
|
||||
|
@ -15,13 +20,11 @@ const routeDef: RouteDef = {
|
|||
};
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
_server: BunServer,
|
||||
request: ExtendedRequest,
|
||||
_server: Server,
|
||||
requestBody: unknown,
|
||||
query: Query,
|
||||
params: Params,
|
||||
): Promise<Response> {
|
||||
const { booru } = params as { booru: string };
|
||||
const { booru } = request.params as { booru: string };
|
||||
const {
|
||||
page = 0,
|
||||
tags,
|
||||
|
@ -182,7 +185,12 @@ async function handler(
|
|||
parts.push("&");
|
||||
}
|
||||
|
||||
if (isGelbooru && gelbooruAuth) {
|
||||
if (
|
||||
isGelbooru &&
|
||||
gelbooruAuth &&
|
||||
gelbooruAuth.apiKey &&
|
||||
gelbooruAuth.userId
|
||||
) {
|
||||
parts.push("api_key");
|
||||
parts.push(gelbooruAuth.apiKey);
|
||||
parts.push("&");
|
||||
|
@ -200,7 +208,7 @@ async function handler(
|
|||
};
|
||||
|
||||
try {
|
||||
let headers: Record<string, string> | undefined;
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (isE621) {
|
||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { RouteDef } from "#types/routes";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
|
|
169
src/server.ts
169
src/server.ts
|
@ -1,6 +1,12 @@
|
|||
import { environment } from "@config/environment";
|
||||
import { logger } from "@helpers/logger";
|
||||
import { FileSystemRouter, type MatchedRoute, type Serve } from "bun";
|
||||
import { resolve } from "node:path";
|
||||
import { type Echo, echo } from "@atums/echo";
|
||||
import { FileSystemRouter, type MatchedRoute, type Server } from "bun";
|
||||
import { environment } from "#environment";
|
||||
import { reqLoggerIgnores } from "#environment/constants";
|
||||
import { noFileLog } from "#index";
|
||||
|
||||
import type { ExtendedRequest } from "#types/bun";
|
||||
import type { RouteModule } from "#types/routes";
|
||||
|
||||
class ServerHandler {
|
||||
private router: FileSystemRouter;
|
||||
|
@ -11,28 +17,27 @@ class ServerHandler {
|
|||
) {
|
||||
this.router = new FileSystemRouter({
|
||||
style: "nextjs",
|
||||
dir: "./src/routes",
|
||||
dir: resolve("src", "routes"),
|
||||
fileExtensions: [".ts"],
|
||||
origin: `http://${this.host}:${this.port}`,
|
||||
});
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const server: Serve = Bun.serve({
|
||||
const server: Server = Bun.serve({
|
||||
port: this.port,
|
||||
hostname: this.host,
|
||||
fetch: this.handleRequest.bind(this),
|
||||
});
|
||||
|
||||
logger.info(
|
||||
noFileLog.info(
|
||||
`Server running at http://${server.hostname}:${server.port}`,
|
||||
true,
|
||||
);
|
||||
|
||||
this.logRoutes();
|
||||
this.logRoutes(noFileLog);
|
||||
}
|
||||
|
||||
private logRoutes(): void {
|
||||
logger.info("Available routes:");
|
||||
private logRoutes(echo: Echo): void {
|
||||
echo.info("Available routes:");
|
||||
|
||||
const sortedRoutes: [string, string][] = Object.entries(
|
||||
this.router.routes,
|
||||
|
@ -41,17 +46,76 @@ class ServerHandler {
|
|||
);
|
||||
|
||||
for (const [path, filePath] of sortedRoutes) {
|
||||
logger.info(`Route: ${path}, File: ${filePath}`);
|
||||
echo.info(`Route: ${path}, File: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
private logRequest(
|
||||
request: ExtendedRequest,
|
||||
response: Response,
|
||||
ip: string | undefined,
|
||||
): void {
|
||||
const pathname = new URL(request.url).pathname;
|
||||
|
||||
const { ignoredStartsWith, ignoredPaths } = reqLoggerIgnores;
|
||||
|
||||
if (
|
||||
ignoredStartsWith.some((prefix) => pathname.startsWith(prefix)) ||
|
||||
ignoredPaths.includes(pathname)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo.custom(`${request.method}`, `${response.status}`, [
|
||||
pathname,
|
||||
`${(performance.now() - request.startPerf).toFixed(2)}ms`,
|
||||
ip || "unknown",
|
||||
]);
|
||||
}
|
||||
|
||||
private async handleRequest(
|
||||
request: Request,
|
||||
server: BunServer,
|
||||
server: Server,
|
||||
): Promise<Response> {
|
||||
const extendedRequest: ExtendedRequest = request as ExtendedRequest;
|
||||
extendedRequest.startPerf = performance.now();
|
||||
|
||||
const headers = request.headers;
|
||||
let ip = server.requestIP(request)?.address;
|
||||
let response: Response;
|
||||
|
||||
if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") {
|
||||
ip =
|
||||
headers.get("CF-Connecting-IP")?.trim() ||
|
||||
headers.get("X-Real-IP")?.trim() ||
|
||||
headers.get("X-Forwarded-For")?.split(",")[0]?.trim() ||
|
||||
"unknown";
|
||||
}
|
||||
|
||||
const pathname: string = new URL(request.url).pathname;
|
||||
|
||||
const baseDir = resolve("custom");
|
||||
const customPath = resolve(baseDir, pathname.slice(1));
|
||||
|
||||
if (!customPath.startsWith(baseDir)) {
|
||||
response = new Response("Forbidden", { status: 403 });
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
||||
const customFile = Bun.file(customPath);
|
||||
if (await customFile.exists()) {
|
||||
const content = await customFile.arrayBuffer();
|
||||
const type: string = customFile.type ?? "application/octet-stream";
|
||||
response = new Response(content, {
|
||||
headers: { "Content-Type": type },
|
||||
});
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
|
||||
const match: MatchedRoute | null = this.router.match(request);
|
||||
let requestBody: unknown = {};
|
||||
let response: Response;
|
||||
|
||||
if (match) {
|
||||
const { filePath, params, query } = match;
|
||||
|
@ -60,7 +124,7 @@ class ServerHandler {
|
|||
const routeModule: RouteModule = await import(filePath);
|
||||
const contentType: string | null = request.headers.get("Content-Type");
|
||||
const actualContentType: string | null = contentType
|
||||
? contentType.split(";")[0].trim()
|
||||
? (contentType.split(";")[0]?.trim() ?? null)
|
||||
: null;
|
||||
|
||||
if (
|
||||
|
@ -83,47 +147,76 @@ class ServerHandler {
|
|||
}
|
||||
}
|
||||
|
||||
if (routeModule.routeDef.method !== request.method) {
|
||||
if (
|
||||
(Array.isArray(routeModule.routeDef.method) &&
|
||||
!routeModule.routeDef.method.includes(request.method)) ||
|
||||
(!Array.isArray(routeModule.routeDef.method) &&
|
||||
routeModule.routeDef.method !== request.method)
|
||||
) {
|
||||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 405,
|
||||
error: `Method ${request.method} Not Allowed`,
|
||||
error: `Method ${request.method} Not Allowed, expected ${
|
||||
Array.isArray(routeModule.routeDef.method)
|
||||
? routeModule.routeDef.method.join(", ")
|
||||
: routeModule.routeDef.method
|
||||
}`,
|
||||
},
|
||||
{ status: 405 },
|
||||
);
|
||||
} else {
|
||||
const expectedContentType: string | null =
|
||||
const expectedContentType: string | string[] | null =
|
||||
routeModule.routeDef.accepts;
|
||||
|
||||
const matchesAccepts: boolean =
|
||||
expectedContentType === "*/*" ||
|
||||
actualContentType === expectedContentType;
|
||||
let matchesAccepts: boolean;
|
||||
|
||||
if (Array.isArray(expectedContentType)) {
|
||||
matchesAccepts =
|
||||
expectedContentType.includes("*/*") ||
|
||||
expectedContentType.includes(actualContentType || "");
|
||||
} else {
|
||||
matchesAccepts =
|
||||
expectedContentType === "*/*" ||
|
||||
actualContentType === expectedContentType;
|
||||
}
|
||||
|
||||
if (!matchesAccepts) {
|
||||
response = Response.json(
|
||||
{
|
||||
success: false,
|
||||
code: 406,
|
||||
error: `Content-Type ${contentType} Not Acceptable`,
|
||||
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
|
||||
Array.isArray(expectedContentType)
|
||||
? expectedContentType.join(", ")
|
||||
: expectedContentType
|
||||
}`,
|
||||
},
|
||||
{ status: 406 },
|
||||
);
|
||||
} else {
|
||||
extendedRequest.params = params;
|
||||
extendedRequest.query = query;
|
||||
|
||||
response = await routeModule.handler(
|
||||
request,
|
||||
extendedRequest,
|
||||
server,
|
||||
requestBody,
|
||||
query,
|
||||
params,
|
||||
);
|
||||
|
||||
response.headers.set("Content-Type", routeModule.routeDef.returns);
|
||||
if (routeModule.routeDef.returns !== "*/*") {
|
||||
response.headers.set(
|
||||
"Content-Type",
|
||||
routeModule.routeDef.returns,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logger.error(`Error handling route ${request.url}:`);
|
||||
logger.error(error as Error);
|
||||
echo.error({
|
||||
message: `Error handling route ${request.url}`,
|
||||
error: error,
|
||||
});
|
||||
|
||||
response = Response.json(
|
||||
{
|
||||
|
@ -145,27 +238,11 @@ class ServerHandler {
|
|||
);
|
||||
}
|
||||
|
||||
const headers: Headers = response.headers;
|
||||
let ip: string | null = server.requestIP(request)?.address || null;
|
||||
|
||||
if (!ip) {
|
||||
ip =
|
||||
headers.get("CF-Connecting-IP") ||
|
||||
headers.get("X-Real-IP") ||
|
||||
headers.get("X-Forwarded-For") ||
|
||||
null;
|
||||
}
|
||||
|
||||
logger.info([
|
||||
`[${request.method}]`,
|
||||
request.url,
|
||||
`${response.status}`,
|
||||
`(${ip || "unknown"})`,
|
||||
]);
|
||||
|
||||
this.logRequest(extendedRequest, response, ip);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
const serverHandler: ServerHandler = new ServerHandler(
|
||||
environment.port,
|
||||
environment.host,
|
||||
|
|
|
@ -2,33 +2,29 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@config/*": ["config/*"],
|
||||
"@types/*": ["types/*"],
|
||||
"@helpers/*": ["src/helpers/*"],
|
||||
"@database/*": ["src/database/*"]
|
||||
"#*": ["src/*"],
|
||||
"#types/*": ["types/*"],
|
||||
"#environment": ["config/index.ts"],
|
||||
"#environment/*": ["config/*"]
|
||||
},
|
||||
"typeRoots": ["./src/types", "./node_modules/@types"],
|
||||
// Enable latest features
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
// Bundler mode
|
||||
"allowJs": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"include": ["src", "types", "config"]
|
||||
"include": ["src", "environment", "config"]
|
||||
}
|
||||
|
|
26
types/booruResponses.d.ts
vendored
26
types/booruResponses.d.ts
vendored
|
@ -1,26 +0,0 @@
|
|||
type Data = {
|
||||
post?: Post;
|
||||
posts?: Post[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
interface DefaultPost {
|
||||
directory: number;
|
||||
hash: string;
|
||||
id: number;
|
||||
image: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
type E621Post = {
|
||||
id: number;
|
||||
file: {
|
||||
url: string;
|
||||
};
|
||||
tags: string;
|
||||
};
|
||||
|
||||
type BooruPost = {
|
||||
file_url?: string | null;
|
||||
post_url?: string;
|
||||
} & (DefaultPost | e621Post);
|
28
types/booruResponses.ts
Normal file
28
types/booruResponses.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
type Data = {
|
||||
post?: BooruPost;
|
||||
posts?: BooruPost[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
interface DefaultPost {
|
||||
directory?: number;
|
||||
hash?: string;
|
||||
id: number;
|
||||
image?: string;
|
||||
tags: string | Record<string, string[]>;
|
||||
}
|
||||
|
||||
type E621Post = {
|
||||
id: number;
|
||||
file: {
|
||||
url: string;
|
||||
};
|
||||
tags: Record<string, string[]>;
|
||||
};
|
||||
|
||||
type BooruPost = {
|
||||
file_url?: string | null;
|
||||
post_url?: string;
|
||||
} & (DefaultPost | E621Post);
|
||||
|
||||
export type { Data, DefaultPost, E621Post, BooruPost };
|
5
types/bun.d.ts
vendored
5
types/bun.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
import type { Server } from "bun";
|
||||
|
||||
declare global {
|
||||
type BunServer = Server;
|
||||
}
|
10
types/bun.ts
Normal file
10
types/bun.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
type Query = Record<string, string>;
|
||||
type Params = Record<string, string>;
|
||||
|
||||
interface ExtendedRequest extends Request {
|
||||
startPerf: number;
|
||||
query: Query;
|
||||
params: Params;
|
||||
}
|
||||
|
||||
export type { ExtendedRequest, Query, Params };
|
|
@ -29,3 +29,5 @@ type IBooruConfig = {
|
|||
functions: IBooruDefaults;
|
||||
autocomplete?: string;
|
||||
};
|
||||
|
||||
export type { Environment, IBooruDefaults, IBooruConfigMap, IBooruConfig };
|
9
types/logger.d.ts
vendored
9
types/logger.d.ts
vendored
|
@ -1,9 +0,0 @@
|
|||
type ILogMessagePart = { value: string; color: string };
|
||||
|
||||
type ILogMessageParts = {
|
||||
level: ILogMessagePart;
|
||||
filename: ILogMessagePart;
|
||||
readableTimestamp: ILogMessagePart;
|
||||
message: ILogMessagePart;
|
||||
[key: string]: ILogMessagePart;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import type { Server } from "bun";
|
||||
|
||||
type RouteDef = {
|
||||
method: string;
|
||||
accepts: string | null;
|
||||
|
@ -11,10 +13,10 @@ type Params = Record<string, string>;
|
|||
type RouteModule = {
|
||||
handler: (
|
||||
request: Request,
|
||||
server: BunServer,
|
||||
server: Server,
|
||||
requestBody: unknown,
|
||||
query: Query,
|
||||
params: Params,
|
||||
) => Promise<Response> | Response;
|
||||
routeDef: RouteDef;
|
||||
};
|
||||
|
||||
export type { RouteDef, Query, Params, RouteModule };
|
Loading…
Add table
Add a link
Reference in a new issue