-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refa: migrate most small booru to subpackages of core
- Loading branch information
Showing
34 changed files
with
727 additions
and
645 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,48 @@ | |
"lib", | ||
"dist" | ||
], | ||
"exports": { | ||
"./danbooru": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/danbooru/index.js", | ||
"default": "./lib/sources/danbooru/index.js" | ||
}, | ||
"./e621": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/e621/index.js", | ||
"default": "./lib/sources/e621/index.js" | ||
}, | ||
"./gelbooru": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/gelbooru/index.js", | ||
"default": "./lib/sources/gelbooru/index.js" | ||
}, | ||
"./konachan": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/konachan/index.js", | ||
"default": "./lib/sources/konachan/index.js" | ||
}, | ||
"./lolibooru": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/lolibooru/index.js", | ||
"default": "./lib/sources/lolibooru/index.js" | ||
}, | ||
"./safebooru": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/safebooru/index.js", | ||
"default": "./lib/sources/safebooru/index.js" | ||
}, | ||
"./sankaku": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/sankaku/index.js", | ||
"default": "./lib/sources/sankaku/index.js" | ||
}, | ||
"./yande": { | ||
"types": "./lib/sources/danbooru/index.d.ts", | ||
"require": "./lib/sources/yande/index.js", | ||
"default": "./lib/sources/yande/index.js" | ||
} | ||
}, | ||
"author": "Shigma <[email protected]>", | ||
"license": "MIT", | ||
"repository": { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Context, Schema, trimSlash } from 'koishi' | ||
import { ImageSource } from '../../source' | ||
import { Danbooru } from './types' | ||
|
||
class DanbooruImageSource extends ImageSource<DanbooruImageSource.Config> { | ||
languages = ['en'] | ||
source = 'danbooru' | ||
|
||
constructor(ctx: Context, config: DanbooruImageSource.Config) { | ||
super(ctx, config) | ||
} | ||
|
||
get keyPair() { | ||
if (!this.config.keyPairs.length) return | ||
return this.config.keyPairs[Math.floor(Math.random() * this.config.keyPairs.length)] | ||
} | ||
|
||
async get(query: ImageSource.Query): Promise<ImageSource.Result[]> { | ||
const keyPair = this.keyPair | ||
const data = await this.http.get<Danbooru.Post[]>(trimSlash(this.config.endpoint) + '/posts.json', { | ||
params: { | ||
tags: query.tags.join(' '), | ||
random: true, | ||
limit: query.count, | ||
...(keyPair ? { login: keyPair.login, api_key: keyPair.apiKey } : {}), | ||
}, | ||
}) | ||
|
||
if (!Array.isArray(data)) { | ||
return | ||
} | ||
|
||
return data.map((post) => { | ||
return { | ||
url: post.file_url, | ||
pageUrl: post.source, | ||
author: post.tag_string_artist.replace(/ /g, ', ').replace(/_/g, ' '), | ||
tags: post.tag_string.split(' ').map((t) => t.replace(/_/g, ' ')), | ||
nsfw: post.rating === 'e' || post.rating === 'q', | ||
} | ||
}) | ||
} | ||
} | ||
|
||
namespace DanbooruImageSource { | ||
export interface Config extends ImageSource.Config { | ||
endpoint: string | ||
keyPairs: { login: string; apiKey: string }[] | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.intersect([ | ||
ImageSource.createSchema({ label: 'danbooru' }), | ||
Schema.object({ | ||
endpoint: Schema.string().description('Danbooru 的 URL。').default('https://danbooru.donmai.us/'), | ||
/** | ||
* @see https://danbooru.donmai.us/wiki_pages/help%3Aapi | ||
*/ | ||
keyPairs: Schema.array( | ||
Schema.object({ | ||
login: Schema.string().required().description('用户名。'), | ||
apiKey: Schema.string().required().role('secret').description('API 密钥。'), | ||
}), | ||
).description( | ||
'API 密钥对。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/danbooru.html#获取与设置登录凭据)', | ||
), | ||
}).description('搜索设置'), | ||
]) | ||
} | ||
|
||
export default DanbooruImageSource |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { Context, Quester, Schema, trimSlash } from 'koishi' | ||
import { ImageSource } from '../../source' | ||
import { e621 } from './types' | ||
|
||
class e621ImageSource extends ImageSource<e621ImageSource.Config> { | ||
languages = ['en'] | ||
source = 'e621' | ||
http: Quester | ||
|
||
constructor(ctx: Context, config: e621ImageSource.Config) { | ||
super(ctx, config) | ||
this.http = this.http.extend({ | ||
headers: { | ||
'User-Agent': config.userAgent, | ||
}, | ||
}) | ||
} | ||
|
||
get keyPair() { | ||
if (!this.config.keyPairs.length) return | ||
return this.config.keyPairs[Math.floor(Math.random() * this.config.keyPairs.length)] | ||
} | ||
|
||
async get(query: ImageSource.Query): Promise<ImageSource.Result[]> { | ||
if (!query.tags.find((t) => t.startsWith('order:'))) query.tags.push('order:random') | ||
const keyPair = this.keyPair | ||
const data = await this.http.get<{ | ||
posts: e621.Post[] | ||
}>(trimSlash(this.config.endpoint) + '/posts.json', { | ||
params: { | ||
tags: query.tags.join(' '), | ||
limit: query.count, | ||
}, | ||
headers: keyPair | ||
? { Authorization: 'Basic ' + Buffer.from(`${keyPair.login}:${keyPair.apiKey}`).toString('base64') } | ||
: {}, | ||
}) | ||
|
||
if (!Array.isArray(data.posts)) { | ||
return | ||
} | ||
|
||
return data.posts.map((post) => { | ||
return { | ||
url: post.file.url, | ||
pageUrl: trimSlash(this.config.endpoint) + `/post/${post.id}`, | ||
author: post.tags.artist.join(', '), | ||
tags: Object.values(post.tags).flat(), | ||
nsfw: post.rating !== 's', | ||
desc: post.description, | ||
} | ||
}) | ||
} | ||
} | ||
|
||
namespace e621ImageSource { | ||
export interface Config extends ImageSource.Config { | ||
endpoint: string | ||
keyPairs: { login: string; apiKey: string }[] | ||
userAgent: string | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.intersect([ | ||
ImageSource.createSchema({ label: 'e621' }), | ||
Schema.object({ | ||
endpoint: Schema.string().description('e621/e926 的 URL。').default('https://e621.net/'), | ||
keyPairs: Schema.array( | ||
Schema.object({ | ||
login: Schema.string().required().description('e621/e926 的用户名。'), | ||
apiKey: Schema.string().required().role('secret').description('e621/e926 的 API Key。'), | ||
}), | ||
) | ||
.default([]) | ||
.description('e621/e926 的登录凭据。'), | ||
userAgent: Schema.string() | ||
.description('设置请求的 User Agent。') | ||
.default( | ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.37', | ||
), | ||
}).description('搜索设置'), | ||
]) | ||
} | ||
|
||
export default e621ImageSource |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Context, Schema, trimSlash } from 'koishi' | ||
import { ImageSource } from '../../source' | ||
import { Gelbooru } from './types' | ||
|
||
class GelbooruImageSource extends ImageSource<GelbooruImageSource.Config> { | ||
languages = ['en'] | ||
source = 'gelbooru' | ||
|
||
constructor(ctx: Context, config: GelbooruImageSource.Config) { | ||
super(ctx, config) | ||
} | ||
|
||
get keyPair() { | ||
if (!this.config.keyPairs.length) return | ||
return this.config.keyPairs[Math.floor(Math.random() * this.config.keyPairs.length)] | ||
} | ||
|
||
async get(query: ImageSource.Query): Promise<ImageSource.Result[]> { | ||
// API docs: https://gelbooru.com/index.php?page=help&topic=dapi | ||
const params = { | ||
tags: query.tags.join('+') + "+sort:random", | ||
page: 'dapi', | ||
s: 'post', | ||
q: "index", | ||
json: 1, | ||
limit: query.count | ||
} | ||
let url = trimSlash(this.config.endpoint) + '?' + Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&') | ||
|
||
const keyPair = this.keyPair | ||
if (keyPair) { | ||
// The keyPair from Gelbooru is already url-encoded. | ||
url += keyPair | ||
} | ||
|
||
const data = await this.http.get<Gelbooru.Response>(url) | ||
|
||
if (!Array.isArray(data.post)) { | ||
return | ||
} | ||
|
||
return data.post.map((post) => { | ||
return { | ||
url: post.file_url, | ||
pageUrl: post.source, | ||
author: post.owner.replace(/ /g, ', ').replace(/_/g, ' '), | ||
tags: post.tags.split(' ').map((t) => t.replace(/_/g, ' ')), | ||
nsfw: ['explicit', 'questionable'].includes(post.rating), | ||
} | ||
}) | ||
} | ||
} | ||
|
||
namespace GelbooruImageSource { | ||
export interface Config extends ImageSource.Config { | ||
endpoint: string | ||
keyPairs: string[] | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.intersect([ | ||
ImageSource.createSchema({ label: 'gelbooru' }), | ||
Schema.object({ | ||
endpoint: Schema.string().description('Gelbooru 的 URL。').default('https://gelbooru.com/index.php'), | ||
keyPairs: Schema.array(Schema.string().required().role('secret')).description('Gelbooru 的登录凭据。').default([]), | ||
}).description('搜索设置'), | ||
]) | ||
} | ||
|
||
export default GelbooruImageSource |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { createHash } from 'node:crypto' | ||
import { Context, Dict, Schema, trimSlash } from 'koishi' | ||
import { ImageSource } from '../../source' | ||
import { Konachan } from './types' | ||
|
||
/** | ||
* Konachan requires a password hash for authentication. | ||
* | ||
* @see https://konachan.net/help/api | ||
*/ | ||
function hashPassword(password: string) { | ||
const salted = `So-I-Heard-You-Like-Mupkids-?--${password}--` | ||
// do a SHA1 hash of the salted password | ||
const hash = createHash('sha1') | ||
hash.update(salted) | ||
return hash.digest('hex') | ||
} | ||
|
||
class KonachanImageSource extends ImageSource<KonachanImageSource.Config> { | ||
languages = ['en'] | ||
source = 'konachan' | ||
|
||
constructor(ctx: Context, config: KonachanImageSource.Config) { | ||
super(ctx, config) | ||
} | ||
|
||
get keyPair() { | ||
if (!this.config.keyPairs.length) return | ||
const key = this.config.keyPairs[Math.floor(Math.random() * this.config.keyPairs.length)] | ||
return { | ||
login: key.login, | ||
password_hash: hashPassword(key.password), | ||
} | ||
} | ||
|
||
async get(query: ImageSource.Query): Promise<ImageSource.Result[]> { | ||
// API docs: https://konachan.net/help/api and https://konachan.com/help/api | ||
const params: Dict<string> = { | ||
tags: query.tags.join('+') + '+order:random', | ||
limit: `${query.count}`, | ||
} | ||
let url = trimSlash(this.config.endpoint) + '/post.json' | ||
|
||
const keyPair = this.keyPair | ||
if (keyPair) { | ||
params['login'] = keyPair.login | ||
params['password_hash'] = keyPair.password_hash | ||
} | ||
const data = await this.http.get<Konachan.Response[]>(url, { params: new URLSearchParams(params) }) | ||
|
||
if (!Array.isArray(data)) { | ||
return | ||
} | ||
|
||
return data.map((post) => { | ||
return { | ||
url: post.file_url, | ||
pageUrl: post.source, | ||
author: post.author.replace(/ /g, ', ').replace(/_/g, ' '), | ||
tags: post.tags.split(' ').map((t) => t.replace(/_/g, ' ')), | ||
nsfw: ['e', 'q'].includes(post.rating), | ||
} | ||
}) | ||
} | ||
} | ||
|
||
namespace KonachanImageSource { | ||
export interface Config extends ImageSource.Config { | ||
endpoint: string | ||
keyPairs: { login: string; password: string }[] | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.intersect([ | ||
ImageSource.createSchema({ label: 'konachan' }), | ||
Schema.object({ | ||
endpoint: Schema.union([ | ||
Schema.const('https://konachan.com/').description('Konachan.com (NSFW)'), | ||
Schema.const('https://konachan.net/').description('Konachan.net (SFW)'), | ||
]) | ||
.description('Konachan 的 URL。') | ||
.default('https://konachan.com/'), | ||
keyPairs: Schema.array( | ||
Schema.object({ | ||
login: Schema.string().required().description('用户名'), | ||
password: Schema.string().required().role('secret').description('密码'), | ||
}), | ||
).description('Konachan 的登录凭据。'), | ||
}).description('搜索设置'), | ||
]) | ||
} | ||
|
||
export default KonachanImageSource |
File renamed without changes.
Oops, something went wrong.