Skip to content

Commit

Permalink
refa: migrate most small booru to subpackages of core
Browse files Browse the repository at this point in the history
  • Loading branch information
MaikoTan committed Mar 21, 2024
1 parent bd17daf commit cbfb751
Show file tree
Hide file tree
Showing 34 changed files with 749 additions and 667 deletions.
42 changes: 42 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/sources/danbooru/index.ts
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.
84 changes: 84 additions & 0 deletions packages/core/src/sources/e621/index.ts
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.
76 changes: 76 additions & 0 deletions packages/core/src/sources/gelbooru/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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.
92 changes: 92 additions & 0 deletions packages/core/src/sources/konachan/index.ts
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.
Loading

0 comments on commit cbfb751

Please sign in to comment.