diff --git a/packages/core/package.json b/packages/core/package.json index 242688cdf..7a51d49aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,6 +8,45 @@ "lib", "dist" ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json", + "./danbooru": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/danbooru/index.js" + }, + "./e621": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/e621/index.js" + }, + "./gelbooru": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/gelbooru/index.js" + }, + "./konachan": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/konachan/index.js" + }, + "./lolibooru": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/lolibooru/index.js" + }, + "./safebooru": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/safebooru/index.js" + }, + "./sankaku": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/sankaku/index.js" + }, + "./yande": { + "types": "./lib/sources/danbooru/index.d.ts", + "default": "./lib/sources/yande/index.js" + } + }, "author": "Shigma ", "license": "MIT", "repository": { diff --git a/packages/core/src/sources/danbooru/index.ts b/packages/core/src/sources/danbooru/index.ts new file mode 100644 index 000000000..8318046b0 --- /dev/null +++ b/packages/core/src/sources/danbooru/index.ts @@ -0,0 +1,77 @@ +import { Context, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { Danbooru } from './types' + +class DanbooruImageSource extends ImageSource { + 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 { + const keyPair = this.keyPair + const data = await this.http.get(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 { + // Size: file_url > large_file_url > preview_file_url + urls: { + original: post.file_url, + large: post.large_file_url, + thumbnail: post.preview_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 = 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 diff --git a/packages/danbooru/src/types.ts b/packages/core/src/sources/danbooru/types.ts similarity index 100% rename from packages/danbooru/src/types.ts rename to packages/core/src/sources/danbooru/types.ts diff --git a/packages/core/src/sources/e621/index.ts b/packages/core/src/sources/e621/index.ts new file mode 100644 index 000000000..ab8831b83 --- /dev/null +++ b/packages/core/src/sources/e621/index.ts @@ -0,0 +1,92 @@ +import { Context, Quester, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { e621 } from './types' + +class e621ImageSource extends ImageSource { + 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 { + 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 { + // Size: file > sample > preview + urls: { + original: post.file.url, + medium: post.sample.url, + thumbnail: post.preview.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 = 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 的登录凭据。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/e621.html#configure-credentials)', + ), + userAgent: Schema.string().description('设置请求的 User Agent。').default( + // eslint-disable-next-line max-len + '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 diff --git a/packages/e621/src/types.ts b/packages/core/src/sources/e621/types.ts similarity index 100% rename from packages/e621/src/types.ts rename to packages/core/src/sources/e621/types.ts diff --git a/packages/core/src/sources/gelbooru/index.ts b/packages/core/src/sources/gelbooru/index.ts new file mode 100644 index 000000000..f03ce26b1 --- /dev/null +++ b/packages/core/src/sources/gelbooru/index.ts @@ -0,0 +1,85 @@ +import { Context, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { Gelbooru } from './types' + +class GelbooruImageSource extends ImageSource { + 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 { + // 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(url) + + if (!Array.isArray(data.post)) { + return + } + + return data.post.map((post) => { + return { + // Size: file_url > sample_url > preview_url + urls: { + original: post.file_url, + medium: post.sample_url, + thumbnail: post.preview_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 = 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 的登录凭据。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/gelbooru.html#configure-credentials)', + ) + .default([]), + }).description('搜索设置'), + ]) +} + +export default GelbooruImageSource diff --git a/packages/gelbooru/src/types.ts b/packages/core/src/sources/gelbooru/types.ts similarity index 100% rename from packages/gelbooru/src/types.ts rename to packages/core/src/sources/gelbooru/types.ts diff --git a/packages/core/src/sources/konachan/index.ts b/packages/core/src/sources/konachan/index.ts new file mode 100644 index 000000000..a32dcb26d --- /dev/null +++ b/packages/core/src/sources/konachan/index.ts @@ -0,0 +1,102 @@ +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 { + 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 { + // API docs: https://konachan.net/help/api and https://konachan.com/help/api + const params: Dict = { + tags: query.tags.join('+') + '+order:random', + limit: `${query.count}`, + } + + const 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(url, { params: new URLSearchParams(params) }) + + if (!Array.isArray(data)) { + return + } + + return data.map((post) => { + return { + urls: { + original: post.file_url, + medium: post.sample_url, + thumbnail: post.preview_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 = 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('密码'), + }), + ) + .default([]) + .description('Konachan 的登录凭据。'), + }).description('搜索设置'), + ]) +} + +export default KonachanImageSource diff --git a/packages/konachan/src/types.ts b/packages/core/src/sources/konachan/types.ts similarity index 100% rename from packages/konachan/src/types.ts rename to packages/core/src/sources/konachan/types.ts diff --git a/packages/core/src/sources/lolibooru/index.ts b/packages/core/src/sources/lolibooru/index.ts new file mode 100644 index 000000000..68f6425a7 --- /dev/null +++ b/packages/core/src/sources/lolibooru/index.ts @@ -0,0 +1,99 @@ +import { createHash } from 'node:crypto' + +import { Context, Dict, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { Lolibooru } from './types' +/** + * Lolibooru requires a password hash for authentication. + * + * @see https://lolibooru.moe/help/api + */ +function hashPassword(password: string) { + const salted = `--${password}--` + // do a SHA1 hash of the salted password + const hash = createHash('sha1') + hash.update(salted) + return hash.digest('hex') +} + +class LolibooruImageSource extends ImageSource { + languages = ['en'] + source = 'lolibooru' + + constructor(ctx: Context, config: LolibooruImageSource.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 { + // API docs: https://lolibooru.moe/help/api + const params: Dict = { + tags: query.tags.join('+') + '+order:random', + limit: `${query.count}`, + } + + const url = trimSlash(this.config.endpoint) + '/post/index.json' + + const keyPair = this.keyPair + if (keyPair) { + params['login'] = keyPair.login + params['password_hash'] = keyPair.password_hash + } + const data = await this.http.get(url, { params: new URLSearchParams(params) }) + + if (!Array.isArray(data)) { + return + } + + return data.map((post) => { + return { + // Since lolibooru returns URL that contains white spaces that are not transformed + // into `%20`, which breaks in go-cqhttp who cannot resolve to a valid URL. + // Fixes: https://github.com/koishijs/koishi-plugin-booru/issues/95 + urls: { + original: encodeURI(post.file_url), + medium: encodeURI(post.sample_url), + thumbnail: encodeURI(post.preview_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 LolibooruImageSource { + export interface Config extends ImageSource.Config { + endpoint: string + keyPairs: { login: string; password: string }[] + } + + export const Config: Schema = Schema.intersect([ + ImageSource.createSchema({ label: 'lolibooru' }), + Schema.object({ + endpoint: Schema.string().description('Lolibooru 的 URL。').default('https://lolibooru.moe'), + keyPairs: Schema.array( + Schema.object({ + login: Schema.string().required().description('用户名'), + password: Schema.string().required().role('secret').description('密码'), + }), + ) + .default([]) + .description('Lolibooru 的登录凭据。'), + }).description('搜索设置'), + ]) +} + +export default LolibooruImageSource diff --git a/packages/lolibooru/src/types.ts b/packages/core/src/sources/lolibooru/types.ts similarity index 100% rename from packages/lolibooru/src/types.ts rename to packages/core/src/sources/lolibooru/types.ts diff --git a/packages/core/src/sources/safebooru/index.ts b/packages/core/src/sources/safebooru/index.ts new file mode 100644 index 000000000..291d1f8d2 --- /dev/null +++ b/packages/core/src/sources/safebooru/index.ts @@ -0,0 +1,73 @@ +import { Context, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { Safebooru } from './types' + +class SafebooruImageSource extends ImageSource { + languages = ['en'] + source = 'safebooru' + + constructor(ctx: Context, config: SafebooruImageSource.Config) { + super(ctx, config) + } + + async get(query: ImageSource.Query): Promise { + // API docs: https://safebooru.org/index.php?page=help&topic=dapi + const params = { + // TODO random 无效 + tags: query.tags.join('+') + '+sort:random', + page: 'dapi', + s: 'post', + q: 'index', + json: 1, + limit: query.count, + } + const url = + trimSlash(this.config.endpoint) + + '?' + + Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join('&') + + const data = await this.http.get(url) + + if (!Array.isArray(data)) { + return + } + + return data.map((post) => { + return { + // Safebooru didn't straightly provide image urls, so we should construct them manually. + // `sample` url only exists when the image is too large, in that case, `post.sample` + // would be `true`, and then we could construct the sample url. + urls: { + original: `https://safebooru.org/images/${post.directory}/${post.image}?${post.id}`, + large: post.sample + ? `https://safebooru.org/samples/${post.directory}/sample_${post.image}?${post.id}` + : undefined, + thumbnail: `https://safebooru.org/thumbnails/${post.directory}/thumbnail_${post.image}?${post.id}`, + }, + // pageUrl: post.source, + author: post.owner.replace(/ /g, ', ').replace(/_/g, ' '), + tags: post.tags.split(' ').map((t) => t.replace(/_/g, ' ')), + nsfw: !['safe', 'general'].includes(post.rating), + } + }) + } +} + +namespace SafebooruImageSource { + export interface Config extends ImageSource.Config { + endpoint: string + } + + export const Config: Schema = Schema.intersect([ + ImageSource.createSchema({ label: 'safebooru' }), + Schema.object({ + endpoint: Schema.string().description('Safebooru 的 URL。').default('https://safebooru.org/index.php'), + }).description('搜索设置'), + ]) +} + +export default SafebooruImageSource diff --git a/packages/safebooru/src/types.ts b/packages/core/src/sources/safebooru/types.ts similarity index 100% rename from packages/safebooru/src/types.ts rename to packages/core/src/sources/safebooru/types.ts diff --git a/packages/sankaku/src/constants.ts b/packages/core/src/sources/sankaku/constants.ts similarity index 100% rename from packages/sankaku/src/constants.ts rename to packages/core/src/sources/sankaku/constants.ts diff --git a/packages/core/src/sources/sankaku/index.ts b/packages/core/src/sources/sankaku/index.ts new file mode 100644 index 000000000..1474260b7 --- /dev/null +++ b/packages/core/src/sources/sankaku/index.ts @@ -0,0 +1,116 @@ +import { Context, Schema } from 'koishi' + +import { ImageSource } from '../../source' + +import * as consts from './constants' +import { SankakuComplex } from './types' + +class SankakuComplexImageSource extends ImageSource { + languages = ['en'] + source = 'sankaku' + + constructor(ctx: Context, config: SankakuComplexImageSource.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 { + // API docs: https://chan.sankakucomplex.com/cn/help/api (Link not available) + const params = { + tags: [...query.tags, 'order:random'].join('+'), + limit: `${query.count}`, + } + + const keyPair = this.keyPair + if (!keyPair.accessToken) { + await this._login(keyPair) + } + + const data = await this.http.get(consts.POSTS_URL, { + params, + headers: keyPair.accessToken ? { Authentication: `${keyPair.tokenType} ${keyPair.accessToken}` } : {}, + }) + console.log(data) + + if (!Array.isArray(data)) return + + return data.map((post) => { + return { + urls: { + original: post.file_url, + medium: post.sample_url, + thumbnail: post.preview_url, + }, + pageUrl: post.source, + author: post.author.name.replace(/ /g, ', ').replace(/_/g, ' '), + tags: post.tags.map((t) => t.name.replace(/_/g, ' ')), + nsfw: ['e', 'q'].includes(post.rating), + } + }) + } + + async _login(keyPair: SankakuComplexImageSource.KeyPair) { + if (!keyPair.accessToken) { + const data = await this.http.post(consts.LOGIN_URL, { + login: keyPair.login, + password: keyPair.password, + }) + console.log(data) + + if (data.access_token) { + keyPair.accessToken = data.access_token + keyPair.tokenType = data.token_type + + this.ctx.setTimeout(() => { + this.ctx.scope.update(this.config) + }, 0) + } + } + return keyPair + } +} + +namespace SankakuComplexImageSource { + export interface KeyPair { + login?: string + password?: string + tokenType?: string + accessToken?: string + } + + export interface Config extends ImageSource.Config { + keyPairs: KeyPair[] + userAgent: string + } + + export const Config: Schema = Schema.intersect([ + ImageSource.createSchema({ label: 'sankaku' }), + Schema.object({ + keyPairs: Schema.array( + Schema.object({ + login: Schema.string().required().description('SankakuComplex 用户名'), + password: Schema.string().required().role('secret').description('SankakuComplex 密码'), + tokenType: Schema.string().hidden().default('Bearer').description('SankakuComplex 访问令牌类型'), + accessToken: Schema.string().hidden().description('SankakuComplex 访问令牌'), + }), + ) + .default([]) + .description('SankakuComplex 的登录凭证'), + userAgent: Schema.string().description('设置请求的 User Agent。').default( + // eslint-disable-next-line max-len + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + ), + }).description('搜索设置'), + ]) +} + +export default SankakuComplexImageSource diff --git a/packages/sankaku/src/types.ts b/packages/core/src/sources/sankaku/types.ts similarity index 100% rename from packages/sankaku/src/types.ts rename to packages/core/src/sources/sankaku/types.ts diff --git a/packages/core/src/sources/yande/index.ts b/packages/core/src/sources/yande/index.ts new file mode 100644 index 000000000..84094f980 --- /dev/null +++ b/packages/core/src/sources/yande/index.ts @@ -0,0 +1,100 @@ +import { createHash } from 'node:crypto' + +import { Context, Schema, trimSlash } from 'koishi' + +import { ImageSource } from '../../source' + +import { Yande } from './types' + +/** + * Yande.re requires a password hash for authentication. + * + * @see https://yande.re/help/api + */ +function hashPassword(password: string) { + const salted = `choujin-steiner--${password}--` + // do a SHA1 hash of the salted password + const hash = createHash('sha1') + hash.update(salted) + return hash.digest('hex') +} + +class YandeImageSource extends ImageSource { + languages = ['en'] + source = 'yande' + + constructor(ctx: Context, config: YandeImageSource.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 { + // API docs: https://yande.re/help/api + const params = { + tags: [...query.tags, 'order:random'].join('+'), + limit: query.count, + } + const 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(url, { params }) + + if (!Array.isArray(data)) { + return + } + + return data.map((post) => { + return { + urls: { + original: post.file_url, + medium: post.sample_url, + thumbnail: post.preview_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 YandeImageSource { + export interface Config extends ImageSource.Config { + endpoint: string + keyPairs: { + login: string + password: string + }[] + } + + export const Config: Schema = Schema.intersect([ + ImageSource.createSchema({ label: 'yande' }), + Schema.object({ + endpoint: Schema.string().description('Yande.re 的 URL。').default('https://yande.re'), + keyPairs: Schema.array( + Schema.object({ + login: Schema.string().required().description('Yande.re 的用户名。'), + password: Schema.string().required().role('secret').description('Yande.re 的密码。'), + }), + ) + .default([]) + .description('Yande.re 的登录凭据。'), + }).description('搜索设置'), + ]) +} + +export default YandeImageSource diff --git a/packages/yande/src/types.ts b/packages/core/src/sources/yande/types.ts similarity index 100% rename from packages/yande/src/types.ts rename to packages/core/src/sources/yande/types.ts diff --git a/packages/danbooru/src/index.ts b/packages/danbooru/src/index.ts index 214beee34..4b2d8736b 100644 --- a/packages/danbooru/src/index.ts +++ b/packages/danbooru/src/index.ts @@ -1,74 +1,3 @@ -import { Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import { Danbooru } from './types' - -class DanbooruImageSource extends ImageSource { - languages = ['en'] - source = 'danbooru' - - 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 { - const keyPair = this.keyPair - const data = await this.http.get(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 { - // Size: file_url > large_file_url > preview_file_url - urls: { - original: post.file_url, - large: post.large_file_url, - thumbnail: post.preview_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 = 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 密钥。'), - }), - ) - .default([]) - .description( - 'Danbooru 的登录凭据。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/danbooru.html#configure-credentials)', - ), - }).description('搜索设置'), - ]) -} +import DanbooruImageSource from 'koishi-plugin-booru/danbooru' export default DanbooruImageSource diff --git a/packages/danbooru/tsconfig.json b/packages/danbooru/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/danbooru/tsconfig.json +++ b/packages/danbooru/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/e621/src/index.ts b/packages/e621/src/index.ts index 9893ffc01..fbed31ab8 100644 --- a/packages/e621/src/index.ts +++ b/packages/e621/src/index.ts @@ -1,91 +1,3 @@ -import { Context, Quester, Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' +import E621ImageSource from 'koishi-plugin-booru/e621' -import { e621 } from './types' - -class e621ImageSource extends ImageSource { - 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 { - 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 { - // Size: file > sample > preview - urls: { - original: post.file.url, - medium: post.sample.url, - thumbnail: post.preview.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 = 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 的登录凭据。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/e621.html#configure-credentials)', - ), - userAgent: Schema.string().description('设置请求的 User Agent。').default( - // eslint-disable-next-line max-len - '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 +export default E621ImageSource diff --git a/packages/e621/tsconfig.json b/packages/e621/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/e621/tsconfig.json +++ b/packages/e621/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/gelbooru/src/index.ts b/packages/gelbooru/src/index.ts index 465dd1b3d..6e2f6bb19 100644 --- a/packages/gelbooru/src/index.ts +++ b/packages/gelbooru/src/index.ts @@ -1,80 +1,3 @@ -import { Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import { Gelbooru } from './types' - -class GelbooruImageSource extends ImageSource { - languages = ['en'] - source = 'gelbooru' - - 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 { - // 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(url) - - if (!Array.isArray(data.post)) { - return - } - - return data.post.map((post) => { - return { - // Size: file_url > sample_url > preview_url - urls: { - original: post.file_url, - medium: post.sample_url, - thumbnail: post.preview_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 = 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 的登录凭据。[点击前往获取及设置教程](https://booru.koishi.chat/zh-CN/plugins/gelbooru.html#configure-credentials)', - ) - .default([]), - }).description('搜索设置'), - ]) -} +import GelbooruImageSource from 'koishi-plugin-booru/gelbooru' export default GelbooruImageSource diff --git a/packages/gelbooru/tsconfig.json b/packages/gelbooru/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/gelbooru/tsconfig.json +++ b/packages/gelbooru/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/konachan/src/index.ts b/packages/konachan/src/index.ts index ca038ce86..1c412dc61 100644 --- a/packages/konachan/src/index.ts +++ b/packages/konachan/src/index.ts @@ -1,96 +1,3 @@ -import { createHash } from 'node:crypto' - -import { Dict, Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -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 { - languages = ['en'] - source = 'konachan' - - 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 { - // API docs: https://konachan.net/help/api and https://konachan.com/help/api - const params: Dict = { - tags: query.tags.join('+') + '+order:random', - limit: `${query.count}`, - } - const 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(url, { params: new URLSearchParams(params) }) - - if (!Array.isArray(data)) { - return - } - - return data.map((post) => { - return { - urls: { - original: post.file_url, - medium: post.sample_url, - thumbnail: post.preview_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 = 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('密码'), - }), - ) - .default([]) - .description('Konachan 的登录凭据。'), - }).description('搜索设置'), - ]) -} +import KonachanImageSource from 'koishi-plugin-booru/konachan' export default KonachanImageSource diff --git a/packages/konachan/tsconfig.json b/packages/konachan/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/konachan/tsconfig.json +++ b/packages/konachan/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/lolibooru/src/index.ts b/packages/lolibooru/src/index.ts index eb4294a9b..bcaedee4f 100644 --- a/packages/lolibooru/src/index.ts +++ b/packages/lolibooru/src/index.ts @@ -1,94 +1,3 @@ -import { createHash } from 'node:crypto' - -import { Dict, Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import { Lolibooru } from './types' -/** - * Lolibooru requires a password hash for authentication. - * - * @see https://lolibooru.moe/help/api - */ -function hashPassword(password: string) { - const salted = `--${password}--` - // do a SHA1 hash of the salted password - const hash = createHash('sha1') - hash.update(salted) - return hash.digest('hex') -} - -class LolibooruImageSource extends ImageSource { - languages = ['en'] - source = 'lolibooru' - - 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 { - // API docs: https://lolibooru.moe/help/api - const params: Dict = { - tags: query.tags.join('+') + '+order:random', - limit: `${query.count}`, - } - - const url = trimSlash(this.config.endpoint) + '/post/index.json' - - const keyPair = this.keyPair - if (keyPair) { - params['login'] = keyPair.login - params['password_hash'] = keyPair.password_hash - } - const data = await this.http.get(url, { params: new URLSearchParams(params) }) - - if (!Array.isArray(data)) { - return - } - - return data.map((post) => { - return { - // Since lolibooru returns URL that contains white spaces that are not transformed - // into `%20`, which breaks in go-cqhttp who cannot resolve to a valid URL. - // Fixes: https://github.com/koishijs/koishi-plugin-booru/issues/95 - urls: { - original: encodeURI(post.file_url), - medium: encodeURI(post.sample_url), - thumbnail: encodeURI(post.preview_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 LolibooruImageSource { - export interface Config extends ImageSource.Config { - endpoint: string - keyPairs: { login: string; password: string }[] - } - - export const Config: Schema = Schema.intersect([ - ImageSource.createSchema({ label: 'lolibooru' }), - Schema.object({ - endpoint: Schema.string().description('Lolibooru 的 URL。').default('https://lolibooru.moe'), - keyPairs: Schema.array( - Schema.object({ - login: Schema.string().required().description('用户名'), - password: Schema.string().required().role('secret').description('密码'), - }), - ) - .default([]) - .description('Lolibooru 的登录凭据。'), - }).description('搜索设置'), - ]) -} +import LolibooruImageSource from 'koishi-plugin-booru/lolibooru' export default LolibooruImageSource diff --git a/packages/lolibooru/tsconfig.json b/packages/lolibooru/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/lolibooru/tsconfig.json +++ b/packages/lolibooru/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/safebooru/src/index.ts b/packages/safebooru/src/index.ts index dd6de2a10..6bd82ece2 100644 --- a/packages/safebooru/src/index.ts +++ b/packages/safebooru/src/index.ts @@ -1,68 +1,3 @@ -import { Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import { Safebooru } from './types' - -class SafebooruImageSource extends ImageSource { - languages = ['en'] - source = 'safebooru' - - async get(query: ImageSource.Query): Promise { - // API docs: https://safebooru.org/index.php?page=help&topic=dapi - const params = { - // TODO random 无效 - tags: query.tags.join('+') + '+sort:random', - page: 'dapi', - s: 'post', - q: 'index', - json: 1, - limit: query.count, - } - const url = - trimSlash(this.config.endpoint) + - '?' + - Object.entries(params) - .map(([key, value]) => `${key}=${value}`) - .join('&') - - const data = await this.http.get(url) - - if (!Array.isArray(data)) { - return - } - - return data.map((post) => { - return { - // Safebooru didn't straightly provide image urls, so we should construct them manually. - // `sample` url only exists when the image is too large, in that case, `post.sample` - // would be `true`, and then we could construct the sample url. - urls: { - original: `https://safebooru.org/images/${post.directory}/${post.image}?${post.id}`, - large: post.sample - ? `https://safebooru.org/samples/${post.directory}/sample_${post.image}?${post.id}` - : undefined, - thumbnail: `https://safebooru.org/thumbnails/${post.directory}/thumbnail_${post.image}?${post.id}`, - }, - // pageUrl: post.source, - author: post.owner.replace(/ /g, ', ').replace(/_/g, ' '), - tags: post.tags.split(' ').map((t) => t.replace(/_/g, ' ')), - nsfw: !['safe', 'general'].includes(post.rating), - } - }) - } -} - -namespace SafebooruImageSource { - export interface Config extends ImageSource.Config { - endpoint: string - } - - export const Config: Schema = Schema.intersect([ - ImageSource.createSchema({ label: 'safebooru' }), - Schema.object({ - endpoint: Schema.string().description('Safebooru 的 URL。').default('https://safebooru.org/index.php'), - }).description('搜索设置'), - ]) -} +import SafebooruImageSource from 'koishi-plugin-booru/safebooru' export default SafebooruImageSource diff --git a/packages/safebooru/tsconfig.json b/packages/safebooru/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/safebooru/tsconfig.json +++ b/packages/safebooru/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/sankaku/src/index.ts b/packages/sankaku/src/index.ts index ee555b572..3f5e9e920 100644 --- a/packages/sankaku/src/index.ts +++ b/packages/sankaku/src/index.ts @@ -1,115 +1,3 @@ -import { Context, Schema } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import * as consts from './constants' -import { SankakuComplex } from './types' - -class SankakuComplexImageSource extends ImageSource { - languages = ['en'] - source = 'sankaku' - - constructor(ctx: Context, config: SankakuComplexImageSource.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 { - // API docs: https://chan.sankakucomplex.com/cn/help/api (Link not available) - const params = { - tags: [...query.tags, 'order:random'].join('+'), - limit: `${query.count}`, - } - - const keyPair = this.keyPair - if (!keyPair.accessToken) { - await this._login(keyPair) - } - - const data = await this.http.get(consts.POSTS_URL, { - params, - headers: keyPair.accessToken ? { Authentication: `${keyPair.tokenType} ${keyPair.accessToken}` } : {}, - }) - console.log(data) - - if (!Array.isArray(data)) return - - return data.map((post) => { - return { - urls: { - original: post.file_url, - medium: post.sample_url, - thumbnail: post.preview_url, - }, - pageUrl: post.source, - author: post.author.name.replace(/ /g, ', ').replace(/_/g, ' '), - tags: post.tags.map((t) => t.name.replace(/_/g, ' ')), - nsfw: ['e', 'q'].includes(post.rating), - } - }) - } - - async _login(keyPair: SankakuComplexImageSource.KeyPair) { - if (!keyPair.accessToken) { - const data = await this.http.post(consts.LOGIN_URL, { - login: keyPair.login, - password: keyPair.password, - }) - console.log(data) - - if (data.access_token) { - keyPair.accessToken = data.access_token - keyPair.tokenType = data.token_type - - this.ctx.setTimeout(() => { - this.ctx.scope.update(this.config) - }, 0) - } - } - return keyPair - } -} - -namespace SankakuComplexImageSource { - export interface KeyPair { - login?: string - password?: string - tokenType?: string - accessToken?: string - } - - export interface Config extends ImageSource.Config { - keyPairs: KeyPair[] - userAgent: string - } - - export const Config: Schema = Schema.intersect([ - ImageSource.createSchema({ label: 'sankaku' }), - Schema.object({ - keyPairs: Schema.array( - Schema.object({ - login: Schema.string().required().description('SankakuComplex 用户名'), - password: Schema.string().required().role('secret').description('SankakuComplex 密码'), - tokenType: Schema.string().hidden().default('Bearer').description('SankakuComplex 访问令牌类型'), - accessToken: Schema.string().hidden().description('SankakuComplex 访问令牌'), - }), - ) - .default([]) - .description('SankakuComplex 的登录凭证'), - userAgent: Schema.string().description('设置请求的 User Agent。').default( - // eslint-disable-next-line max-len - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', - ), - }).description('搜索设置'), - ]) -} +import SankakuComplexImageSource from 'koishi-plugin-booru/sankaku' export default SankakuComplexImageSource diff --git a/packages/sankaku/tsconfig.json b/packages/sankaku/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/sankaku/tsconfig.json +++ b/packages/sankaku/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/packages/yande/src/index.ts b/packages/yande/src/index.ts index dedac8869..162d6866b 100644 --- a/packages/yande/src/index.ts +++ b/packages/yande/src/index.ts @@ -1,95 +1,3 @@ -import { createHash } from 'node:crypto' - -import { Schema, trimSlash } from 'koishi' -import { ImageSource } from 'koishi-plugin-booru' - -import { Yande } from './types' - -/** - * Yande.re requires a password hash for authentication. - * - * @see https://yande.re/help/api - */ -function hashPassword(password: string) { - const salted = `choujin-steiner--${password}--` - // do a SHA1 hash of the salted password - const hash = createHash('sha1') - hash.update(salted) - return hash.digest('hex') -} - -class YandeImageSource extends ImageSource { - languages = ['en'] - source = 'yande' - - 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 { - // API docs: https://yande.re/help/api - const params = { - tags: [...query.tags, 'order:random'].join('+'), - limit: query.count, - } - const 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(url, { params }) - - if (!Array.isArray(data)) { - return - } - - return data.map((post) => { - return { - urls: { - original: post.file_url, - medium: post.sample_url, - thumbnail: post.preview_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 YandeImageSource { - export interface Config extends ImageSource.Config { - endpoint: string - keyPairs: { - login: string - password: string - }[] - } - - export const Config: Schema = Schema.intersect([ - ImageSource.createSchema({ label: 'yande' }), - Schema.object({ - endpoint: Schema.string().description('Yande.re 的 URL。').default('https://yande.re'), - keyPairs: Schema.array( - Schema.object({ - login: Schema.string().required().description('Yande.re 的用户名。'), - password: Schema.string().required().role('secret').description('Yande.re 的密码。'), - }), - ) - .default([]) - .description('Yande.re 的登录凭据。'), - }).description('搜索设置'), - ]) -} +import YandeImageSource from 'koishi-plugin-booru/yande' export default YandeImageSource diff --git a/packages/yande/tsconfig.json b/packages/yande/tsconfig.json index 0c0cc098d..4e957d7a6 100644 --- a/packages/yande/tsconfig.json +++ b/packages/yande/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "src", - "outDir": "lib" + "outDir": "lib", + "module": "Node16", + "moduleResolution": "Node16" }, "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index bc90e6905..94838ff5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "baseUrl": ".", "paths": { "koishi-plugin-booru": ["packages/core/src"], + "koishi-plugin-booru/*": ["packages/core/src/sources/*"], "koishi-plugin-booru-*": ["packages/*/src"] } }