Commit 088052e1 authored by nanahira's avatar nanahira

first

parent fc8a5324
webpack.config.js
dist/*
build/*
\ No newline at end of file
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
/build
/output
/dest
/config.yaml
.idea
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
build:
stage: build
tags:
- linux
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
upload_to_minio:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- aws s3 --endpoint=https://minio.mycard.moe:9000 sync --delete dist/ s3://nanahira/koishi-plugin/myplugin
only:
- master
deploy_npm:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- apt update;apt -y install coreutils
- echo $NPMRC | base64 --decode > ~/.npmrc
- npm publish . || true
only:
- master
/install-npm.sh
.git*
/output
/dest
/config.yaml
.idea
.dockerignore
Dockerfile
/src
.eslintrc.js
.prettierrc
webpack.config.js
tsconfig.json
\ No newline at end of file
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
The MIT License (MIT)
Copyright (c) 2021 Nanahira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
#!/bin/bash
npm install --save \
source-map-support
npm install --save-dev \
@types/node \
typescript \
'@typescript-eslint/eslint-plugin@^4.28.2' \
'@typescript-eslint/parser@^4.28.2 '\
'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0' \
prettier \
raw-loader \
ts-loader \
webpack \
webpack-cli \
koishi@next \
koishi-utils-schemagen
This diff is collapsed.
{
"name": "koishi-plugin-pics",
"description": "Koishi 的随机图片插件",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint --fix .",
"build": "webpack"
},
"repository": {
"type": "git",
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-pics.git"
},
"author": "Nanahira <nanahira@momobako.com>",
"license": "MIT",
"keywords": [
"Koishi.js",
"qqbot",
"cqhttp",
"onebot",
"service:picscontainer"
],
"bugs": {
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-pics/issues"
},
"homepage": "https://code.mycard.moe/3rdeye/koishi-plugin-pics",
"dependencies": {
"source-map-support": "^0.5.20"
},
"peerDependencies": {
"koishi": "^4.0.0-alpha.11"
},
"devDependencies": {
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.4",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"koishi": "^4.0.0-alpha.11",
"koishi-utils-schemagen": "^1.1.5",
"lodash": "^4.17.21",
"prettier": "^2.4.1",
"raw-loader": "^4.0.2",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4",
"webpack": "^5.59.1",
"webpack-cli": "^4.9.1"
}
}
import 'source-map-support/register';
import { DefineSchema, schemaFromClass } from 'koishi-utils-schemagen';
export class PicsPluginConfig {
@DefineSchema({ desc: '命令名', default: 'pic', hidden: true })
commandName: string;
@DefineSchema({
desc: '获取失败的提示信息',
default: '未找到任何图片。',
hidden: true,
})
failedMessage: string;
}
export type PicsPluginConfigLike = Partial<PicsPluginConfig>;
// For convenience of plugins
export interface PicSourceInfo {
tags?: string[];
weight?: number;
name: string;
description?: string;
}
export class PicSourceConfig implements PicSourceInfo {
@DefineSchema({ type: 'string', default: [], desc: '图源标签' })
tags: string[];
@DefineSchema({ default: 1, desc: '图源权重' })
weight: number;
@DefineSchema({ default: 1, desc: '图源名称', required: true })
name: string;
@DefineSchema({ desc: '图源描述' })
description?: string;
applyTo(target: PicSourceInfo) {
target.tags ||= this.tags;
target.weight ||= this.weight;
target.name ||= this.name;
target.description ||= this.description;
}
}
export const PicSourceSchema = schemaFromClass(PicSourceConfig);
import 'source-map-support/register';
import { Context } from 'koishi';
import { PicsPlugin } from './plugin';
import { PicsPluginConfigLike } from './config';
export * from './config';
export * from './plugin';
export const name = 'pics';
const plugin = new PicsPlugin();
export const schema = plugin.schema;
export function apply(ctx: Context, config: Partial<PicsPluginConfigLike>) {
ctx.plugin(plugin, config);
}
import 'source-map-support/register';
import { Context, Schema, Awaitable, Random } from 'koishi';
import {
PicSourceInfo,
PicsPluginConfig,
PicsPluginConfigLike,
} from './config';
import {
DefineSchema,
schemaFromClass,
schemaTransform,
} from 'koishi-utils-schemagen';
import _ from 'lodash';
import { segment } from 'koishi';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
pics: PicsContainer;
}
}
}
Context.service('pics');
export interface PicResult {
url: string;
description?: string;
}
export class PicSource implements PicSourceInfo {
constructor(protected ctx: Context) {}
tags: string[] = [];
weight = 1;
name = 'default';
description = '';
randomPic(picTags: string[]): Awaitable<PicResult> {
// For override
throw new Error(`Not implemented`);
}
onStartup(): Awaitable<void> {
return;
}
onShutdown(): Awaitable<void> {
return;
}
getDisplayString() {
let pattern = this.name;
if (this.tags.length) {
pattern += `\t标签: ${this.tags.join(',')}`;
}
if (this.description) {
pattern += `\t${this.description}`;
}
return pattern;
}
}
export class PicsContainer {
constructor(private ctx: Context) {}
private sources = new Map<PicSource, () => boolean>();
addSource(source: PicSource, targetCtx?: Context) {
const processingCtx: Context = targetCtx || this[Context.current];
const dispose = processingCtx.on('disconnect', () =>
this.removeSource(source),
);
this.sources.set(source, dispose);
processingCtx.on('connect', () => source.onStartup());
this.ctx.logger('pics').info(`Loaded pic source ${source.name}.`);
}
async removeSource(source: PicSource) {
try {
await source.onShutdown();
} catch (e) {
this.ctx
.logger('pics')
.warn(`Shutdown of ${source.name} failed: ${e.toString()}`);
}
const disposable = this.sources.get(source);
this.sources.delete(source);
if (disposable) {
disposable();
}
this.ctx.logger('pics').info(`Removed pic source ${source.name}.`);
}
private allSources() {
return Array.from(this.sources.keys());
}
pickAvailableSources(sourceTags: string[] = []) {
let sources = this.allSources();
if (sourceTags.length) {
sources = sources.filter(
(s) =>
sourceTags.some((exact) => s.name === exact) ||
sourceTags.every((t) => s.tags.includes(t)),
);
}
return sources;
}
randomSourceWithWeight(sources: PicSource[] = this.allSources()) {
if (!sources.length) {
return null;
}
if (sources.length === 1) {
return sources[0];
}
const weightedSources = _.flatten(
sources.map((s) => _.range(s.weight).map(() => s)),
);
return Random.pick(weightedSources);
}
private async retryWithAnotherSource(
failedSource: PicSource,
sources: PicSource[],
picTags: string[],
) {
const remainingSources = sources.filter((s) => s !== failedSource);
return this.fetchPicsWithSources(remainingSources, picTags);
}
private async fetchPicsWithSources(
sources: PicSource[],
picTags: string[],
): Promise<PicResult> {
const source = this.randomSourceWithWeight(sources);
if (!source) {
return null;
}
this.ctx
.logger('pics')
.debug(`Using source ${source.name} for searching ${picTags.join(',')}`);
try {
const result = await source.randomPic(picTags);
if (!result) {
this.ctx
.logger('pics')
.debug(
`Pic not found from ${source.name}, retrying with another source.`,
);
return this.retryWithAnotherSource(source, sources, picTags);
}
this.ctx.logger('pics').debug(`Got pic ${result.url}`);
return result;
} catch (e) {
this.ctx
.logger('pics')
.warn(
`Fetch pic failed from ${
source.name
}, retrying with another source: ${e.toString()}`,
);
return this.retryWithAnotherSource(source, sources, picTags);
}
}
async randomPic(picTags: string[] = [], sourceTags: string[] = []) {
const sources = this.pickAvailableSources(sourceTags);
return this.fetchPicsWithSources(sources, sourceTags);
}
}
export class PicsPlugin {
private config: PicsPluginConfig;
private ctx: Context;
name = 'pics-main';
schema: Schema<PicsPluginConfigLike> = schemaFromClass(PicsPluginConfig);
apply(ctx: Context, config: PicsPluginConfigLike) {
this.ctx = ctx;
this.config = schemaTransform(PicsPluginConfig, config);
ctx.pics = new PicsContainer(ctx);
ctx
.command(`${this.config.commandName}`, '获取随机图片')
.usage(
`从各个图源中随机获取一张随机图片。图源可以用 ${this.config.commandName}.sources 查询。参数均为可选。`,
)
.option(
'source',
'-s <source:string> 指定图源,逗号分隔。图源可以用 ${this.config.commandName}.sources 查询。',
)
.option('tag', '-t <tag:string> 需要查询的图片标签,逗号分隔。')
.example(
`${this.config.commandName} -s pixiv -t yuyuko 从 pixiv 图源中获取一张具有 yuyuko 标签的图。`,
)
.action(async (argv) => {
const sourceTags = argv.options.source
? argv.options.source.split(',')
: [];
const picTags = argv.options.tag ? argv.options.tag.split(',') : [];
const result = await ctx.pics.randomPic(picTags, sourceTags);
if (!result) {
return this.config.failedMessage;
}
let msg = `获取到图片:\n${segment('image', {
url: result.url,
cache: true,
})}`;
if (result.description) {
msg += `\n${result.description}`;
}
return msg;
})
.subcommand('.sources', '查询图源列表')
.option('source', '-s <source:string> 要查询的图源标签,逗号分隔。')
.usage('图源标签可用于图片获取的图源筛选。')
.example(
`${this.config.commandName}.sources 查询全部的图源。 ${this.config.commandName} -s pixiv 查询含有 pixiv 标签的图源。`,
)
.action(async (argv) => {
const sourceTags = argv.options.source
? argv.options.source.split(',')
: [];
const sources = ctx.pics.pickAvailableSources(sourceTags);
return `图源的列表如下:\n${sources
.map((s) => s.getDisplayString())
.join('\n')}`;
});
}
}
{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "es2021",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts"
]
}
const path = require("path");
module.exports = {
entry: "./src/index.ts",
mode: "production",
target: "node",
devtool: "source-map",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{ test: /\.mustache$/, use: "raw-loader" },
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: "index.js",
library: {
type: "commonjs",
},
path: path.resolve(__dirname, "dist"),
},
externals: {
'koishi': 'koishi',
}
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment