diff --git a/.eslintrc.yml b/.eslintrc.yml index f8a1d4bb4c..bff73023ae 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -32,6 +32,9 @@ rules: - after import/export: off keyword-spacing: off + max-len: + - warn + - 160 # https://github.com/typescript-eslint/typescript-eslint/issues/618 no-dupe-class-members: off no-ex-assign: off diff --git a/.mocharc.js b/.mocharc.js index 7ebf613555..dbe41bce71 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -5,6 +5,8 @@ module.exports = { 'packages/koishi-core/tests/*.spec.ts', 'packages/koishi-utils/tests/*.spec.ts', 'packages/koishi-test-utils/tests/*.spec.ts', + 'packages/plugin-common/tests/admin.spec.ts', + 'packages/plugin-common/tests/sender.spec.ts', 'packages/plugin-eval/tests/*.spec.ts', 'packages/plugin-github/tests/*.spec.ts', 'packages/plugin-teach/tests/*.spec.ts', diff --git a/build/publish.ts b/build/publish.ts index 8d9c644818..f563b58f85 100644 --- a/build/publish.ts +++ b/build/publish.ts @@ -1,6 +1,7 @@ import { PackageJson, getWorkspaces, spawnAsync, spawnSync } from './utils' import { gt, prerelease } from 'semver' import { Octokit } from '@octokit/rest' +import { draft } from './release' import latest from 'latest-version' import ora from 'ora' @@ -11,15 +12,6 @@ if (CI && (GITHUB_REF !== 'refs/heads/master' || GITHUB_EVENT_NAME !== 'push')) process.exit(0) } -const headerMap = { - feat: 'Features', - fix: 'Bug Fixes', - dep: 'Dependencies', -} - -const prefixes = Object.keys(headerMap) -const prefixRegExp = new RegExp(`^(${prefixes.join('|')})(?:\\((\\S+)\\))?: (.+)$`) - ;(async () => { let folders = await getWorkspaces() if (process.argv[2]) { @@ -66,24 +58,7 @@ const prefixRegExp = new RegExp(`^(${prefixes.join('|')})(?:\\((\\S+)\\))?: (.+) return console.log(`Tag ${version} already exists.`) } - const updates = {} - const lastTag = tags[tags.length - 1] - const commits = spawnSync(`git log ${lastTag}..HEAD --format=%H%s`).split(/\r?\n/).reverse() - for (const commit of commits) { - const hash = commit.slice(0, 40) - const details = prefixRegExp.exec(commit.slice(40)) - if (!details) continue - let message = details[3] - if (details[2]) message = `**${details[2]}:** ${message}` - if (!updates[details[1]]) updates[details[1]] = '' - updates[details[1]] += `- ${message} (${hash})\n` - } - - let body = '' - for (const type in headerMap) { - if (!updates[type]) continue - body += `## ${headerMap[type]}\n\n${updates[type]}\n` - } + const body = draft(tags[tags.length - 1]) console.log(`Start to release a new version with tag ${version} ...`) await github.repos.createRelease({ diff --git a/build/release.ts b/build/release.ts new file mode 100644 index 0000000000..6c7ecc149d --- /dev/null +++ b/build/release.ts @@ -0,0 +1,36 @@ +import { spawnSync } from './utils' + +const headerMap = { + feat: 'Features', + fix: 'Bug Fixes', + dep: 'Dependencies', +} + +const prefixes = Object.keys(headerMap) +const prefixRegExp = new RegExp(`^(${prefixes.join('|')})(?:\\((\\S+)\\))?: (.+)$`) + +export function draft(base: string) { + const updates = {} + const commits = spawnSync(`git log ${base}..HEAD --format=%H%s`).split(/\r?\n/).reverse() + for (const commit of commits) { + const hash = commit.slice(0, 40) + const details = prefixRegExp.exec(commit.slice(40)) + if (!details) continue + let message = details[3] + if (details[2]) message = `**${details[2]}:** ${message}` + if (!updates[details[1]]) updates[details[1]] = '' + updates[details[1]] += `- ${message} (${hash})\n` + } + + let body = '' + for (const type in headerMap) { + if (!updates[type]) continue + body += `## ${headerMap[type]}\n\n${updates[type]}\n` + } + return body +} + +if (require.main === module) { + const tags = spawnSync('git tag -l').split(/\r?\n/) + console.log(draft(tags[tags.length - 1])) +} diff --git a/package.json b/package.json index 6d3fb996f5..5354dff5ba 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,10 @@ "bump": "ts-node build/bump", "dep": "ts-node build/dep", "docs": "cd docs && yarn dev", + "draft": "ts-node build/release", "test": "cross-env TS_NODE_PROJECT=tsconfig.test.json mocha --experimental-vm-modules --enable-source-maps", "test:json": "c8 -r json yarn test", - "test:lcov": "rimraf coverage && c8 -r lcov yarn test", + "test:html": "rimraf coverage && c8 -r html yarn test", "test:text": "c8 -r text yarn test", "lint": "eslint packages/*/src/**/*.ts --fix --cache", "pub": "ts-node build/publish", @@ -31,14 +32,14 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@octokit/rest": "^18.0.4", + "@octokit/rest": "^18.0.5", "@sinonjs/fake-timers": "^6.0.1", "@types/chai": "^4.2.12", "@types/chai-as-promised": "^7.1.3", "@types/cross-spawn": "^6.0.2", "@types/fs-extra": "^9.0.1", "@types/mocha": "^8.0.3", - "@types/node": "^14.6.3", + "@types/node": "^14.6.4", "@types/semver": "^7.3.3", "@types/sinonjs__fake-timers": "^6.0.1", "@typescript-eslint/eslint-plugin": "^3.10.1", @@ -50,7 +51,7 @@ "cross-env": "^7.0.2", "cross-spawn": "^7.0.3", "del": "^5.1.0", - "eslint": "^7.8.0", + "eslint": "^7.8.1", "eslint-config-standard": "^14.1.1", "eslint-import-resolver-typescript": "^2.3.0", "eslint-plugin-import": "^2.22.0", diff --git a/packages/adapter-cqhttp/package.json b/packages/adapter-cqhttp/package.json index 78b84ac3c8..9576e7e21a 100644 --- a/packages/adapter-cqhttp/package.json +++ b/packages/adapter-cqhttp/package.json @@ -1,7 +1,7 @@ { "name": "koishi-adapter-cqhttp", "description": "CQHTTP adapter for Koishi", - "version": "1.0.2", + "version": "1.0.3", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -31,7 +31,7 @@ "koishi" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { "@types/ms": "^0.7.31", @@ -43,6 +43,6 @@ "axios": "^0.20.0", "ms": "^2.1.2", "ws": "^7.3.1", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/adapter-cqhttp/src/api.ts b/packages/adapter-cqhttp/src/api.ts index 2d125afebc..11a184310a 100644 --- a/packages/adapter-cqhttp/src/api.ts +++ b/packages/adapter-cqhttp/src/api.ts @@ -81,18 +81,14 @@ declare module 'koishi-core/dist/server' { getAsync(action: string, params?: Record): Promise sendGroupMsgAsync(groupId: number, message: string, autoEscape?: boolean): Promise sendPrivateMsgAsync(userId: number, message: string, autoEscape?: boolean): Promise - setGroupAnonymousBan(groupId: number, anonymous: object, duration?: number): Promise - setGroupAnonymousBan(groupId: number, flag: string, duration?: number): Promise - setGroupAnonymousBanAsync(groupId: number, anonymous: object, duration?: number): Promise - setGroupAnonymousBanAsync(groupId: number, flag: string, duration?: number): Promise + setGroupAnonymousBan(groupId: number, anonymous: string | object, duration?: number): Promise + setGroupAnonymousBanAsync(groupId: number, anonymous: string | object, duration?: number): Promise setFriendAddRequest(flag: string, approve?: boolean): Promise setFriendAddRequest(flag: string, remark?: string): Promise setFriendAddRequestAsync(flag: string, approve?: boolean): Promise setFriendAddRequestAsync(flag: string, remark?: string): Promise - setGroupAddRequest(flag: string, subType: 'add' | 'invite', approve?: boolean): Promise - setGroupAddRequest(flag: string, subType: 'add' | 'invite', reason?: string): Promise - setGroupAddRequestAsync(flag: string, subType: 'add' | 'invite', approve?: boolean): Promise - setGroupAddRequestAsync(flag: string, subType: 'add' | 'invite', reason?: string): Promise + setGroupAddRequest(flag: string, subType: 'add' | 'invite', approve?: string | boolean): Promise + setGroupAddRequestAsync(flag: string, subType: 'add' | 'invite', approve?: string | boolean): Promise deleteMsg(messageId: number): Promise deleteMsgAsync(messageId: number): Promise sendLike(userId: number, times?: number): Promise @@ -169,7 +165,7 @@ Bot.prototype.sendGroupMsg = async function (this: Bot, groupId, message, autoEs if (!message) return const session = this.createSession('group', 'group', groupId, message) if (this.app.bail(session, 'before-send', session)) return - const { messageId } = await this.get('send_group_msg', { groupId, message, autoEscape }) + const { messageId } = await this.get('send_group_msg', { groupId, message: session.message, autoEscape }) session.messageId = messageId this.app.emit(session, 'send', session) return messageId @@ -179,14 +175,14 @@ Bot.prototype.sendGroupMsgAsync = function (this: Bot, groupId, message, autoEsc if (!message) return const session = this.createSession('group', 'group', groupId, message) if (this.app.bail(session, 'before-send', session)) return - return this.getAsync('send_group_msg', { groupId, message, autoEscape }) + return this.getAsync('send_group_msg', { groupId, message: session.message, autoEscape }) } Bot.prototype.sendPrivateMsg = async function (this: Bot, userId, message, autoEscape = false) { if (!message) return const session = this.createSession('private', 'user', userId, message) if (this.app.bail(session, 'before-send', session)) return - const { messageId } = await this.get('send_private_msg', { userId, message, autoEscape }) + const { messageId } = await this.get('send_private_msg', { userId, message: session.message, autoEscape }) session.messageId = messageId this.app.emit(session, 'send', session) return messageId @@ -196,16 +192,16 @@ Bot.prototype.sendPrivateMsgAsync = function (this: Bot, userId, message, autoEs if (!message) return const session = this.createSession('private', 'user', userId, message) if (this.app.bail(session, 'before-send', session)) return - return this.getAsync('send_private_msg', { userId, message, autoEscape }) + return this.getAsync('send_private_msg', { userId, message: session.message, autoEscape }) } -Bot.prototype.setGroupAnonymousBan = async function (this: Bot, groupId: number, meta: object | string, duration?: number) { +Bot.prototype.setGroupAnonymousBan = async function (this: Bot, groupId, meta, duration) { const args = { groupId, duration } as any args[typeof meta === 'string' ? 'flag' : 'anonymous'] = meta await this.get('set_group_anonymous_ban', args) } -Bot.prototype.setGroupAnonymousBanAsync = function (this: Bot, groupId: number, meta: object | string, duration?: number) { +Bot.prototype.setGroupAnonymousBanAsync = function (this: Bot, groupId, meta, duration) { const args = { groupId, duration } as any args[typeof meta === 'string' ? 'flag' : 'anonymous'] = meta return this.getAsync('set_group_anonymous_ban', args) @@ -227,7 +223,7 @@ Bot.prototype.setFriendAddRequestAsync = function (this: Bot, flag: string, info } } -Bot.prototype.setGroupAddRequest = async function (this: Bot, flag: string, subType: 'add' | 'invite', info: string | boolean = true) { +Bot.prototype.setGroupAddRequest = async function (this: Bot, flag, subType, info = true) { if (typeof info === 'string') { await this.get('set_group_add_request', { flag, subType, approve: false, reason: info }) } else { @@ -235,7 +231,7 @@ Bot.prototype.setGroupAddRequest = async function (this: Bot, flag: string, subT } } -Bot.prototype.setGroupAddRequestAsync = function (this: Bot, flag: string, subType: 'add' | 'invite', info: string | boolean = true) { +Bot.prototype.setGroupAddRequestAsync = function (this: Bot, flag, subType, info = true) { if (typeof info === 'string') { return this.getAsync('set_group_add_request', { flag, subType, approve: false, reason: info }) } else { diff --git a/packages/adapter-cqhttp/src/index.ts b/packages/adapter-cqhttp/src/index.ts index 7b789ce6c9..1f0473bd44 100644 --- a/packages/adapter-cqhttp/src/index.ts +++ b/packages/adapter-cqhttp/src/index.ts @@ -83,7 +83,7 @@ Session.prototype.$send = async function $send(this: Session, message: string, a if (this._response) { const session = this.$bot.createSession(this.messageType, ctxType, ctxId, message) if (this.$app.bail(this, 'before-send', session)) return - return this._response({ reply: message, autoEscape, atSender: false }) + return this._response({ reply: session.message, autoEscape, atSender: false }) } return ctxType === 'group' ? this.$bot.sendGroupMsgAsync(ctxId, message, autoEscape) diff --git a/packages/koishi-core/package.json b/packages/koishi-core/package.json index 9d3b66800b..03230705a1 100644 --- a/packages/koishi-core/package.json +++ b/packages/koishi-core/package.json @@ -1,7 +1,7 @@ { "name": "koishi-core", "description": "Core features for Koishi", - "version": "2.2.0", + "version": "2.2.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -44,7 +44,7 @@ "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-router": "^9.4.0", - "koishi-utils": "^3.1.3", + "koishi-utils": "^3.1.4", "leven": "^3.1.0", "lru-cache": "^6.0.0" } diff --git a/packages/koishi-core/src/app.ts b/packages/koishi-core/src/app.ts index 1124d29277..9fc967e0df 100644 --- a/packages/koishi-core/src/app.ts +++ b/packages/koishi-core/src/app.ts @@ -248,8 +248,9 @@ export class App extends Context { await session.$group?._update() } - private _parse(message: string, { $reply, $prefix, $appel, messageType }: Session, builtin: boolean, terminator = '') { + private _parse(message: string, session: Session, builtin: boolean, terminator = '') { // group message should have prefix or appel to be interpreted as a command call + const { $reply, $prefix, $appel, messageType } = session if (builtin && ($reply || messageType !== 'private' && $prefix === null && !$appel)) return terminator = escapeRegExp(terminator) const name = message.split(new RegExp(`[\\s${terminator}]`), 1)[0] diff --git a/packages/koishi-core/src/command.ts b/packages/koishi-core/src/command.ts index 90820e7c29..e3c1fef20a 100644 --- a/packages/koishi-core/src/command.ts +++ b/packages/koishi-core/src/command.ts @@ -82,7 +82,8 @@ export interface ParsedLine { options: O } -export interface ParsedArgv extends Partial> { +export interface ParsedArgv +extends Partial> { command: Command session: Session next?: NextFunction @@ -268,14 +269,14 @@ export class Command(name: K, description: string, config: StringOptionConfig): Command> - option(name: K, description: string, config: NumberOptionConfig): Command> - option(name: K, description: string, config: BooleanOptionConfig): Command> - option(name: K, description: string, config?: OptionConfig): Command> - option(name: K, description: string, config: OptionConfig = {}) { + option(name: K, desc: string, config: StringOptionConfig): Command> + option(name: K, desc: string, config: NumberOptionConfig): Command> + option(name: K, desc: string, config: BooleanOptionConfig): Command> + option(name: K, desc: string, config?: OptionConfig): Command> + option(name: K, desc: string, config: OptionConfig = {}) { const fallbackType = typeof config.fallback as never const type = config['type'] || supportedType.includes(fallbackType) && fallbackType - return this._registerOption(name, description, { ...config, type }) as any + return this._registerOption(name, desc, { ...config, type }) as any } removeOption(name: K) { diff --git a/packages/koishi-core/src/context.ts b/packages/koishi-core/src/context.ts index 99fd2a8b6a..81ec3f8984 100644 --- a/packages/koishi-core/src/context.ts +++ b/packages/koishi-core/src/context.ts @@ -203,7 +203,8 @@ export class Context { } removeListener(name: K, listener: EventMap[K]) { - const index = (this.app._hooks[name] || []).findIndex(([context, callback]) => context === this && callback === listener) + const index = (this.app._hooks[name] || []) + .findIndex(([context, callback]) => context === this && callback === listener) if (index >= 0) { this.app._hooks[name].splice(index, 1) return true diff --git a/packages/koishi-core/src/plugins/help.ts b/packages/koishi-core/src/plugins/help.ts index 4464286835..e38818bc62 100644 --- a/packages/koishi-core/src/plugins/help.ts +++ b/packages/koishi-core/src/plugins/help.ts @@ -134,7 +134,8 @@ function getOptions(command: Command, session: Session, maxUsag if (command.config.hideOptions && !config.showHidden) return [] const options = config.showHidden ? Object.values(command._options) - : Object.values(command._options).filter(option => !option.hidden && (!session.$user || option.authority <= session.$user.authority)) + : Object.values(command._options) + .filter(option => !option.hidden && (!session.$user || option.authority <= session.$user.authority)) if (!options.length) return [] const output = config.authority && options.some(o => o.authority) diff --git a/packages/koishi-core/src/session.ts b/packages/koishi-core/src/session.ts index 563dc28373..17acdc01fe 100644 --- a/packages/koishi-core/src/session.ts +++ b/packages/koishi-core/src/session.ts @@ -6,13 +6,18 @@ import { App } from './app' export type PostType = 'message' | 'notice' | 'request' | 'meta_event' | 'send' export type MessageType = 'private' | 'group' +export type NoticeType = + | 'group_upload' | 'group_admin' | 'group_increase' | 'group_decrease' + | 'group_ban' | 'friend_add' | 'group_recall' +export type RequestType = 'friend' | 'group' +export type MetaEventType = 'lifecycle' | 'heartbeat' export interface MetaTypeMap { message: MessageType - notice: 'group_upload' | 'group_admin' | 'group_increase' | 'group_decrease' | 'group_ban' | 'friend_add' | 'group_recall' - request: 'friend' | 'group' + notice: NoticeType + request: RequestType // eslint-disable-next-line camelcase - meta_event: 'lifecycle' | 'heartbeat' + meta_event: MetaEventType send: null } diff --git a/packages/koishi-test-utils/package.json b/packages/koishi-test-utils/package.json index 94a431b1f9..ae0ce2b876 100644 --- a/packages/koishi-test-utils/package.json +++ b/packages/koishi-test-utils/package.json @@ -40,8 +40,8 @@ "dependencies": { "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "koishi-core": "^2.2.0", - "koishi-utils": "^3.1.3" + "koishi-core": "^2.2.1", + "koishi-utils": "^3.1.4" }, "devDependencies": { "@types/chai": "^4.2.12", diff --git a/packages/koishi-test-utils/src/memory.ts b/packages/koishi-test-utils/src/memory.ts index 679cb703e7..d55f79e383 100644 --- a/packages/koishi-test-utils/src/memory.ts +++ b/packages/koishi-test-utils/src/memory.ts @@ -72,6 +72,7 @@ extendDatabase(MemoryDatabase, { selfId = typeof selfId === 'number' ? selfId : 0 const data = table[groupId] if (data) return clone(data) + if (selfId < 0) return null const fallback = Group.create(groupId, selfId) if (selfId) table[groupId] = fallback return clone(fallback) diff --git a/packages/koishi-utils/package.json b/packages/koishi-utils/package.json index b27a1a23ba..e1f62a10f6 100644 --- a/packages/koishi-utils/package.json +++ b/packages/koishi-utils/package.json @@ -1,7 +1,7 @@ { "name": "koishi-utils", "description": "Utilities for Koishi", - "version": "3.1.3", + "version": "3.1.4", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/koishi-utils/src/logger.ts b/packages/koishi-utils/src/logger.ts index 90563b2d27..4b244448f7 100644 --- a/packages/koishi-utils/src/logger.ts +++ b/packages/koishi-utils/src/logger.ts @@ -48,7 +48,9 @@ export class Logger { private code: number private displayName: string - constructor(private name: string, private showDiff = false) { + public stream: NodeJS.WritableStream = process.stderr + + constructor(public name: string, private showDiff = false) { if (name in instances) return instances[name] let hash = 0 for (let i = 0; i < name.length; i++) { @@ -72,7 +74,7 @@ export class Logger { private createMethod(name: LogType, prefix: string, minLevel: number) { this[name] = (...args: [any, ...any[]]) => { if (this.level < minLevel) return - process.stderr.write(prefix + this.displayName + this.format(...args) + '\n') + this.stream.write(prefix + this.displayName + this.format(...args) + '\n') } } @@ -80,8 +82,8 @@ export class Logger { return Logger.levels[this.name] ?? Logger.baseLevel } - extend = (namespace: string) => { - return new Logger(`${this.name}:${namespace}`) + extend = (namespace: string, showDiff = this.showDiff) => { + return new Logger(`${this.name}:${namespace}`, showDiff) } format: (format: any, ...param: any[]) => string = (...args) => { @@ -93,7 +95,7 @@ export class Logger { let index = 0 args[0] = (args[0] as string).replace(/%([a-zA-Z%])/g, (match, format) => { - if (match === '%%') return match + if (match === '%%') return '%' index += 1 const formatter = Logger.formatters[format] if (typeof formatter === 'function') { diff --git a/packages/koishi-utils/src/time.ts b/packages/koishi-utils/src/time.ts index 328650dd1b..75d2b67b7a 100644 --- a/packages/koishi-utils/src/time.ts +++ b/packages/koishi-utils/src/time.ts @@ -28,7 +28,14 @@ export namespace Time { return new Date(+date + offset * minute) } - const timeRegExp = /^(\d+(?:\.\d+)?w(?:eek(?:s)?)?)?(\d+(?:\.\d+)?d(?:ay(?:s)?)?)?(\d+(?:\.\d+)?h(?:our(?:s)?)?)?(\d+(?:\.\d+)?m(?:in(?:ute)?(?:s)?)?)?(\d+(?:\.\d+)?s(?:ec(?:ond)?(?:s)?)?)?$/ + const numeric = /\d+(?:\.\d+)?/.source + const timeRegExp = new RegExp(`^${[ + 'w(?:eek(?:s)?)?', + 'd(?:ay(?:s)?)?', + 'h(?:our(?:s)?)?', + 'm(?:in(?:ute)?(?:s)?)?', + 's(?:ec(?:ond)?(?:s)?)?', + ].map(unit => `(${numeric}${unit})?`).join('')}$`) export function parseTime(source: string) { const capture = timeRegExp.exec(source) diff --git a/packages/koishi-utils/tests/logger.spec.ts b/packages/koishi-utils/tests/logger.spec.ts new file mode 100644 index 0000000000..19c737f49f --- /dev/null +++ b/packages/koishi-utils/tests/logger.spec.ts @@ -0,0 +1,54 @@ +import { install, InstalledClock } from '@sinonjs/fake-timers' +import { expect } from 'chai' +import { Logger } from 'koishi-utils' +import { Writable } from 'stream' + +describe('Logger API', () => { + let logger: Logger + let data: string + let clock: InstalledClock + const { colors } = Logger.options + + before(() => { + Logger.options.colors = false + clock = install({ now: Date.now() }) + }) + + after(() => { + Logger.options.colors = colors + clock.uninstall() + }) + + it('basic support', () => { + logger = new Logger('test').extend('logger', true) + expect(logger.name).to.equal('test:logger') + logger.stream = new Writable({ + write(chunk, encoding, callback) { + data = chunk.toString() + callback() + }, + }) + }) + + it('format error', () => { + const error = new Error('message') + error.stack = null + logger.error(error) + expect(data).to.equal('[E] test:logger message\n') + }) + + it('format object', () => { + clock.tick(2) + const object = { foo: 'bar' } + logger.success(object) + expect(data).to.equal("[S] test:logger { foo: 'bar' } +2ms\n") + }) + + it('custom formatter', () => { + clock.tick(1) + Logger.formatters.x = () => 'custom' + Logger.levels[logger.name] = 2 + logger.info('%x%%x') + expect(data).to.equal('[I] test:logger custom%x +1ms\n') + }) +}) diff --git a/packages/koishi/ecosystem.json b/packages/koishi/ecosystem.json index a75e156a92..cb4e923abb 100644 --- a/packages/koishi/ecosystem.json +++ b/packages/koishi/ecosystem.json @@ -1,6 +1,6 @@ { "koishi-adapter-cqhttp": { - "version": "1.0.2", + "version": "1.0.3", "description": "CQHTTP adapter for Koishi" }, "koishi-plugin-chess": { @@ -8,7 +8,7 @@ "description": "Chess Plugin for Koishi" }, "koishi-plugin-common": { - "version": "3.0.0-beta.15", + "version": "3.0.0-beta.16", "description": "Common plugins for Koishi" }, "koishi-plugin-eval": { @@ -20,7 +20,7 @@ "description": "Execute JavaScript in Koishi" }, "koishi-plugin-github": { - "version": "2.0.1", + "version": "2.0.2", "description": "GitHub webhook plugin for Koishi" }, "koishi-plugin-image-search": { @@ -28,7 +28,7 @@ "description": "Image searching plugin for Koishi" }, "koishi-plugin-mongo": { - "version": "1.0.2", + "version": "1.0.3", "description": "MongoDB support for Koishi" }, "koishi-plugin-monitor": { @@ -47,7 +47,7 @@ "description": "Subscribe RSS Url for Koishi" }, "koishi-plugin-schedule": { - "version": "2.0.1", + "version": "2.0.2", "description": "Schedule plugin for Koishi" }, "koishi-plugin-status": { diff --git a/packages/koishi/package.json b/packages/koishi/package.json index 1a4a10cb10..3559e58375 100644 --- a/packages/koishi/package.json +++ b/packages/koishi/package.json @@ -1,7 +1,7 @@ { "name": "koishi", "description": "A QQ bot framework based on CQHTTP", - "version": "2.2.0", + "version": "2.2.1", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -40,9 +40,9 @@ "dependencies": { "cac": "^6.6.1", "kleur": "^4.1.1", - "koishi-adapter-cqhttp": "^1.0.2", - "koishi-core": "^2.2.0", - "koishi-plugin-common": "^3.0.0-beta.15", + "koishi-adapter-cqhttp": "^1.0.3", + "koishi-core": "^2.2.1", + "koishi-plugin-common": "^3.0.0-beta.16", "prompts": "^2.3.2" } } diff --git a/packages/plugin-chess/package.json b/packages/plugin-chess/package.json index ada579c604..85a0039af6 100644 --- a/packages/plugin-chess/package.json +++ b/packages/plugin-chess/package.json @@ -33,10 +33,10 @@ "game" ], "peerDependencies": { - "koishi-core": "^2.2.0", + "koishi-core": "^2.2.1", "koishi-plugin-puppeteer": "^1.0.0" }, "dependencies": { - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-common/package.json b/packages/plugin-common/package.json index 7bcb1417c7..3d0270521c 100644 --- a/packages/plugin-common/package.json +++ b/packages/plugin-common/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-common", "description": "Common plugins for Koishi", - "version": "3.0.0-beta.15", + "version": "3.0.0-beta.16", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -31,12 +31,12 @@ "plugin" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { "koishi-test-utils": "^5.0.0" }, "dependencies": { - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-common/src/admin.ts b/packages/plugin-common/src/admin.ts index e4051ce1c1..5d47c7bfae 100644 --- a/packages/plugin-common/src/admin.ts +++ b/packages/plugin-common/src/admin.ts @@ -1,184 +1,217 @@ -import { isInteger, difference, Observed, paramCase, observe, Time, enumKeys } from 'koishi-utils' -import { Context, Session, getTargetId, User, Group } from 'koishi-core' +import { isInteger, difference, observe, Time, enumKeys } from 'koishi-utils' +import { Context, getTargetId, User, Group, Command, ParsedArgv } from 'koishi-core' -type ActionCallback = - (this: Context, session: Session<'authority'>, target: Observed>, ...args: string[]) => Promise +type AdminAction + = (argv: ParsedArgv & { target: T }, ...args: string[]) + => void | string | Promise -export interface ActionItem { - callback: ActionCallback - fields: (keyof T)[] +declare module 'koishi-core/dist/command' { + interface Command { + adminUser(callback: AdminAction>): this + adminGruop(callback: AdminAction>): this + } } -export class Action { - commands: Record> = {} +interface FlagOptions { + list?: boolean + set?: boolean + unset?: boolean +} - add(name: string, callback: ActionCallback, fields?: K[]) { - this.commands[paramCase(name)] = { callback, fields } - } +type FlagMap = Record & Record + +interface FlagArgv extends ParsedArgv { + target: User.Observed<'flag'> | Group.Observed<'flag'> } -export const UserAction = new Action() -export const GroupAction = new Action() - -UserAction.add('setAuth', async (session, user, value) => { - const authority = Number(value) - if (!isInteger(authority) || authority < 0) return '参数错误。' - if (authority >= session.$user.authority) return '权限不足。' - if (authority === user.authority) { - return '用户权限未改动。' - } else { - user.authority = authority - await user._update() - return '用户权限已修改。' +function flagAction(map: any, { target, options }: FlagArgv, ...flags: string[]): string +function flagAction(map: FlagMap, { target, options }: FlagArgv, ...flags: string[]) { + if (options.set || options.unset) { + const notFound = difference(flags, enumKeys(map)) + if (notFound.length) return `未找到标记 ${notFound.join(', ')}。` + for (const name of flags) { + options.set ? target.flag |= map[name] : target.flag &= ~map[name] + } + return } -}, ['authority']) - -UserAction.add('setFlag', async (session, user, ...flags) => { - const userFlags = enumKeys(User.Flag) - if (!flags.length) return `可用的标记有 ${userFlags.join(', ')}。` - const notFound = difference(flags, userFlags) - if (notFound.length) return `未找到标记 ${notFound.join(', ')}。` - for (const name of flags) { - user.flag |= User.Flag[name] + + if (options.list) { + return `全部标记为:${enumKeys(map).join(', ')}。` } - await user._update() - return '用户信息已修改。' -}, ['flag']) - -UserAction.add('unsetFlag', async (session, user, ...flags) => { - const userFlags = enumKeys(User.Flag) - if (!flags.length) return `可用的标记有 ${userFlags.join(', ')}。` - const notFound = difference(flags, userFlags) - if (notFound.length) return `未找到标记 ${notFound.join(', ')}。` - for (const name of flags) { - user.flag &= ~User.Flag[name] + + let flag = target.flag + const keys: string[] = [] + while (flag) { + const value = 2 ** Math.floor(Math.log2(flag)) + flag -= value + keys.unshift(map[value]) } - await user._update() - return '用户信息已修改。' -}, ['flag']) - -UserAction.add('setUsage', async (session, user, name, _count) => { - const count = +_count - if (!isInteger(count) || count < 0) return '参数错误。' - user.usage[name] = count - await user._update() - return '用户信息已修改。' -}, ['usage']) - -UserAction.add('clearUsage', async (session, user, ...commands) => { - if (commands.length) { - for (const command of commands) { - delete user.usage[command] + if (!keys.length) return '未设置任何标记。' + return `当前的标记为:${keys.join(', ')}。` +} + +Command.prototype.adminUser = function (this: Command, callback) { + const command = this + .userFields(['authority']) + .option('user', '-u [user] 指定目标用户', { authority: 3 }) + + command._action = async (argv) => { + const { options, session, args } = argv + const fields = Command.collect(argv, 'user') + let target: User.Observed + if (options.user) { + const qq = getTargetId(options.user) + if (!qq) return '请指定正确的目标。' + const { database } = session.$app + const data = await database.getUser(qq, -1, [...fields]) + if (!data) return '未找到指定的用户。' + if (qq === session.userId) { + target = await session.$observeUser(fields) + } else if (session.$user.authority <= data.authority) { + return '权限不足。' + } else { + target = observe(data, diff => database.setUser(qq, diff), `user ${qq}`) + } + } else { + target = await session.$observeUser(fields) } - } else { - user.usage = {} + const diffKeys = Object.keys(target._diff) + const result = await callback({ ...argv, target }, ...args) + if (typeof result === 'string') return result + if (!difference(Object.keys(target._diff), diffKeys).length) return '用户数据未改动。' + await target._update() + return '用户数据已修改。' } - await user._update() - return '用户信息已修改。' -}, ['usage']) - -UserAction.add('setTimer', async (session, user, name, offset) => { - if (!name || !offset) return '参数不足。' - const timestamp = Time.parseTime(offset) - if (!timestamp) return '请输入合法的时间。' - user.timers[name] = Date.now() + timestamp - await user._update() - return '用户信息已修改。' -}, ['timers']) - -UserAction.add('clearTimer', async (session, user, ...commands) => { - if (commands.length) { - for (const command of commands) { - delete user.timers[command] + + return command +} + +Command.prototype.adminGruop = function (this: Command, callback) { + const command = this + .userFields(['authority']) + .option('group', '-g [group] 指定目标群', { authority: 3 }) + + command._action = async (argv) => { + const { options, session, args } = argv + const fields = Command.collect(argv, 'group') + let target: Group.Observed + if (options.group) { + const { database } = session.$app + if (!isInteger(options.group) || options.group <= 0) return '请指定正确的目标。' + const data = await database.getGroup(options.group, -1, [...fields]) + if (!data) return '未找到指定的群。' + target = observe(data, diff => database.setGroup(options.group, diff), `group ${options.group}`) + } else if (session.messageType === 'group') { + target = await session.$observeGroup(fields) + } else { + return '当前不在群上下文中,请使用 -g 参数指定目标群。' } - } else { - user.timers = {} - } - await user._update() - return '用户信息已修改。' -}, ['timers']) - -GroupAction.add('setFlag', async (session, group, ...flags) => { - const groupFlags = enumKeys(Group.Flag) - if (!flags.length) return `可用的标记有 ${groupFlags.join(', ')}。` - const notFound = difference(flags, groupFlags) - if (notFound.length) return `未找到标记 ${notFound.join(', ')}。` - for (const name of flags) { - group.flag |= Group.Flag[name] - } - await group._update() - return '群信息已修改。' -}, ['flag']) - -GroupAction.add('unsetFlag', async (session, group, ...flags) => { - const groupFlags = enumKeys(Group.Flag) - if (!flags.length) return `可用的标记有 ${groupFlags.join(', ')}。` - const notFound = difference(flags, groupFlags) - if (notFound.length) return `未找到标记 ${notFound.join(', ')}。` - for (const name of flags) { - group.flag &= ~Group.Flag[name] + const result = await callback({ ...argv, target }, ...args) + if (typeof result === 'string') return result + if (!Object.keys(target._diff).length) return '群数据未改动。' + await target._update() + return '群数据已修改。' } - await group._update() - return '群信息已修改。' -}, ['flag']) - -GroupAction.add('setAssignee', async (session, group, _assignee) => { - const assignee = _assignee ? +_assignee : session.selfId - if (!isInteger(assignee) || assignee < 0) return '参数错误。' - group.assignee = assignee - await group._update() - return '群信息已修改。' -}, ['assignee']) + + return command +} export function apply(ctx: Context) { - ctx.command('admin [...args]', '管理用户', { authority: 4 }) - .userFields(['authority']) - .before(session => !session.$app.database) - .option('user', '-u [user] 指定目标用户') - .option('group', '-g [group] 指定目标群') - .option('thisGroup', '-G, --this-group 指定目标群为本群') - .action(async ({ session, options }, name, ...args) => { - const isGroup = 'group' in options || 'thisGroup' in options - if ('user' in options && isGroup) return '不能同时目标为指定用户和群。' - - const actionMap = isGroup ? GroupAction.commands : UserAction.commands - const actionList = Object.keys(actionMap).map(paramCase).join(', ') - if (!name) return `当前的可用指令有:${actionList}。` - - const action = actionMap[paramCase(name)] - if (!action) return `指令未找到。当前的可用指令有:${actionList}。` - - if (isGroup) { - const fields = action.fields ? action.fields.slice() as Group.Field[] : Group.fields - let group: Group.Observed - if (options.thisGroup) { - group = await session.$observeGroup(fields) - } else if (isInteger(options.group) && options.group > 0) { - const data = await ctx.database.getGroup(options.group, fields) - if (!data) return '未找到指定的群。' - group = observe(data, diff => ctx.database.setGroup(options.group, diff), `group ${options.group}`) - } - return (action as ActionItem).callback.call(ctx, session, group, ...args) - } else { - const fields = action.fields ? action.fields.slice() as User.Field[] : User.fields - if (!fields.includes('authority')) fields.push('authority') - let user: User.Observed - if (options.user) { - const qq = getTargetId(options.user) - if (!qq) return '未指定目标。' - const data = await ctx.database.getUser(qq, -1, fields) - if (!data) return '未找到指定的用户。' - if (qq === session.userId) { - user = await session.$observeUser(fields) - } else if (session.$user.authority <= data.authority) { - return '权限不足。' - } else { - user = observe(data, diff => ctx.database.setUser(qq, diff), `user ${qq}`) - } - } else { - user = await session.$observeUser(fields) - } - return (action as ActionItem).callback.call(ctx, session, user, ...args) + ctx.command('user', '用户管理', { authority: 3 }) + ctx.command('group', '群管理', { authority: 3 }) + + ctx.command('user.auth ', '权限信息', { authority: 4 }) + .adminUser(({ session, target }, value) => { + const authority = Number(value) + if (!isInteger(authority) || authority < 0) return '参数错误。' + if (authority >= session.$user.authority) return '权限不足。' + target.authority = authority + }) + + ctx.command('user.flag [-s|-S] [...flags]', '标记信息', { authority: 3 }) + .userFields(['flag']) + .option('list', '-l 标记列表') + .option('set', '-s 添加标记', { authority: 4 }) + .option('unset', '-S 删除标记', { authority: 4 }) + .adminUser(flagAction.bind(null, User.Flag)) + + ctx.command('usage [key]', '调用次数信息') + .alias('usages') + .userFields(['usage']) + .option('set', '-s 设置调用次数', { authority: 4 }) + .option('clear', '-c 清空调用次数', { authority: 4 }) + .adminUser(({ target, options }, name, value) => { + if (options.clear) { + name ? delete target.usage[name] : target.usage = {} + return + } + + if (options.set) { + if (value === undefined) return '参数不足。' + const count = +value + if (!isInteger(count) || count < 0) return '参数错误。' + target.usage[name] = count + return + } + + if (name) return `今日 ${name} 功能的调用次数为:${target.usage[name] || 0}` + const output: string[] = [] + for (const name of Object.keys(target.usage).sort()) { + if (name.startsWith('$')) continue + output.push(`${name}:${target.usage[name]}`) } + if (!output.length) return '今日没有调用过消耗次数的功能。' + output.unshift('今日各功能的调用次数为:') + return output.join('\n') }) + + ctx.command('timer [key]', '定时器信息') + .alias('timers') + .userFields(['timers']) + .option('set', '-s 设置定时器', { authority: 4 }) + .option('clear', '-c 清空定时器', { authority: 4 }) + .adminUser(({ target, options }, name, value) => { + if (options.clear) { + name ? delete target.timers[name] : target.timers = {} + return + } + + if (options.set) { + if (value === undefined) return '参数不足。' + const timestamp = +Time.parseDate(value) + if (!timestamp) return '请输入合法的时间。' + target.timers[name] = timestamp + return + } + + const now = Date.now() + if (name) { + const delta = target.timers[name] - now + if (delta > 0) return `定时器 ${name} 的生效时间为:剩余 ${Time.formatTime(delta)}` + return `定时器 ${name} 当前并未生效。` + } + const output: string[] = [] + for (const name of Object.keys(target.timers).sort()) { + if (name.startsWith('$')) continue + output.push(`${name}:剩余 ${Time.formatTime(target.timers[name] - now)}`) + } + if (!output.length) return '当前没有生效的定时器。' + output.unshift('各定时器的生效时间为:') + return output.join('\n') + }) + + ctx.command('group.assign [bot]', '受理者账号', { authority: 4 }) + .groupFields(['assignee']) + .adminGruop(({ session, target }, value) => { + const assignee = value ? +value : session.selfId + if (!isInteger(assignee) || assignee < 0) return '参数错误。' + target.assignee = assignee + }) + + ctx.command('group.flag [-s|-S] [...flags]', '标记信息', { authority: 3 }) + .groupFields(['flag']) + .option('list', '-l 标记列表') + .option('set', '-s 添加标记', { authority: 4 }) + .option('unset', '-S 删除标记', { authority: 4 }) + .adminGruop(flagAction.bind(null, Group.Flag)) } diff --git a/packages/plugin-common/src/broadcast.ts b/packages/plugin-common/src/broadcast.ts deleted file mode 100644 index 1ec991286a..0000000000 --- a/packages/plugin-common/src/broadcast.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Context, Group } from 'koishi-core' - -export function apply(ctx: Context) { - ctx.command('broadcast ', '全服广播', { authority: 4 }) - .before(session => !session.$app.database) - .option('forced', '-f 无视 silent 标签进行广播') - .option('only', '-o 仅向当前 Bot 负责的群进行广播') - .action(async ({ options, session }, message) => { - if (!message) return '请输入要发送的文本。' - if (!options.only) { - await ctx.broadcast(message, options.forced) - return - } - - let groups = await ctx.database.getAllGroups(['id', 'flag'], [session.selfId]) - if (!options.forced) { - groups = groups.filter(g => !(g.flag & Group.Flag.silent)) - } - await session.$bot.broadcast(groups.map(g => g.id), message) - }) -} diff --git a/packages/plugin-common/src/echo.ts b/packages/plugin-common/src/echo.ts deleted file mode 100644 index 2380a37bf3..0000000000 --- a/packages/plugin-common/src/echo.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Context } from 'koishi-core' -import { CQCode } from 'koishi-utils' - -export function apply(ctx: Context) { - ctx.command('echo ', '向当前上下文发送消息', { authority: 2 }) - .option('anonymous', '-a 匿名发送消息', { authority: 3 }) - .option('forceAnonymous', '-A 匿名发送消息', { authority: 3 }) - .option('unescape', '-e 发送非转义的消息', { authority: 3 }) - .action(async ({ options }, message) => { - if (!message) return '请输入要发送的文本。' - - if (options.unescape) { - message = CQCode.unescape(message) - } - - if (options.forceAnonymous) { - message = CQCode.stringify('anonymous') + message - } else if (options.anonymous) { - message = CQCode.stringify('anonymous', { ignore: true }) + message - } - - return message - }) -} diff --git a/packages/plugin-common/src/index.ts b/packages/plugin-common/src/index.ts index 721cc6eebf..4e0b94bf86 100644 --- a/packages/plugin-common/src/index.ts +++ b/packages/plugin-common/src/index.ts @@ -2,33 +2,30 @@ import { Context } from 'koishi-core' import { DebugOptions } from './debug' import repeater, { RepeaterOptions } from './repeater' import handler, { HandlerOptions } from './handler' +import sender, { SenderConfig } from './sender' export * from './admin' -export * from './broadcast' export * from './info' export * from './repeater' -export interface Options extends HandlerOptions, RepeaterOptions { +export interface Config extends HandlerOptions, RepeaterOptions, SenderConfig { admin?: false broadcast?: false contextify?: false echo?: false info?: false - usage?: false debug?: DebugOptions } export const name = 'common' -export function apply(ctx: Context, options: Options = {}) { - ctx.plugin(handler, options) - ctx.plugin(repeater, options) +export function apply(ctx: Context, config: Config = {}) { + ctx.plugin(handler, config) + ctx.plugin(repeater, config) + ctx.plugin(sender, config) - if (options.echo !== false) ctx.plugin(require('./echo')) - if (options.admin !== false) ctx.plugin(require('./admin')) - if (options.contextify !== false) ctx.plugin(require('./contextify')) - if (options.broadcast !== false) ctx.plugin(require('./broadcast')) - if (options.debug) ctx.plugin(require('./debug'), options.debug) - if (options.info !== false) ctx.plugin(require('./info')) - if (options.usage !== false) ctx.plugin(require('./usage')) + if (config.admin !== false) ctx.plugin(require('./admin')) + if (config.contextify !== false) ctx.plugin(require('./contextify')) + if (config.debug) ctx.plugin(require('./debug'), config.debug) + if (config.info !== false) ctx.plugin(require('./info')) } diff --git a/packages/plugin-common/src/sender.ts b/packages/plugin-common/src/sender.ts new file mode 100644 index 0000000000..265b49be41 --- /dev/null +++ b/packages/plugin-common/src/sender.ts @@ -0,0 +1,49 @@ +import { Context, Group } from 'koishi-core' +import { CQCode } from 'koishi-utils' + +export interface SenderConfig { + broadcast?: false + echo?: false +} + +export default function apply(ctx: Context, config: SenderConfig = {}) { + config.broadcast !== false && ctx + .command('broadcast ', '全服广播', { authority: 4 }) + .before(session => !session.$app.database) + .option('forced', '-f 无视 silent 标签进行广播') + .option('only', '-o 仅向当前 Bot 负责的群进行广播') + .action(async ({ options, session }, message) => { + if (!message) return '请输入要发送的文本。' + if (!options.only) { + await ctx.broadcast(message, options.forced) + return + } + + let groups = await ctx.database.getAllGroups(['id', 'flag'], [session.selfId]) + if (!options.forced) { + groups = groups.filter(g => !(g.flag & Group.Flag.silent)) + } + await session.$bot.broadcast(groups.map(g => g.id), message) + }) + + config.echo !== false && ctx + .command('echo ', '向当前上下文发送消息', { authority: 2 }) + .option('anonymous', '-a 匿名发送消息', { authority: 3 }) + .option('forceAnonymous', '-A 匿名发送消息', { authority: 3 }) + .option('unescape', '-e 发送非转义的消息', { authority: 3 }) + .action(async ({ options }, message) => { + if (!message) return '请输入要发送的文本。' + + if (options.unescape) { + message = CQCode.unescape(message) + } + + if (options.forceAnonymous) { + message = CQCode.stringify('anonymous') + message + } else if (options.anonymous) { + message = CQCode.stringify('anonymous', { ignore: true }) + message + } + + return message + }) +} diff --git a/packages/plugin-common/src/usage.ts b/packages/plugin-common/src/usage.ts deleted file mode 100644 index 6c7a3a75bf..0000000000 --- a/packages/plugin-common/src/usage.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Context } from 'koishi-core' - -export function apply(ctx: Context) { - ctx.command('usage [...commands]', '查看指令的调用次数') - .userFields(['usage']) - .before(session => !session.$app.database) - .shortcut('调用次数', { fuzzy: true }) - .action(async ({ session }, ...commands) => { - const { usage } = session.$user - if (!commands.length) commands = Object.keys(usage) - const output: string[] = [] - for (const name of commands.sort()) { - if (name.startsWith('$')) continue - output.push(`${name}:${usage[name] || 0} 次`) - } - if (!output.length) return '你今日没有调用过消耗次数的指令。' - output.unshift('你今日各指令的调用次数为:') - return output.join('\n') - }) -} diff --git a/packages/plugin-common/tests/__snapshots__/admin.spec.ts.snap b/packages/plugin-common/tests/__snapshots__/admin.spec.ts.snap deleted file mode 100644 index c132db96d8..0000000000 --- a/packages/plugin-common/tests/__snapshots__/admin.spec.ts.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`group operations list actions: admin -G 1`] = ` -Array [ - "当前的可用指令有:set-flag, unset-flag。", -] -`; - -exports[`group operations list actions: admin -G foo 1`] = ` -Array [ - "指令未找到。当前的可用指令有:set-flag, unset-flag。", -] -`; - -exports[`user operations list actions: admin 1`] = ` -Array [ - "当前的可用指令有:set-auth, set-flag, unset-flag, clear-usage, show-usage。", -] -`; - -exports[`user operations list actions: admin foo 1`] = ` -Array [ - "指令未找到。当前的可用指令有:set-auth, set-flag, unset-flag, clear-usage, show-usage。", -] -`; diff --git a/packages/plugin-common/tests/__snapshots__/help.spec.ts.snap b/packages/plugin-common/tests/__snapshots__/help.spec.ts.snap deleted file mode 100644 index cb267773cb..0000000000 --- a/packages/plugin-common/tests/__snapshots__/help.spec.ts.snap +++ /dev/null @@ -1,150 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`help command alias and shortcut: help baz 1`] = ` -Array [ - "指令未找到。", -] -`; - -exports[`help command alias and shortcut: help baz-alias 1`] = ` -Array [ - "bar.baz -command with alias and shortcut -中文别名:baz-alias。 -相关全局指令:baz-shortcut。 -usage text", -] -`; - -exports[`help command alias and shortcut: help baz-shortcut 1`] = ` -Array [ - "bar.baz -command with alias and shortcut -中文别名:baz-alias。 -相关全局指令:baz-shortcut。 -usage text", -] -`; - -exports[`help command global help message: help -e 1`] = ` -Array [ - "当前可用的指令有(括号内为对应的最低权限等级,标有星号的表示含有子指令): - bar (1*) command with usage and examples - foo (1) command with options - help (0*) 显示帮助信息 -注:部分指令组已展开,故不再显示。 -群聊普通指令可以通过“@我+指令名”的方式进行触发。 -私聊或全局指令则不需要添加上述前缀,直接输入指令名即可触发。 -输入“全局指令”查看全部可用的全局指令。 -输入“帮助+指令名”查看特定指令的语法和使用示例。", -] -`; - -exports[`help command global help message: help -s 1`] = ` -Array [ - "当前可用的全局指令有:baz-shortcut,帮助,全局指令。", -] -`; - -exports[`help command global help message: help 1`] = ` -Array [ - "当前可用的指令有(括号内为对应的最低权限等级,标有星号的表示含有子指令): - foo (1) command with options - help (0*) 显示帮助信息 -群聊普通指令可以通过“@我+指令名”的方式进行触发。 -私聊或全局指令则不需要添加上述前缀,直接输入指令名即可触发。 -输入“全局指令”查看全部可用的全局指令。 -输入“帮助+指令名”查看特定指令的语法和使用示例。", -] -`; - -exports[`help command show command help: bar -h 1`] = ` -Array [ - "bar -command with usage and examples -usage text -使用示例: - example 1 - example 2 -可用的子指令有(括号内为对应的最低权限等级): - bar.baz (1) command with alias and shortcut", -] -`; - -exports[`help command show command help: foo -h 1`] = ` -Array [ - "foo -command with options -已调用次数:0/100。 -距离下次调用还需:0/1 秒。 -可用的选项有(括号内为额外要求的权限等级): - (2) -o [value] option(不计入总次数)", -] -`; - -exports[`help command show command help: help -h 1`] = ` -Array [ - "help [command] -显示帮助信息 -相关全局指令:帮助,全局指令。 -可用的选项有: - -e, --expand 展开指令列表 - -s, --shortcut 查看全局指令列表 -可用的子指令有(括号内为对应的最低权限等级,标有星号的表示含有子指令): - bar (1*) command with usage and examples", -] -`; - -exports[`help command show command help: help help -e 1`] = ` -Array [ - "help [command] -显示帮助信息 -相关全局指令:帮助,全局指令。 -可用的选项有: - -e, --expand 展开指令列表 - -s, --shortcut 查看全局指令列表 -可用的子指令有(括号内为对应的最低权限等级): -注:部分指令组已展开,故不再显示。", -] -`; - -exports[`help command show command help: help help 1`] = ` -Array [ - "help [command] -显示帮助信息 -相关全局指令:帮助,全局指令。 -可用的选项有: - -e, --expand 展开指令列表 - -s, --shortcut 查看全局指令列表 -可用的子指令有(括号内为对应的最低权限等级,标有星号的表示含有子指令): - bar (1*) command with usage and examples", -] -`; - -exports[`help command show command help: help help.foo 1`] = ` -Array [ - "help.foo -command without options -最低权限:2 级。", -] -`; - -exports[`help command show help with usage: help foo 1`] = ` -Array [ - "foo -command with options -已调用次数:1/100。 -距离下次调用还需:1/1 秒。 -可用的选项有(括号内为额外要求的权限等级): - (2) -o [value] option(不计入总次数)", -] -`; - -exports[`help without database: help foo 1`] = ` -Array [ - "foo -command with options -可用的选项有(括号内为额外要求的权限等级): - (2) -o [value] option(不计入总次数)", -] -`; diff --git a/packages/plugin-common/tests/admin.spec.ts b/packages/plugin-common/tests/admin.spec.ts index 7e99ed3cd1..e187a44e52 100644 --- a/packages/plugin-common/tests/admin.spec.ts +++ b/packages/plugin-common/tests/admin.spec.ts @@ -1,7 +1,6 @@ import { App } from 'koishi-test-utils' import { User, Group } from 'koishi-core' -import { enumKeys } from 'koishi-utils' -import { expect } from 'chai' +import { install } from '@sinonjs/fake-timers' import * as admin from '../src/admin' const app = new App({ mockDatabase: true }) @@ -9,102 +8,88 @@ const session = app.session(123, 321) app.plugin(admin) app.command('foo', { maxUsage: 10 }).action(({ session }) => session.$send('bar')) -app.command('bar', { maxUsage: 10 }).action(({ session }) => session.$send('foo')) +app.command('bar', { minInterval: 1000 }).action(({ session }) => session.$send('foo')) + +;((flags: Record) => { + flags[flags[1 << 4] = 'test'] = 1 << 4 +})(User.Flag) + +;((flags: Record) => { + flags[flags[1 << 4] = 'test'] = 1 << 4 +})(Group.Flag) before(async () => { - await app.start() await app.database.getUser(123, 4) await app.database.getUser(456, 3) await app.database.getUser(789, 4) - await app.database.getGroup(321, app.bots[0].selfId) - await app.database.getGroup(654, app.bots[0].selfId) -}) - -describe('basic features', () => { - it('check', async () => { - await session.shouldReply('admin -u 456 -g 321', '不能同时目标为指定用户和群。') - }) + await app.database.getGroup(321, app.selfId) + await app.database.getGroup(654, app.selfId) }) -describe('user operations', () => { - it('list actions', async () => { - await session.shouldMatchSnapshot('admin') - await session.shouldMatchSnapshot('admin foo') +describe('Admin Commands', () => { + it('user.auth', async () => { + await session.shouldReply('user.auth -u nan', '请指定正确的目标。') + await session.shouldReply('user.auth -u 321', '未找到指定的用户。') + await session.shouldReply('user.auth -u 789', '权限不足。') + await session.shouldReply('user.auth -u 456 -1', '参数错误。') + await session.shouldReply('user.auth -u 456 3', '用户数据未改动。') + await session.shouldReply('user.auth -u 456 4', '权限不足。') }) - it('check target', async () => { - await session.shouldReply('admin -u bar set-flag', '未指定目标。') - await session.shouldReply('admin -u 233 set-flag', '未找到指定的用户。') - await session.shouldReply('admin -u 789 show-usage', '权限不足。') + it('user.flag', async () => { + await session.shouldReply('user.flag -u 123', '未设置任何标记。') + await session.shouldReply('user.flag -l', '全部标记为:ignore, test。') + await session.shouldReply('user.flag -s foo', '未找到标记 foo。') + await session.shouldReply('user.flag -s test', '用户数据已修改。') + await session.shouldReply('user.flag', '当前的标记为:test。') + await session.shouldReply('user.flag -S ignore', '用户数据未改动。') + await session.shouldReply('user.flag', '当前的标记为:test。') }) - it('setAuth', async () => { - await session.shouldReply('admin -u 456 set-auth -1', '参数错误。') - await session.shouldReply('admin -u 456 set-auth 3', '用户权限未改动。') - await session.shouldReply('admin -u 456 set-auth 2', '用户权限已修改。') - await session.shouldReply('admin -u 456 set-auth 4', '权限不足。') + it('usage', async () => { + await session.shouldReply('usage', '今日没有调用过消耗次数的功能。') + await session.shouldReply('foo', 'bar') + await session.shouldReply('usage', '今日各功能的调用次数为:\nfoo:1') + await session.shouldReply('usage -c foo', '用户数据已修改。') + await session.shouldReply('usage', '今日没有调用过消耗次数的功能。') + await session.shouldReply('usage -s bar', '参数不足。') + await session.shouldReply('usage -s bar nan', '参数错误。') + await session.shouldReply('usage -s bar 2', '用户数据已修改。') + await session.shouldReply('usage bar', '今日 bar 功能的调用次数为:2') + await session.shouldReply('usage baz', '今日 baz 功能的调用次数为:0') + await session.shouldReply('usage -c', '用户数据已修改。') }) - const userFlags = enumKeys(User.Flag).join(', ') - - it('setFlag', async () => { - await session.shouldReply('admin -u 456 set-flag', `可用的标记有 ${userFlags}。`) - await session.shouldReply('admin -u 456 set-flag foo', '未找到标记 foo。') - await session.shouldReply('admin -u 456 set-flag ignore', '用户信息已修改。') - await expect(app.database.getUser(456)).eventually.to.have.property('flag', User.Flag.ignore) - }) - - it('unsetFlag', async () => { - await session.shouldReply('admin -u 456 unset-flag', `可用的标记有 ${userFlags}。`) - await session.shouldReply('admin -u 456 unset-flag foo', '未找到标记 foo。') - await session.shouldReply('admin -u 456 unset-flag ignore', '用户信息已修改。') - await expect(app.database.getUser(456)).eventually.to.have.property('flag', 0) - }) - - it('clearUsage', async () => { + it('timer', async () => { + const clock = install({ now: Date.now() }) + await session.shouldReply('timer', '当前没有生效的定时器。') await session.shouldReply('bar', 'foo') - await session.shouldReply('admin clear-usage foo', '用户信息已修改。') - await session.shouldReply('admin show-usage', '用户今日各指令的调用次数为:\nbar:1 次') - await session.shouldReply('admin clear-usage', '用户信息已修改。') - await session.shouldReply('admin show-usage', '用户今日没有调用过指令。') + await session.shouldReply('timer', '各定时器的生效时间为:\nbar:剩余 1 秒') + await session.shouldReply('timer -c bar', '用户数据已修改。') + await session.shouldReply('timer', '当前没有生效的定时器。') + await session.shouldReply('timer -s foo', '参数不足。') + await session.shouldReply('timer -s foo nan', '请输入合法的时间。') + await session.shouldReply('timer -s foo 2min', '用户数据已修改。') + await session.shouldReply('timer foo', '定时器 foo 的生效时间为:剩余 2 分钟') + await session.shouldReply('timer fox', '定时器 fox 当前并未生效。') + await session.shouldReply('timer -c', '用户数据已修改。') + clock.uninstall() }) -}) - -describe('group operations', () => { - it('list actions', async () => { - await session.shouldMatchSnapshot('admin -G') - await session.shouldMatchSnapshot('admin -G foo') - }) - - it('check target', async () => { - await session.shouldReply('admin -g bar set-flag', '未找到指定的群。') - }) - - const groupFlags = enumKeys(Group.Flag).join(', ') - - it('setFlag', async () => { - await session.shouldReply('admin -G set-flag', `可用的标记有 ${groupFlags}。`) - await session.shouldReply('admin -g 654 set-flag foo', '未找到标记 foo。') - await session.shouldReply('admin -g 654 set-flag silent', '群信息已修改。') - await expect(app.database.getGroup(654)).eventually.to.have.property('flag', Group.Flag.silent) - }) - - it('unsetFlag', async () => { - await session.shouldReply('admin -G unset-flag', `可用的标记有 ${groupFlags}。`) - await session.shouldReply('admin -g 654 unset-flag foo', '未找到标记 foo。') - await session.shouldReply('admin -g 654 unset-flag silent ignore', '群信息已修改。') - await expect(app.database.getGroup(654)).eventually.to.have.property('flag', 0) - }) -}) -describe('custom actions', () => { - it('user action', async () => { - admin.UserAction.add('test', session => session.$send('foo')) - await session.shouldReply('admin test', 'foo') + it('group.assignee', async () => { + await app.session(123).shouldReply('group.assign', '当前不在群上下文中,请使用 -g 参数指定目标群。') + await session.shouldReply('group.assign -g nan', '请指定正确的目标。') + await session.shouldReply('group.assign -g 123', '未找到指定的群。') + await session.shouldReply('group.assign -g 321', '群数据未改动。') + await session.shouldReply('group.assign -g 321 nan', '参数错误。') }) - it('group action', async () => { - admin.GroupAction.add('test', session => session.$send('bar')) - await session.shouldReply('admin -G test', 'bar') + it('group.flag', async () => { + await session.shouldReply('group.flag', '未设置任何标记。') + await session.shouldReply('group.flag -s foo', '未找到标记 foo。') + await session.shouldReply('group.flag -s test', '群数据已修改。') + await session.shouldReply('group.flag', '当前的标记为:test。') + await session.shouldReply('group.flag -S ignore', '群数据未改动。') + await session.shouldReply('group.flag', '当前的标记为:test。') }) }) diff --git a/packages/plugin-common/tests/broadcast.spec.ts b/packages/plugin-common/tests/broadcast.spec.ts deleted file mode 100644 index d5a4153bc1..0000000000 --- a/packages/plugin-common/tests/broadcast.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { MockedApp, BASE_SELF_ID } from 'koishi-test-utils' -import { startAll, stopAll, GroupFlag } from 'koishi-core' -import { broadcast } from '../src' -import 'koishi-database-memory' - -const app1 = new MockedApp({ database: { memory: {} } }) -const app2 = new MockedApp({ database: { memory: {} }, selfId: BASE_SELF_ID + 1 }) - -app1.plugin(broadcast) - -before(async () => { - await startAll() - await app1.database.getUser(123, 4) - await app1.database.getGroup(321, app1.selfId) - await app1.database.getGroup(654, app1.selfId) - await app1.database.setGroup(654, { flag: GroupFlag.silent }) - await app2.database.getGroup(987, app2.selfId) -}) - -after(() => stopAll()) - -utils.sleep.mockResolvedValue(undefined) - -beforeEach(() => utils.sleep.mockClear()) - -test('check message', async () => { - await app1.receiveMessage('user', 'broadcast', 123) - expect(utils.sleep).toBeCalledTimes(0) - app1.shouldHaveLastRequests([ - ['send_private_msg', { message: '请输入要发送的文本。', userId: 123 }], - ]) - app2.shouldHaveNoRequests() -}) - -test('basic support', async () => { - await app1.receiveMessage('user', 'broadcast foo bar', 123) - expect(utils.sleep).toBeCalledTimes(0) - app1.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 321 }], - ]) - app2.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 987 }], - ]) -}) - -test('self only', async () => { - await app1.receiveMessage('user', 'broadcast -o foo bar', 123) - expect(utils.sleep).toBeCalledTimes(0) - app1.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 321 }], - ]) - app2.shouldHaveNoRequests() -}) - -test('force emit', async () => { - await app1.receiveMessage('user', 'broadcast -f foo bar', 123) - expect(utils.sleep).toBeCalledTimes(1) - app1.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 321 }], - ['send_group_msg', { message: 'foo bar', groupId: 654 }], - ]) - app2.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 987 }], - ]) - app2.shouldHaveNoRequests() -}) - -test('self only & force emit', async () => { - await app1.receiveMessage('user', 'broadcast -of foo bar', 123) - expect(utils.sleep).toBeCalledTimes(1) - app1.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo bar', groupId: 321 }], - ['send_group_msg', { message: 'foo bar', groupId: 654 }], - ]) - app2.shouldHaveNoRequests() -}) diff --git a/packages/plugin-common/tests/echo.spec.ts b/packages/plugin-common/tests/echo.spec.ts deleted file mode 100644 index e7ee150334..0000000000 --- a/packages/plugin-common/tests/echo.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MockedApp } from 'koishi-test-utils' -import { echo } from '../src/echo' - -const app = new MockedApp() - -app.plugin(echo) - -describe('echo command', () => { - it('basic support', async () => { - await app.receiveMessage('user', 'echo foo', 123) - app.shouldHaveLastRequest('send_private_msg', { message: 'foo', userId: 123 }) - await app.receiveMessage('group', 'echo foo', 123, 456) - app.shouldHaveLastRequest('send_group_msg', { message: 'foo', groupId: 456 }) - await app.receiveMessage('discuss', 'echo foo', 123, 789) - app.shouldHaveLastRequest('send_discuss_msg', { message: 'foo', discussId: 789 }) - }) - - it('send to other contexts', async () => { - await app.receiveMessage('user', 'echo -u 456 foo', 123) - app.shouldHaveLastRequest('send_private_msg', { message: 'foo', userId: 456 }) - await app.receiveMessage('user', 'echo -g 456 -d 789 foo', 123) - app.shouldHaveLastRequests([ - ['send_group_msg', { message: 'foo', groupId: 456 }], - ['send_discuss_msg', { message: 'foo', discussId: 789 }], - ]) - }) -}) diff --git a/packages/plugin-common/tests/handler.spec.ts b/packages/plugin-common/tests/handler.spec.ts index 033a611afd..70089d6812 100644 --- a/packages/plugin-common/tests/handler.spec.ts +++ b/packages/plugin-common/tests/handler.spec.ts @@ -1,14 +1,14 @@ -import { MockedApp } from 'koishi-test-utils' +import { App } from 'koishi-test-utils' import { sleep } from 'koishi-utils' -import { requestHandler } from '../src' +import handler from '../src/handler' import 'koishi-database-memory' -let app: MockedApp +let app: App describe('type: undefined', () => { - beforeAll(async () => { - app = new MockedApp() - app.plugin(requestHandler) + before(async () => { + app = new App() + app.plugin(handler) await app.start() }) @@ -32,9 +32,9 @@ describe('type: undefined', () => { }) describe('type: string', () => { - beforeAll(async () => { - app = new MockedApp() - app.plugin(requestHandler, { + before(async () => { + app = new App() + app.plugin(handler, { handleFriend: 'foo', handleGroupAdd: 'bar', handleGroupInvite: 'baz', @@ -62,9 +62,9 @@ describe('type: string', () => { }) describe('type: boolean', () => { - beforeAll(async () => { - app = new MockedApp() - app.plugin(requestHandler, { + before(async () => { + app = new App() + app.plugin(handler, { handleFriend: false, handleGroupAdd: false, handleGroupInvite: false, @@ -92,9 +92,9 @@ describe('type: boolean', () => { }) describe('type: function', () => { - beforeAll(async () => { - app = new MockedApp() - app.plugin(requestHandler, { + before(async () => { + app = new App() + app.plugin(handler, { handleFriend: () => true, handleGroupAdd: () => true, handleGroupInvite: () => true, diff --git a/packages/plugin-common/tests/sender.spec.ts b/packages/plugin-common/tests/sender.spec.ts new file mode 100644 index 0000000000..6df5043303 --- /dev/null +++ b/packages/plugin-common/tests/sender.spec.ts @@ -0,0 +1,36 @@ +import { App } from 'koishi-test-utils' +import { fn } from 'jest-mock' +import { expect } from 'chai' +import sender from '../src/sender' + +const app = new App({ mockDatabase: true }) +const session = app.session(123) + +app.plugin(sender) + +before(async () => { + await app.database.getUser(123, 4) + await app.database.getGroup(456, 514) +}) + +describe('Sender Commands', () => { + it('echo', async () => { + await session.shouldReply('echo', '请输入要发送的文本。') + await session.shouldReply('echo foo', 'foo') + await session.shouldReply('echo -e []', '[]') + await session.shouldReply('echo -A foo', '[CQ:anonymous]foo') + await session.shouldReply('echo -a foo', '[CQ:anonymous,ignore=true]foo') + }) + + it('broadcast', async () => { + const sendGroupMsg = app.bots[0].sendGroupMsg = fn() + await session.shouldReply('broadcast', '请输入要发送的文本。') + expect(sendGroupMsg.mock.calls).to.have.length(0) + await session.shouldNotReply('broadcast foo') + expect(sendGroupMsg.mock.calls).to.have.length(1) + await session.shouldNotReply('broadcast -o foo') + expect(sendGroupMsg.mock.calls).to.have.length(2) + await session.shouldNotReply('broadcast -of foo') + expect(sendGroupMsg.mock.calls).to.have.length(3) + }) +}) diff --git a/packages/plugin-eval-addons/package.json b/packages/plugin-eval-addons/package.json index 63307fce0c..49d16dcf94 100644 --- a/packages/plugin-eval-addons/package.json +++ b/packages/plugin-eval-addons/package.json @@ -36,13 +36,13 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.2.0", + "koishi-core": "^2.2.1", "koishi-plugin-eval": "^2.0.1" }, "dependencies": { "js-yaml": "^3.14.0", "json5": "^2.1.3", - "koishi-utils": "^3.1.3", + "koishi-utils": "^3.1.4", "simple-git": "^2.20.1", "typescript": "^4.0.2" }, diff --git a/packages/plugin-eval/package.json b/packages/plugin-eval/package.json index 35d825c2b9..f6460d250f 100644 --- a/packages/plugin-eval/package.json +++ b/packages/plugin-eval/package.json @@ -37,12 +37,12 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { "koishi-test-utils": "^5.0.0" }, "dependencies": { - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-eval/src/internal.ts b/packages/plugin-eval/src/internal.ts index d9cf716ef9..3ccc542d8c 100644 --- a/packages/plugin-eval/src/internal.ts +++ b/packages/plugin-eval/src/internal.ts @@ -102,7 +102,6 @@ function instanceOf(value, construct) { } catch (ex) { // Never pass the handled exception through! throw new VMError('Unable to perform instanceOf check.') - // This exception actually never get to the user. It only instructs the caller to return null because we wasn't able to perform instanceOf check. } } diff --git a/packages/plugin-github/package.json b/packages/plugin-github/package.json index b81d5f6570..b3826402c3 100644 --- a/packages/plugin-github/package.json +++ b/packages/plugin-github/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-github", "description": "GitHub webhook plugin for Koishi", - "version": "2.0.1", + "version": "2.0.2", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -36,11 +36,11 @@ "koishi-test-utils": "^5.0.0" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { "@octokit/webhooks": "^7.11.2", "axios": "^0.20.0", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-github/src/events.ts b/packages/plugin-github/src/events.ts index c25c1056ae..1b9f3aebeb 100644 --- a/packages/plugin-github/src/events.ts +++ b/packages/plugin-github/src/events.ts @@ -3,6 +3,96 @@ import { EventNames } from '@octokit/webhooks' import { GetWebhookPayloadTypeFromEvent } from '@octokit/webhooks/dist-types/generated/get-webhook-payload-type-from-event' +export interface EventConfig { + commitComment?: boolean | { + created?: boolean + } + fork?: boolean + issueComment?: boolean | { + created?: boolean + deleted?: boolean + edited?: boolean + } + issues?: boolean | { + assigned?: boolean + closed?: boolean + deleted?: boolean + demilestoned?: boolean + edited?: boolean + labeled?: boolean + locked?: boolean + milestoned?: boolean + opened?: boolean + pinned?: boolean + reopened?: boolean + transferred?: boolean + unassigned?: boolean + unlabeled?: boolean + unlocked?: boolean + unpinned?: boolean + } + pullRequest?: boolean | { + assigned?: boolean + closed?: boolean + edited?: boolean + labeled?: boolean + locked?: boolean + merged?: boolean + opened?: boolean + readyForReview?: boolean + reopened?: boolean + reviewRequestRemoved?: boolean + reviewRequested?: boolean + synchronize?: boolean + unassigned?: boolean + unlabeled?: boolean + unlocked?: boolean + } + pullRequestReview?: boolean | { + dismissed?: boolean + edited?: boolean + submitted?: boolean + } + pullRequestReviewComment?: boolean | { + created?: boolean + deleted?: boolean + edited?: boolean + } + push?: boolean + star?: boolean | { + created?: boolean + deleted?: boolean + } +} + +export const defaultEvents: EventConfig = { + commitComment: { + created: true, + }, + fork: true, + issueComment: { + created: true, + }, + issues: { + closed: true, + opened: true, + }, + pullRequest: { + closed: true, + opened: true, + }, + pullRequestReview: { + submitted: true, + }, + pullRequestReviewComment: { + created: true, + }, + push: true, + star: { + created: true, + }, +} + export interface ReplyPayloads { link?: string react?: string @@ -19,18 +109,32 @@ export function addListeners(on: (event: T, handler: E .replace(/\n\s*\n/g, '\n') } - on('commit_comment.created', ({ repository, comment }) => { - const { full_name } = repository - const { user, url, html_url, commit_id, body, path, position } = comment - if (user.type === 'bot') return + type CommentEvent = 'commit_comment' | 'issue_comment' | 'pull_request_review_comment' + type CommandHandler = (payload: Payload) => [target: string, replies: ReplyPayloads] - return [[ - `[GitHub] ${user.login} commented on commit ${full_name}@${commit_id.slice(0, 6)}`, - `Path: ${path}`, - formatMarkdown(body), - ].join('\n'), { - link: html_url, - react: url + `/reactions`, + function onComment(event: E, handler: CommandHandler) { + on(event as CommentEvent, (payload) => { + const { user, body, html_url, url } = payload.comment + if (user.type === 'bot') return + + const [target, replies] = handler(payload) + if (payload.action === 'deleted') { + return [`[GitHub] ${user.login} deleted a comment on ${target}`] + } + + const operation = payload.action === 'created' ? 'commented' : 'edited a comment' + return [`[GitHub] ${user.login} ${operation} on ${target}\n${formatMarkdown(body)}`, { + link: html_url, + react: url + `/reactions`, + ...replies, + }] + }) + } + + onComment('commit_comment', ({ repository, comment }) => { + const { full_name } = repository + const { commit_id, path, position } = comment + return [`commit ${full_name}@${commit_id.slice(0, 6)}\nPath: ${path}`, { // https://docs.github.com/en/rest/reference/repos#create-a-commit-comment reply: [`https://api.github.com/repos/${full_name}/commits/${commit_id}/comments`, { path, position }], }] @@ -41,19 +145,11 @@ export function addListeners(on: (event: T, handler: E return [`[GitHub] ${sender.login} forked ${full_name} to ${forkee.full_name} (total ${forks_count} forks)`] }) - on('issue_comment.created', ({ comment, issue, repository }) => { + onComment('issue_comment', ({ issue, repository }) => { const { full_name } = repository const { number, comments_url } = issue - const { user, url, html_url, body } = comment - if (user.type === 'bot') return - const type = issue['pull_request'] ? 'pull request' : 'issue' - return [[ - `[GitHub] ${user.login} commented on ${type} ${full_name}#${number}`, - formatMarkdown(body), - ].join('\n'), { - link: html_url, - react: url + `/reactions`, + return [`${type} ${full_name}#${number}`, { reply: [comments_url], }] }) @@ -86,18 +182,11 @@ export function addListeners(on: (event: T, handler: E }] }) - on('pull_request_review_comment.created', ({ repository, comment, pull_request }) => { + onComment('pull_request_review_comment', ({ repository, comment, pull_request }) => { const { full_name } = repository const { number } = pull_request - const { user, path, body, html_url, url } = comment - if (user.type === 'bot') return - return [[ - `[GitHub] ${user.login} commented on pull request review ${full_name}#${number}`, - `Path: ${path}`, - formatMarkdown(body), - ].join('\n'), { - link: html_url, - react: url + `/reactions`, + const { path, url } = comment + return [`pull request review ${full_name}#${number}\nPath: ${path}`, { reply: [url], }] }) diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index fcb93c8729..562e6315d6 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -1,60 +1,24 @@ /* eslint-disable camelcase */ /* eslint-disable quote-props */ -import { Context, Session, User } from 'koishi-core' -import { CQCode, defineProperty, Logger, Time } from 'koishi-utils' -import { Webhooks } from '@octokit/webhooks' -import { Agent } from 'https' +import { Context, Session } from 'koishi-core' +import { camelize, CQCode, defineProperty, Time } from 'koishi-utils' import { encode } from 'querystring' -import axios, { AxiosError } from 'axios' -import { addListeners, ReplyPayloads } from './events' +import { addListeners, defaultEvents, EventConfig, ReplyPayloads } from './events' +import { Config, GitHub } from './server' + +export * from './server' declare module 'koishi-core/dist/app' { interface App { - githubWebhooks?: Webhooks - } -} - -declare module 'koishi-core/dist/database' { - interface User { - ghAccessToken?: string - ghRefreshToken?: string + github?: GitHub } } -User.extend(() => ({ - ghAccessToken: '', - ghRefreshToken: '', -})) - -export interface OAuth { - access_token: string - expires_in: string - refresh_token: string - refresh_token_expires_in: string - token_type: string - scope: string -} - type ReplyHandlers = { [K in keyof ReplyPayloads]: (payload: ReplyPayloads[K], session: Session, message: string) => Promise } -export interface Config { - agent?: Agent - secret?: string - webhook?: string - authorize?: string - prefix?: string - appId?: string - appSecret?: string - redirect?: string - promptTimeout?: number - replyTimeout?: number - requestTimeout?: number - repos?: Record -} - const defaultOptions: Config = { secret: '', prefix: '.', @@ -62,37 +26,24 @@ const defaultOptions: Config = { authorize: '/github/authorize', replyTimeout: Time.hour, repos: {}, + events: {}, } -const logger = new Logger('github') - export const name = 'github' export function apply(ctx: Context, config: Config = {}) { config = { ...defaultOptions, ...config } const { app, database, router } = ctx - const { appId, appSecret, prefix, redirect, webhook: path } = config - - const webhooks = new Webhooks({ ...config, path }) - defineProperty(app, 'githubWebhooks', webhooks) - - async function getTokens(params: any) { - const { data } = await axios.post('https://github.com/login/oauth/access_token', { - client_id: appId, - client_secret: appSecret, - ...params, - }, { - httpsAgent: config.agent, - headers: { Accept: 'application/json' }, - }) - return data - } + const { appId, prefix, redirect, webhook } = config + + const github = new GitHub(config) + defineProperty(app, 'github', github) router.get(config.authorize, async (ctx) => { const targetId = parseInt(ctx.query.state) if (Number.isNaN(targetId)) throw new Error('Invalid targetId') const { code, state } = ctx.query - const data = await getTokens({ code, state, redirect_uri: redirect }) + const data = await github.getTokens({ code, state, redirect_uri: redirect }) await database.setUser(targetId, { ghAccessToken: data.access_token, ghRefreshToken: data.refresh_token, @@ -113,61 +64,6 @@ export function apply(ctx: Context, config: Config = {}) { return '请点击下面的链接继续操作:\n' + url }) - type ReplySession = Session<'ghAccessToken' | 'ghRefreshToken'> - - async function plainRequest(url: string, session: ReplySession, params: any, accept: string) { - logger.debug('POST', url, params) - await axios.post(url, params, { - httpsAgent: config.agent, - timeout: config.requestTimeout, - headers: { - accept, - authorization: `token ${session.$user.ghAccessToken}`, - }, - }) - } - - async function authorize(session: Session, message: string) { - await session.$send(message) - const name = await session.$prompt(config.promptTimeout) - if (!name) return session.$send('输入超时。') - return session.$execute({ command: 'github', args: [name] }) - } - - async function request(url: string, session: ReplySession, params: any, accept = 'application/vnd.github.v3+json') { - if (!session.$user.ghAccessToken) { - return authorize(session, '如果想使用此功能,请对机器人进行授权。输入你的 GitHub 用户名。') - } - - try { - return await plainRequest(url, session, params, accept) - } catch (error) { - const { response } = error as AxiosError - if (response?.status !== 401) { - logger.warn(error) - return session.$send('发送失败。') - } - } - - try { - const data = await getTokens({ - refresh_token: session.$user.ghRefreshToken, - grant_type: 'refresh_token', - }) - session.$user.ghAccessToken = data.access_token - session.$user.ghRefreshToken = data.refresh_token - } catch (error) { - return authorize(session, '令牌已失效,需要重新授权。输入你的 GitHub 用户名。') - } - - try { - await plainRequest(url, session, params, accept) - } catch (error) { - logger.warn(error) - return session.$send('发送失败。') - } - } - const reactions = ['+1', '-1', 'laugh', 'confused', 'heart', 'hooray', 'rocket', 'eyes'] function formatReply(source: string) { @@ -180,17 +76,17 @@ export function apply(ctx: Context, config: Config = {}) { const replyHandlers: ReplyHandlers = { link: (url, session) => session.$send(url), - react: (url, session, content) => request(url, session, { content }, 'application/vnd.github.squirrel-girl-preview'), - reply: ([url, params], session, content) => request(url, session, { ...params, body: formatReply(content) }), + react: (url, session, content) => github.request(url, session, { content }, 'application/vnd.github.squirrel-girl-preview'), + reply: ([url, params], session, content) => github.request(url, session, { ...params, body: formatReply(content) }), } const interactions: Record = {} - router.post(path, (ctx, next) => { + router.post(webhook, (ctx, next) => { // workaround @octokit/webhooks for koa ctx.req['body'] = ctx.request.body ctx.status = 200 - return webhooks.middleware(ctx.req, ctx.res, next) + return github.middleware(ctx.req, ctx.res, next) }) ctx.on('before-attach-user', (session, fields) => { @@ -220,18 +116,34 @@ export function apply(ctx: Context, config: Config = {}) { }) addListeners((event, handler) => { - webhooks.on(event, async (callback) => { + const base = camelize(event.split('.', 1)[0]) as keyof EventConfig + github.on(event, async (callback) => { const { repository } = callback.payload + + // step 1: filter repository const groupIds = config.repos[repository.full_name] if (!groupIds) return + // step 2: filter event + const baseConfig = config.events[base] || {} + if (baseConfig === false) return + const action = camelize(callback.payload.action) + if (action && baseConfig !== true) { + const actionConfig = baseConfig[action] + if (actionConfig === false) return + if (actionConfig !== true && !(defaultEvents[base] || {})[action]) return + } + + // step 3: handle event const result = handler(callback.payload) if (!result) return + // step 4: broadcast message const [message, replies] = result const messageIds = await ctx.broadcast(groupIds, message) if (!replies) return + // step 5: save message ids for interactions for (const id of messageIds) { interactions[id] = replies } diff --git a/packages/plugin-github/src/server.ts b/packages/plugin-github/src/server.ts new file mode 100644 index 0000000000..78e81f7527 --- /dev/null +++ b/packages/plugin-github/src/server.ts @@ -0,0 +1,116 @@ +/* eslint-disable camelcase */ + +import { Webhooks } from '@octokit/webhooks' +import { EventConfig } from './events' +import axios, { AxiosError } from 'axios' +import { Session, User } from 'koishi-core' +import { Logger } from 'koishi-utils' + +declare module 'koishi-core/dist/database' { + interface User { + ghAccessToken?: string + ghRefreshToken?: string + } +} + +User.extend(() => ({ + ghAccessToken: '', + ghRefreshToken: '', +})) + +export interface Config { + secret?: string + webhook?: string + authorize?: string + prefix?: string + appId?: string + appSecret?: string + redirect?: string + promptTimeout?: number + replyTimeout?: number + requestTimeout?: number + repos?: Record + events?: EventConfig +} + +export interface OAuth { + access_token: string + expires_in: string + refresh_token: string + refresh_token_expires_in: string + token_type: string + scope: string +} + +type ReplySession = Session<'ghAccessToken' | 'ghRefreshToken'> + +const logger = new Logger('github') + +export class GitHub extends Webhooks { + constructor(public config: Config) { + super({ ...config, path: config.webhook }) + } + + async getTokens(params: any) { + const { data } = await axios.post('https://github.com/login/oauth/access_token', { + client_id: this.config.appId, + client_secret: this.config.appSecret, + ...params, + }, { + headers: { Accept: 'application/json' }, + }) + return data + } + + async _request(url: string, session: ReplySession, params: any, accept: string) { + logger.debug('POST', url, params) + await axios.post(url, params, { + timeout: this.config.requestTimeout, + headers: { + accept, + authorization: `token ${session.$user.ghAccessToken}`, + }, + }) + } + + async authorize(session: Session, message: string) { + await session.$send(message) + const name = await session.$prompt(this.config.promptTimeout) + if (!name) return session.$send('输入超时。') + return session.$execute({ command: 'github', args: [name] }) + } + + async request(url: string, session: ReplySession, params: any, accept = 'application/vnd.github.v3+json') { + if (!session.$user.ghAccessToken) { + return this.authorize(session, '如果想使用此功能,请对机器人进行授权。输入你的 GitHub 用户名。') + } + + try { + return await this._request(url, session, params, accept) + } catch (error) { + const { response } = error as AxiosError + if (response?.status !== 401) { + logger.warn(error) + return session.$send('发送失败。') + } + } + + try { + const data = await this.getTokens({ + refresh_token: session.$user.ghRefreshToken, + grant_type: 'refresh_token', + }) + session.$user.ghAccessToken = data.access_token + session.$user.ghRefreshToken = data.refresh_token + } catch (error) { + return this.authorize(session, '令牌已失效,需要重新授权。输入你的 GitHub 用户名。') + } + + try { + await this._request(url, session, params, accept) + } catch (error) { + logger.warn(error) + return session.$send('发送失败。') + } + } +} diff --git a/packages/plugin-github/tests/index.spec.ts b/packages/plugin-github/tests/index.spec.ts index 81bc448526..a0f47548ac 100644 --- a/packages/plugin-github/tests/index.spec.ts +++ b/packages/plugin-github/tests/index.spec.ts @@ -35,7 +35,7 @@ function check(file: string) { sendGroupMsg.mockClear() const payload = require(`./fixtures/${file}`) const [name] = file.split('.', 1) - await app.githubWebhooks.receive({ id: Random.uuid(), name, payload }) + await app.github.receive({ id: Random.uuid(), name, payload }) if (snapshot[file]) { expect(sendGroupMsg.mock.calls).to.have.length(1) expect(sendGroupMsg.mock.calls[0][1]).to.equal(snapshot[file].trim()) @@ -45,7 +45,7 @@ function check(file: string) { }) } -describe('koishi-plugin-github', () => { +describe('GitHub Plugin', () => { describe('Webhook Events', () => { const files = readdirSync(resolve(__dirname, 'fixtures')) files.forEach(file => { diff --git a/packages/plugin-image-search/package.json b/packages/plugin-image-search/package.json index bd6b5b91dc..677fea2dc4 100644 --- a/packages/plugin-image-search/package.json +++ b/packages/plugin-image-search/package.json @@ -37,12 +37,12 @@ "pixiv" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { "axios": "^0.20.0", "cheerio": "^1.0.0-rc.3", - "koishi-utils": "^3.1.3", + "koishi-utils": "^3.1.4", "nhentai-api": "^3.0.2" } } diff --git a/packages/plugin-mongo/package.json b/packages/plugin-mongo/package.json index 4643ba1b99..bacf4f657c 100644 --- a/packages/plugin-mongo/package.json +++ b/packages/plugin-mongo/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-mongo", "description": "MongoDB support for Koishi", - "version": "1.0.2", + "version": "1.0.3", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -36,12 +36,12 @@ "mysql" ], "devDependencies": { - "@types/mongodb": "^3.5.26" + "@types/mongodb": "^3.5.27" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { - "mongodb": "^3.6.0" + "mongodb": "^3.6.1" } } diff --git a/packages/plugin-monitor/package.json b/packages/plugin-monitor/package.json index 90d97de8a8..41888b16ed 100644 --- a/packages/plugin-monitor/package.json +++ b/packages/plugin-monitor/package.json @@ -21,9 +21,9 @@ }, "homepage": "https://github.com/koishijs/koishi#readme", "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-mysql/package.json b/packages/plugin-mysql/package.json index 3fcaf39dd1..014bc07997 100644 --- a/packages/plugin-mysql/package.json +++ b/packages/plugin-mysql/package.json @@ -36,10 +36,10 @@ "@types/mysql": "^2.15.15" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { - "koishi-utils": "^3.1.3", + "koishi-utils": "^3.1.4", "mysql": "^2.18.1" } } diff --git a/packages/plugin-mysql/src/database.ts b/packages/plugin-mysql/src/database.ts index f3498aa956..9de2ea8f55 100644 --- a/packages/plugin-mysql/src/database.ts +++ b/packages/plugin-mysql/src/database.ts @@ -104,8 +104,7 @@ export default class MysqlDatabase { }) } - select(table: string, fields: readonly (T extends string ? T : string & keyof T)[], conditional?: string, values?: readonly any[]): Promise[]> - select(table: string, fields: readonly (T extends string ? T : string & keyof T)[], conditional?: string, values?: readonly any[]): Promise + select(table: string, fields: readonly (string & keyof T)[], conditional?: string, values?: readonly any[]): Promise select(table: string, fields: string[], conditional?: string, values: readonly any[] = []) { logger.debug(`[select] ${table}: ${fields ? fields.join(', ') : '*'}`) const sql = 'SELECT ' diff --git a/packages/plugin-puppeteer/package.json b/packages/plugin-puppeteer/package.json index 5498fbf193..664e66a4ab 100644 --- a/packages/plugin-puppeteer/package.json +++ b/packages/plugin-puppeteer/package.json @@ -39,12 +39,12 @@ "koishi-test-utils": "^5.0.0" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { "chrome-finder": "^1.0.7", "pngjs": "^5.0.0", "puppeteer-core": "^5.2.1", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-rss/package.json b/packages/plugin-rss/package.json index 09d2d2c86f..c81ca80e9e 100644 --- a/packages/plugin-rss/package.json +++ b/packages/plugin-rss/package.json @@ -36,13 +36,13 @@ "rss" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { "koishi-test-utils": "^5.0.0" }, "dependencies": { "rss-feed-emitter": "^3.2.2", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-schedule/package.json b/packages/plugin-schedule/package.json index d91edad1f9..65332ed14b 100644 --- a/packages/plugin-schedule/package.json +++ b/packages/plugin-schedule/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-schedule", "description": "Schedule plugin for Koishi", - "version": "2.0.1", + "version": "2.0.2", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -34,14 +34,14 @@ "task" ], "devDependencies": { - "koishi-plugin-mongo": "^1.0.2", + "koishi-plugin-mongo": "^1.0.3", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^5.0.0" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-schedule/src/database.ts b/packages/plugin-schedule/src/database.ts index 672078aae3..69802f95d3 100644 --- a/packages/plugin-schedule/src/database.ts +++ b/packages/plugin-schedule/src/database.ts @@ -48,10 +48,16 @@ extendDatabase('koishi-plugin-mysql', { extendDatabase('koishi-plugin-mongo', { async createSchedule(time, interval, command, session) { - const result = await this.db.collection('schedule').insertOne( - { time, assignee: session.selfId, interval, command, session }, - ) - return { time, assignee: session.selfId, interval, command, session, id: result.insertedId } + let _id = 1 + const [latest] = await this.db.collection('schedule').find().sort('_id', -1).limit(1).toArray() + if (latest) _id = latest._id + 1 + const data = { time, interval, command, assignee: session.selfId } + const result = await this.db.collection('schedule').insertOne({ + _id, + ...data, + session: JSON.stringify(session), + }) + return { ...data, session, id: result.insertedId } }, removeSchedule(_id) { @@ -60,13 +66,18 @@ extendDatabase('koishi-plugin-mongo', { async getSchedule(_id) { const res = await this.db.collection('schedule').findOne({ _id }) - if (res) res.id = res._id + if (res) { + res.id = res._id + res.session = JSON.parse(res.session) + } return res }, async getAllSchedules(assignees) { const $in = assignees || await this.app.getSelfIds() return await this.db.collection('schedule') - .find({ assignee: { $in } }).map(doc => ({ ...doc, id: doc._id })).toArray() + .find({ assignee: { $in } }) + .map(doc => ({ ...doc, id: doc._id, session: JSON.parse(doc.session) })) + .toArray() }, }) diff --git a/packages/plugin-schedule/src/index.ts b/packages/plugin-schedule/src/index.ts index 4e0e42ce78..4a2eb2e9b6 100644 --- a/packages/plugin-schedule/src/index.ts +++ b/packages/plugin-schedule/src/index.ts @@ -6,18 +6,23 @@ export * from './database' const logger = new Logger('schedule') -function inspectSchedule({ id, session, interval, command, time }: Schedule) { +function prepareSchedule({ id, session, interval, command, time }: Schedule) { const now = Date.now() const date = time.valueOf() const { database } = session.$app - logger.debug('inspect', command) + logger.debug('prepare %d: %c at %s', id, command, time) + + function executeSchedule() { + logger.debug('execute %d: %c', id, command) + return session.$execute(command) + } if (!interval) { if (date < now) return database.removeSchedule(id) return setTimeout(async () => { if (!await database.getSchedule(id)) return - session.$execute(command) - database.removeSchedule(id) + await database.removeSchedule(id) + await executeSchedule() }, date - now) } @@ -26,9 +31,9 @@ function inspectSchedule({ id, session, interval, command, time }: Schedule) { if (!await database.getSchedule(id)) return const timer = setInterval(async () => { if (!await database.getSchedule(id)) return clearInterval(timer) - session.$execute(command) + await executeSchedule() }, interval) - session.$execute(command) + await executeSchedule() }, timeout) } @@ -46,7 +51,7 @@ export function apply(ctx: Context) { schedules.forEach((schedule) => { if (!ctx.bots[schedule.assignee]) return schedule.session = new Session(ctx.app, schedule.session) - inspectSchedule(schedule) + prepareSchedule(schedule) }) }) @@ -96,7 +101,7 @@ export function apply(ctx: Context) { } const schedule = await database.createSchedule(time, interval, options.rest, session) - inspectSchedule(schedule) + prepareSchedule(schedule) return `日程已创建,编号为 ${schedule.id}。` }) } diff --git a/packages/plugin-status/package.json b/packages/plugin-status/package.json index 404848e589..a4010b77d1 100644 --- a/packages/plugin-status/package.json +++ b/packages/plugin-status/package.json @@ -32,16 +32,16 @@ "status" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { "@types/cross-spawn": "^6.0.2", - "koishi-plugin-mongo": "^1.0.2", + "koishi-plugin-mongo": "^1.0.3", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^5.0.0" }, "dependencies": { "cross-spawn": "^7.0.3", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-teach/package.json b/packages/plugin-teach/package.json index 32f65c2ab9..f9e6724604 100644 --- a/packages/plugin-teach/package.json +++ b/packages/plugin-teach/package.json @@ -38,16 +38,16 @@ "conversation" ], "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "devDependencies": { - "koishi-plugin-mongo": "^1.0.2", + "koishi-plugin-mongo": "^1.0.3", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^5.0.0" }, "dependencies": { "axios": "^0.20.0", - "koishi-utils": "^3.1.3", + "koishi-utils": "^3.1.4", "leven": "^3.1.0", "regexpp": "^3.1.0" } diff --git a/packages/plugin-tools/package.json b/packages/plugin-tools/package.json index 845a341501..cbeb17084c 100644 --- a/packages/plugin-tools/package.json +++ b/packages/plugin-tools/package.json @@ -25,13 +25,13 @@ "@types/qrcode": "^1.3.5" }, "peerDependencies": { - "koishi-core": "^2.2.0" + "koishi-core": "^2.2.1" }, "dependencies": { "axios": "^0.20.0", "cheerio": "^1.0.0-rc.3", "qrcode": "^1.4.4", "xml-js": "^1.6.11", - "koishi-utils": "^3.1.3" + "koishi-utils": "^3.1.4" } } diff --git a/packages/plugin-tools/src/magi.ts b/packages/plugin-tools/src/magi.ts index 56ddbe7a32..103117e672 100644 --- a/packages/plugin-tools/src/magi.ts +++ b/packages/plugin-tools/src/magi.ts @@ -43,10 +43,13 @@ export function apply(ctx: Context) { case 'description': case 'tag': case 'synonym': - message += `\n${tagMap[scope]}: ` + articles.map(a => a.object + (options.confidence ? ` (${a.confidence})` : '')).join(', ') + message += `\n${tagMap[scope]}: ` + + articles.map(a => a.object + (options.confidence ? ` (${a.confidence})` : '')).join(', ') break case 'mixed': - message += '\n' + articles.map(a => (title.includes(a.subject) ? a.object : a.subject) + (options.confidence ? ` (${a.confidence})` : '')).join(', ') + message += '\n' + + articles.map(a => (title.includes(a.subject) ? a.object : a.subject) + + (options.confidence ? ` (${a.confidence})` : '')).join(', ') break case 'property': for (const { object, confidence, predicate } of articles) { diff --git a/packages/plugin-tools/src/maya.ts b/packages/plugin-tools/src/maya.ts index cd7353bb8f..42ebdf957f 100644 --- a/packages/plugin-tools/src/maya.ts +++ b/packages/plugin-tools/src/maya.ts @@ -2,9 +2,23 @@ import { Context } from 'koishi-core' const dayInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] const monthNames = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] -const haabMonthNames = ['Pop', "Wo'", 'Sip', "Sotz'", 'Sek', 'Xul', "Yaxk'in", 'Mol', "Ch'en", 'Yax', "Sak'", 'Keh', 'Mak', "K'ank'in", 'Muwan', 'Pax', "K'ayab", "Kumk'u", 'Wayeb'] -const longCountUnits = ['Kin', 'Uinal', 'Tun', "Ka'tun", "Bak'tun", 'Pictun', 'Kalabtun', "K'inchiltun", 'Alautun'] -const dayNames = ['Ajaw', 'Imix', "Ik'", "Ak'bal", "K'an", 'Chikchan', 'Kimi', "Manik'", 'Lamat', 'Muluk', 'Ok', 'Chuwen', 'Eb', 'Ben', 'Ix', 'Men', "K'ib", 'Kaban', "Etz'nab", 'Kawak'] + +const haabMonthNames = [ + 'Pop', "Wo'", 'Sip', "Sotz'", 'Sek', 'Xul', "Yaxk'in", + 'Mol', "Ch'en", 'Yax', "Sak'", 'Keh', 'Mak', + "K'ank'in", 'Muwan', 'Pax', "K'ayab", "Kumk'u", 'Wayeb', +] + +const longCountUnits = [ + 'Kin', 'Uinal', 'Tun', "Ka'tun", "Bak'tun", + 'Pictun', 'Kalabtun', "K'inchiltun", 'Alautun', +] + +const dayNames = [ + 'Ajaw', 'Imix', "Ik'", "Ak'bal", "K'an", 'Chikchan', + 'Kimi', "Manik'", 'Lamat', 'Muluk', 'Ok', 'Chuwen', 'Eb', + 'Ben', 'Ix', 'Men', "K'ib", 'Kaban', "Etz'nab", 'Kawak', +] function isLeap(year: number) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) diff --git a/packages/plugin-tools/src/translate.ts b/packages/plugin-tools/src/translate.ts index 25e977a9a2..c9d67a12d0 100644 --- a/packages/plugin-tools/src/translate.ts +++ b/packages/plugin-tools/src/translate.ts @@ -26,6 +26,10 @@ export interface TranslateOptions { youdaoSecret?: string } +function encrypt(source: string) { + return createHash('md5').update(source).digest('hex') // lgtm [js/weak-cryptographic-algorithm] +} + export function apply(ctx: Context, config: TranslateOptions) { const appKey = assertProperty(config, 'youdaoAppKey') const secret = assertProperty(config, 'youdaoSecret') @@ -42,7 +46,7 @@ export function apply(ctx: Context, config: TranslateOptions) { const qShort = q.length > 20 ? q.slice(0, 10) + q.length + q.slice(-10) : q const from = options.from const to = options.to - const sign = createHash('md5').update(appKey + qShort + salt + secret).digest('hex') // lgtm [js/weak-cryptographic-algorithm] + const sign = encrypt(appKey + qShort + salt + secret) const { data } = await axios.get('http://openapi.youdao.com/api', { params: { q, appKey, salt, from, to, sign }, })