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
|
23
biome.json
23
biome.json
|
@ -7,7 +7,7 @@
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": true,
|
"ignoreUnknown": true,
|
||||||
"ignore": []
|
"ignore": ["dist"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -17,12 +17,31 @@
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
|
"css": {
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "tab",
|
||||||
|
"lineEnding": "lf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true
|
"recommended": true,
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedImports": "error",
|
||||||
|
"noUnusedVariables": "error"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noConsole": "error"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useConst": "error",
|
||||||
|
"noVar": "error",
|
||||||
|
"useImportType": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ignore": ["types"]
|
||||||
|
},
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"quoteStyle": "double",
|
"quoteStyle": "double",
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
// cSpell:disable
|
// cSpell:disable
|
||||||
|
|
||||||
|
const reqLoggerIgnores = {
|
||||||
|
ignoredStartsWith: ["/public"],
|
||||||
|
ignoredPaths: ["/favicon.ico"],
|
||||||
|
};
|
||||||
|
|
||||||
|
import type { IBooruConfigMap, IBooruDefaults } from "#types/config";
|
||||||
|
|
||||||
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",
|
||||||
id: "index.php?page=dapi&s=post&q=index&json=1&id=",
|
id: "index.php?page=dapi&s=post&q=index&json=1&id=",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const booruConfig: IBooruConfigMap = {
|
const booruConfig: IBooruConfigMap = {
|
||||||
"rule34.xxx": {
|
"rule34.xxx": {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: "rule34.xxx",
|
name: "rule34.xxx",
|
||||||
|
@ -76,3 +83,5 @@ export const booruConfig: IBooruConfigMap = {
|
||||||
functions: booruDefaults,
|
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",
|
"name": "booru-api",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "latest",
|
||||||
"@eslint/js": "^9.24.0",
|
"@types/bun": "latest"
|
||||||
"@types/bun": "^1.2.9",
|
|
||||||
"globals": "^16.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.8.3"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
|
@ -17,5 +12,8 @@
|
||||||
"lint:fix": "bunx biome check --fix",
|
"lint:fix": "bunx biome check --fix",
|
||||||
"cleanup": "rm -rf logs node_modules bun.lock"
|
"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";
|
import { serverHandler } from "./server";
|
||||||
|
|
||||||
|
export const noFileLog = new Echo({
|
||||||
|
disableFile: true,
|
||||||
|
});
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
serverHandler.initialize();
|
serverHandler.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error: Error) => {
|
main().catch((error: Error) => {
|
||||||
logger.error("Error initializing the server:");
|
noFileLog.error("Error initializing the server:");
|
||||||
logger.error(error as Error);
|
noFileLog.error(error as Error);
|
||||||
process.exit(1);
|
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 {
|
export function timestampToReadable(timestamp?: number): string {
|
||||||
const date: Date =
|
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";
|
if (Number.isNaN(date.getTime())) return "Invalid Date";
|
||||||
return date.toISOString().replace("T", " ").replace("Z", "");
|
return date.toISOString().replace("T", " ").replace("Z", "");
|
||||||
}
|
}
|
||||||
|
@ -18,54 +21,60 @@ export function tagsToExpectedFormat(
|
||||||
|
|
||||||
if (!tags) return "";
|
if (!tags) return "";
|
||||||
|
|
||||||
const processTag: (tag: string) => string | null = (tag: string) => {
|
const processTag = (tag: string): string | null => {
|
||||||
const trimmed: string | null = tag.trim();
|
const trimmed = tag.trim();
|
||||||
return trimmed ? trimmed : null;
|
return trimmed || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof tags === "string") {
|
if (typeof tags === "string") {
|
||||||
return tags
|
return tags
|
||||||
.split(/\s+|,/)
|
.split(/\s+|,/)
|
||||||
.map(processTag)
|
.map(processTag)
|
||||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
.filter((tag): tag is string => Boolean(tag))
|
||||||
.join(delimiter);
|
.join(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(tags)) {
|
if (Array.isArray(tags)) {
|
||||||
return tags
|
return tags
|
||||||
.map(processTag)
|
.map(processTag)
|
||||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
.filter((tag): tag is string => Boolean(tag))
|
||||||
.join(delimiter);
|
.join(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTags: string[] = Object.values(tags).flat();
|
const allTags: string[] = Object.values(tags).flat();
|
||||||
return allTags
|
return allTags
|
||||||
.map(processTag)
|
.map(processTag)
|
||||||
.filter((tag: string | null): tag is string => Boolean(tag))
|
.filter((tag): tag is string => Boolean(tag))
|
||||||
.join(delimiter);
|
.join(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shufflePosts<BooruPost>(posts: BooruPost[]): BooruPost[] {
|
export function shufflePosts<T extends BooruPost>(posts: T[]): T[] {
|
||||||
for (let i: number = posts.length - 1; i > 0; i--) {
|
if (posts.length <= 1) return posts;
|
||||||
const j: number = Math.floor(Math.random() * (i + 1));
|
|
||||||
[posts[i], posts[j]] = [posts[j], posts[i]];
|
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>(
|
export function minPosts<T extends BooruPost>(posts: T[], min: number): T[] {
|
||||||
posts: BooruPost[],
|
|
||||||
min: number,
|
|
||||||
): BooruPost[] {
|
|
||||||
return posts.slice(0, min);
|
return posts.slice(0, min);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function determineBooru(
|
export function determineBooru(
|
||||||
booruName: string,
|
booruName: string,
|
||||||
): IBooruConfigMap[keyof IBooruConfigMap] | null {
|
): IBooruConfigMap[keyof IBooruConfigMap] | null {
|
||||||
const booru: IBooruConfigMap[keyof IBooruConfigMap] | undefined =
|
const booru = Object.values(booruConfig).find(
|
||||||
Object.values(booruConfig).find(
|
(booru) =>
|
||||||
(booru: IBooruConfigMap[keyof IBooruConfigMap]) =>
|
|
||||||
booru.name === booruName ||
|
booru.name === booruName ||
|
||||||
booru.aliases.includes(booruName.toLowerCase()),
|
booru.aliases.includes(booruName.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
@ -85,33 +94,47 @@ export function postExpectedFormat(
|
||||||
|
|
||||||
if (booru.name === "e621.net") {
|
if (booru.name === "e621.net") {
|
||||||
return {
|
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 {
|
return {
|
||||||
...post,
|
...post,
|
||||||
file_url: post.file.url ?? null,
|
file_url: fileUrl ?? 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 || {})
|
: typeof post.tags === "object" && post.tags !== null
|
||||||
.flat()
|
? Object.values(post.tags).flat().join(" ")
|
||||||
.join(" "),
|
: String(post.tags || ""),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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((post) => {
|
||||||
const postUrl: string =
|
const postUrl: string =
|
||||||
post.post_url ??
|
post.post_url ??
|
||||||
`https://${fixedDomain}/index.php?page=post&s=view&id=${post.id}`;
|
`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 =
|
const imageExtension: string =
|
||||||
post.image?.substring(post.image.lastIndexOf(".") + 1) ?? "";
|
hasDefaultStructure && post.image
|
||||||
|
? post.image.substring(post.image.lastIndexOf(".") + 1)
|
||||||
|
: "";
|
||||||
|
|
||||||
const fileUrl: string | null =
|
const fileUrl: string | null =
|
||||||
post.file_url ??
|
post.file_url ??
|
||||||
(post.directory && post.hash && imageExtension
|
(hasDefaultStructure && post.directory && post.hash && imageExtension
|
||||||
? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`
|
? `https://${booru.endpoint}/images/${post.directory}/${post.hash}.${imageExtension}`
|
||||||
: null);
|
: null);
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { determineBooru, getE621Auth, getGelBooruAuth } from "@helpers/char";
|
import { echo } from "@atums/echo";
|
||||||
import { fetch } from "bun";
|
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 = {
|
const routeDef: RouteDef = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -9,14 +12,8 @@ const routeDef: RouteDef = {
|
||||||
returns: "application/json",
|
returns: "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
request: Request,
|
const { booru, tag } = request.params as { booru: string; tag: string };
|
||||||
_server: BunServer,
|
|
||||||
_requestBody: unknown,
|
|
||||||
query: Query,
|
|
||||||
params: Params,
|
|
||||||
): Promise<Response> {
|
|
||||||
const { booru, tag } = params as { booru: string; tag: string };
|
|
||||||
|
|
||||||
if (!booru) {
|
if (!booru) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
@ -125,11 +122,11 @@ async function handler(
|
||||||
let url = `https://${booruConfig.autocomplete}${editedTag}`;
|
let url = `https://${booruConfig.autocomplete}${editedTag}`;
|
||||||
|
|
||||||
if (isGelbooru && gelbooruAuth) {
|
if (isGelbooru && gelbooruAuth) {
|
||||||
url += `?api_key=${gelbooruAuth.api_key}&user_id=${gelbooruAuth.user_id}`;
|
url += `?api_key=${gelbooruAuth.apiKey}&user_id=${gelbooruAuth.userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let headers: Record<string, string> | undefined;
|
let headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (isE621) {
|
if (isE621) {
|
||||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||||
|
@ -159,7 +156,7 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error([
|
echo.error([
|
||||||
"Failed to fetch post",
|
"Failed to fetch post",
|
||||||
`Booru: ${booru}`,
|
`Booru: ${booru}`,
|
||||||
`Status: ${response.status}`,
|
`Status: ${response.status}`,
|
||||||
|
@ -181,11 +178,7 @@ async function handler(
|
||||||
const data: unknown = await response.json();
|
const data: unknown = await response.json();
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.error([
|
echo.error(["No data returned", `Booru: ${booru}`, `Tag: ${editedTag}`]);
|
||||||
"No data returned",
|
|
||||||
`Booru: ${booru}`,
|
|
||||||
`Tag: ${editedTag}`,
|
|
||||||
]);
|
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -225,7 +218,7 @@ async function handler(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error([
|
echo.error([
|
||||||
"Failed to fetch post",
|
"Failed to fetch post",
|
||||||
`Booru: ${booru}`,
|
`Booru: ${booru}`,
|
||||||
`Tag: ${editedTag}`,
|
`Tag: ${editedTag}`,
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
|
import { echo } from "@atums/echo";
|
||||||
|
import { fetch } from "bun";
|
||||||
import {
|
import {
|
||||||
determineBooru,
|
determineBooru,
|
||||||
getE621Auth,
|
getE621Auth,
|
||||||
getGelBooruAuth,
|
getGelBooruAuth,
|
||||||
postExpectedFormat,
|
postExpectedFormat,
|
||||||
} from "@helpers/char";
|
} from "#lib/char";
|
||||||
import { fetch } from "bun";
|
|
||||||
|
|
||||||
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 = {
|
const routeDef: RouteDef = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -14,17 +18,11 @@ const routeDef: RouteDef = {
|
||||||
returns: "application/json",
|
returns: "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
request: Request,
|
const { tag_format } = request.query as {
|
||||||
_server: BunServer,
|
|
||||||
_requestBody: unknown,
|
|
||||||
query: Query,
|
|
||||||
params: Params,
|
|
||||||
): Promise<Response> {
|
|
||||||
const { tag_format } = query as {
|
|
||||||
tag_format: string;
|
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) {
|
if (!booru || !id) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
|
@ -89,7 +87,7 @@ async function handler(
|
||||||
let url = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`;
|
let url = `https://${booruConfig.endpoint}/${booruConfig.functions.id}${id}`;
|
||||||
|
|
||||||
if (isGelbooru && gelbooruAuth) {
|
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)) {
|
if (Array.isArray(funcString)) {
|
||||||
|
@ -99,7 +97,7 @@ async function handler(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let headers: Record<string, string> | undefined;
|
let headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (isE621) {
|
if (isE621) {
|
||||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||||
|
@ -129,7 +127,7 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error([
|
echo.error([
|
||||||
"Failed to fetch post",
|
"Failed to fetch post",
|
||||||
`Booru: ${booru}`,
|
`Booru: ${booru}`,
|
||||||
`ID: ${id}`,
|
`ID: ${id}`,
|
||||||
|
@ -152,7 +150,7 @@ async function handler(
|
||||||
const data: unknown = await response.json();
|
const data: unknown = await response.json();
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.error(["No data returned", `Booru: ${booru}`, `ID: ${id}`]);
|
echo.error(["No data returned", `Booru: ${booru}`, `ID: ${id}`]);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -177,7 +175,7 @@ async function handler(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (posts.length === 0) {
|
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(
|
return Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -197,7 +195,7 @@ async function handler(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expectedData) {
|
if (!expectedData) {
|
||||||
logger.error([
|
echo.error([
|
||||||
"Unexpected data format",
|
"Unexpected data format",
|
||||||
`Booru: ${booru}`,
|
`Booru: ${booru}`,
|
||||||
`ID: ${id}`,
|
`ID: ${id}`,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { echo } from "@atums/echo";
|
||||||
|
import { type Server, fetch } from "bun";
|
||||||
import {
|
import {
|
||||||
determineBooru,
|
determineBooru,
|
||||||
getE621Auth,
|
getE621Auth,
|
||||||
|
@ -6,10 +8,12 @@ import {
|
||||||
postExpectedFormat,
|
postExpectedFormat,
|
||||||
shufflePosts,
|
shufflePosts,
|
||||||
tagsToExpectedFormat,
|
tagsToExpectedFormat,
|
||||||
} from "@helpers/char";
|
} from "#lib/char";
|
||||||
import { fetch } from "bun";
|
|
||||||
|
|
||||||
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 = {
|
const routeDef: RouteDef = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -19,13 +23,11 @@ const routeDef: RouteDef = {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(
|
async function handler(
|
||||||
request: Request,
|
request: ExtendedRequest,
|
||||||
_server: BunServer,
|
_server: Server,
|
||||||
requestBody: unknown,
|
requestBody: unknown,
|
||||||
query: Query,
|
|
||||||
params: Params,
|
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const { booru } = params as { booru: string };
|
const { booru } = request.params as { booru: string };
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
results = 5,
|
results = 5,
|
||||||
|
@ -184,7 +186,12 @@ async function handler(
|
||||||
parts.push("&");
|
parts.push("&");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGelbooru && gelbooruAuth) {
|
if (
|
||||||
|
isGelbooru &&
|
||||||
|
gelbooruAuth &&
|
||||||
|
gelbooruAuth.apiKey &&
|
||||||
|
gelbooruAuth.userId
|
||||||
|
) {
|
||||||
parts.push("api_key");
|
parts.push("api_key");
|
||||||
parts.push(gelbooruAuth.apiKey);
|
parts.push(gelbooruAuth.apiKey);
|
||||||
parts.push("&");
|
parts.push("&");
|
||||||
|
@ -211,7 +218,7 @@ async function handler(
|
||||||
const url: string = getUrl(pageString(state.page), resultsString);
|
const url: string = getUrl(pageString(state.page), resultsString);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let headers: Record<string, string> | undefined;
|
let headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (isE621) {
|
if (isE621) {
|
||||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||||
|
@ -272,7 +279,11 @@ async function handler(
|
||||||
|
|
||||||
let posts: BooruPost[] = [];
|
let posts: BooruPost[] = [];
|
||||||
if (booruConfig.name === "realbooru.com" || isGelbooru) {
|
if (booruConfig.name === "realbooru.com" || isGelbooru) {
|
||||||
posts = parsedData.post || [];
|
if (parsedData.post) {
|
||||||
|
posts = Array.isArray(parsedData.post)
|
||||||
|
? parsedData.post
|
||||||
|
: [parsedData.post];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (parsedData.post) {
|
if (parsedData.post) {
|
||||||
posts = [parsedData.post];
|
posts = [parsedData.post];
|
||||||
|
@ -321,7 +332,7 @@ async function handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error([
|
echo.error([
|
||||||
"No posts found",
|
"No posts found",
|
||||||
`Booru: ${booru}`,
|
`Booru: ${booru}`,
|
||||||
`Tags: ${tagsString()}`,
|
`Tags: ${tagsString()}`,
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
import { type Server, fetch } from "bun";
|
||||||
import {
|
import {
|
||||||
determineBooru,
|
determineBooru,
|
||||||
getE621Auth,
|
getE621Auth,
|
||||||
getGelBooruAuth,
|
getGelBooruAuth,
|
||||||
postExpectedFormat,
|
postExpectedFormat,
|
||||||
tagsToExpectedFormat,
|
tagsToExpectedFormat,
|
||||||
} from "@helpers/char";
|
} from "#lib/char";
|
||||||
import { fetch } from "bun";
|
|
||||||
|
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 = {
|
const routeDef: RouteDef = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -15,13 +20,11 @@ const routeDef: RouteDef = {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handler(
|
async function handler(
|
||||||
request: Request,
|
request: ExtendedRequest,
|
||||||
_server: BunServer,
|
_server: Server,
|
||||||
requestBody: unknown,
|
requestBody: unknown,
|
||||||
query: Query,
|
|
||||||
params: Params,
|
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const { booru } = params as { booru: string };
|
const { booru } = request.params as { booru: string };
|
||||||
const {
|
const {
|
||||||
page = 0,
|
page = 0,
|
||||||
tags,
|
tags,
|
||||||
|
@ -182,7 +185,12 @@ async function handler(
|
||||||
parts.push("&");
|
parts.push("&");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGelbooru && gelbooruAuth) {
|
if (
|
||||||
|
isGelbooru &&
|
||||||
|
gelbooruAuth &&
|
||||||
|
gelbooruAuth.apiKey &&
|
||||||
|
gelbooruAuth.userId
|
||||||
|
) {
|
||||||
parts.push("api_key");
|
parts.push("api_key");
|
||||||
parts.push(gelbooruAuth.apiKey);
|
parts.push(gelbooruAuth.apiKey);
|
||||||
parts.push("&");
|
parts.push("&");
|
||||||
|
@ -200,7 +208,7 @@ async function handler(
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let headers: Record<string, string> | undefined;
|
let headers: Record<string, string> = {};
|
||||||
|
|
||||||
if (isE621) {
|
if (isE621) {
|
||||||
const e621Auth: Record<string, string> | null = getE621Auth(
|
const e621Auth: Record<string, string> | null = getE621Auth(
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { RouteDef } from "#types/routes";
|
||||||
|
|
||||||
const routeDef: RouteDef = {
|
const routeDef: RouteDef = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
accepts: "*/*",
|
accepts: "*/*",
|
||||||
|
|
165
src/server.ts
165
src/server.ts
|
@ -1,6 +1,12 @@
|
||||||
import { environment } from "@config/environment";
|
import { resolve } from "node:path";
|
||||||
import { logger } from "@helpers/logger";
|
import { type Echo, echo } from "@atums/echo";
|
||||||
import { FileSystemRouter, type MatchedRoute, type Serve } from "bun";
|
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 {
|
class ServerHandler {
|
||||||
private router: FileSystemRouter;
|
private router: FileSystemRouter;
|
||||||
|
@ -11,28 +17,27 @@ class ServerHandler {
|
||||||
) {
|
) {
|
||||||
this.router = new FileSystemRouter({
|
this.router = new FileSystemRouter({
|
||||||
style: "nextjs",
|
style: "nextjs",
|
||||||
dir: "./src/routes",
|
dir: resolve("src", "routes"),
|
||||||
|
fileExtensions: [".ts"],
|
||||||
origin: `http://${this.host}:${this.port}`,
|
origin: `http://${this.host}:${this.port}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
const server: Serve = Bun.serve({
|
const server: Server = Bun.serve({
|
||||||
port: this.port,
|
port: this.port,
|
||||||
hostname: this.host,
|
hostname: this.host,
|
||||||
fetch: this.handleRequest.bind(this),
|
fetch: this.handleRequest.bind(this),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
noFileLog.info(
|
||||||
`Server running at http://${server.hostname}:${server.port}`,
|
`Server running at http://${server.hostname}:${server.port}`,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
this.logRoutes(noFileLog);
|
||||||
this.logRoutes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private logRoutes(): void {
|
private logRoutes(echo: Echo): void {
|
||||||
logger.info("Available routes:");
|
echo.info("Available routes:");
|
||||||
|
|
||||||
const sortedRoutes: [string, string][] = Object.entries(
|
const sortedRoutes: [string, string][] = Object.entries(
|
||||||
this.router.routes,
|
this.router.routes,
|
||||||
|
@ -41,17 +46,76 @@ class ServerHandler {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [path, filePath] of sortedRoutes) {
|
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(
|
private async handleRequest(
|
||||||
request: Request,
|
request: Request,
|
||||||
server: BunServer,
|
server: Server,
|
||||||
): Promise<Response> {
|
): 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);
|
const match: MatchedRoute | null = this.router.match(request);
|
||||||
let requestBody: unknown = {};
|
let requestBody: unknown = {};
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const { filePath, params, query } = match;
|
const { filePath, params, query } = match;
|
||||||
|
@ -60,7 +124,7 @@ class ServerHandler {
|
||||||
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;
|
: null;
|
||||||
|
|
||||||
if (
|
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(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 405,
|
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 },
|
{ status: 405 },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const expectedContentType: string | null =
|
const expectedContentType: string | string[] | null =
|
||||||
routeModule.routeDef.accepts;
|
routeModule.routeDef.accepts;
|
||||||
|
|
||||||
const matchesAccepts: boolean =
|
let matchesAccepts: boolean;
|
||||||
|
|
||||||
|
if (Array.isArray(expectedContentType)) {
|
||||||
|
matchesAccepts =
|
||||||
|
expectedContentType.includes("*/*") ||
|
||||||
|
expectedContentType.includes(actualContentType || "");
|
||||||
|
} else {
|
||||||
|
matchesAccepts =
|
||||||
expectedContentType === "*/*" ||
|
expectedContentType === "*/*" ||
|
||||||
actualContentType === expectedContentType;
|
actualContentType === expectedContentType;
|
||||||
|
}
|
||||||
|
|
||||||
if (!matchesAccepts) {
|
if (!matchesAccepts) {
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
code: 406,
|
code: 406,
|
||||||
error: `Content-Type ${contentType} Not Acceptable`,
|
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
|
||||||
|
Array.isArray(expectedContentType)
|
||||||
|
? expectedContentType.join(", ")
|
||||||
|
: expectedContentType
|
||||||
|
}`,
|
||||||
},
|
},
|
||||||
{ status: 406 },
|
{ status: 406 },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
extendedRequest.params = params;
|
||||||
|
extendedRequest.query = query;
|
||||||
|
|
||||||
response = await routeModule.handler(
|
response = await routeModule.handler(
|
||||||
request,
|
extendedRequest,
|
||||||
server,
|
server,
|
||||||
requestBody,
|
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) {
|
} catch (error: unknown) {
|
||||||
logger.error(`Error handling route ${request.url}:`);
|
echo.error({
|
||||||
logger.error(error as Error);
|
message: `Error handling route ${request.url}`,
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
|
||||||
response = Response.json(
|
response = Response.json(
|
||||||
{
|
{
|
||||||
|
@ -145,27 +238,11 @@ class ServerHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers: Headers = response.headers;
|
this.logRequest(extendedRequest, response, ip);
|
||||||
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"})`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverHandler: ServerHandler = new ServerHandler(
|
const serverHandler: ServerHandler = new ServerHandler(
|
||||||
environment.port,
|
environment.port,
|
||||||
environment.host,
|
environment.host,
|
||||||
|
|
|
@ -2,33 +2,29 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"#*": ["src/*"],
|
||||||
"@config/*": ["config/*"],
|
"#types/*": ["types/*"],
|
||||||
"@types/*": ["types/*"],
|
"#environment": ["config/index.ts"],
|
||||||
"@helpers/*": ["src/helpers/*"],
|
"#environment/*": ["config/*"]
|
||||||
"@database/*": ["src/database/*"]
|
|
||||||
},
|
},
|
||||||
"typeRoots": ["./src/types", "./node_modules/@types"],
|
"typeRoots": ["./node_modules/@types"],
|
||||||
// Enable latest features
|
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"allowJs": false,
|
||||||
"allowJs": true,
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": false,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
// Some stricter flags (disabled by default)
|
"noUnusedLocals": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedParameters": true,
|
||||||
"noUnusedParameters": false,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"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;
|
functions: IBooruDefaults;
|
||||||
autocomplete?: string;
|
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 = {
|
type RouteDef = {
|
||||||
method: string;
|
method: string;
|
||||||
accepts: string | null;
|
accepts: string | null;
|
||||||
|
@ -11,10 +13,10 @@ type Params = Record<string, string>;
|
||||||
type RouteModule = {
|
type RouteModule = {
|
||||||
handler: (
|
handler: (
|
||||||
request: Request,
|
request: Request,
|
||||||
server: BunServer,
|
server: Server,
|
||||||
requestBody: unknown,
|
requestBody: unknown,
|
||||||
query: Query,
|
|
||||||
params: Params,
|
|
||||||
) => Promise<Response> | Response;
|
) => Promise<Response> | Response;
|
||||||
routeDef: RouteDef;
|
routeDef: RouteDef;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { RouteDef, Query, Params, RouteModule };
|
Loading…
Add table
Add a link
Reference in a new issue