From fb03d329ba83361e852b6bf126923b769ca31d5c Mon Sep 17 00:00:00 2001 From: Shigma Date: Tue, 8 Aug 2023 02:41:15 +0800 Subject: [PATCH] chore: migrate rate-limit to common repository --- plugins/common/rate-limit/.npmignore | 2 - plugins/common/rate-limit/package.json | 56 ------ plugins/common/rate-limit/src/admin.ts | 67 ------- plugins/common/rate-limit/src/index.ts | 179 ------------------ .../common/rate-limit/src/locales/zh-CN.yml | 28 --- plugins/common/rate-limit/tests/index.spec.ts | 140 -------------- plugins/common/rate-limit/tsconfig.json | 10 - 7 files changed, 482 deletions(-) delete mode 100644 plugins/common/rate-limit/.npmignore delete mode 100644 plugins/common/rate-limit/package.json delete mode 100644 plugins/common/rate-limit/src/admin.ts delete mode 100644 plugins/common/rate-limit/src/index.ts delete mode 100644 plugins/common/rate-limit/src/locales/zh-CN.yml delete mode 100644 plugins/common/rate-limit/tests/index.spec.ts delete mode 100644 plugins/common/rate-limit/tsconfig.json diff --git a/plugins/common/rate-limit/.npmignore b/plugins/common/rate-limit/.npmignore deleted file mode 100644 index 7e5fcbc18b..0000000000 --- a/plugins/common/rate-limit/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -tsconfig.tsbuildinfo diff --git a/plugins/common/rate-limit/package.json b/plugins/common/rate-limit/package.json deleted file mode 100644 index 54f85c1d5c..0000000000 --- a/plugins/common/rate-limit/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@koishijs/plugin-rate-limit", - "description": "Set Rate Limits for Commands in Koishi", - "version": "1.3.3", - "main": "lib/index.js", - "typings": "lib/index.d.ts", - "files": [ - "lib" - ], - "author": "Shigma ", - "license": "MIT", - "scripts": { - "lint": "eslint src --ext .ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/koishijs/koishi.git", - "directory": "plugins/common/rate-limit" - }, - "bugs": { - "url": "https://github.com/koishijs/koishi/issues" - }, - "homepage": "https://koishi.chat/plugins/common/rate-limit.html", - "keywords": [ - "bot", - "chatbot", - "koishi", - "plugin", - "rate-limit" - ], - "koishi": { - "category": "tool", - "description": { - "en": "Set rate limits for commands", - "zh": "为指令添加频率限制" - }, - "service": { - "required": [ - "database" - ] - }, - "locales": [ - "zh" - ] - }, - "peerDependencies": { - "koishi": "^4.14.0" - }, - "devDependencies": { - "@koishijs/plugin-database-memory": "^2.3.4", - "@koishijs/plugin-admin": "^1.4.0", - "@koishijs/plugin-help": "^2.3.0", - "@koishijs/plugin-mock": "^2.5.0", - "koishi": "^4.14.0" - } -} diff --git a/plugins/common/rate-limit/src/admin.ts b/plugins/common/rate-limit/src/admin.ts deleted file mode 100644 index c1fe688260..0000000000 --- a/plugins/common/rate-limit/src/admin.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Context } from 'koishi' -import {} from '@koishijs/plugin-admin' - -export const name = 'usage-admin' - -export function apply(ctx: Context) { - ctx.command('usage [key] [value:posint]', { authority: 1, admin: { user: true } }) - .userFields(['usage']) - .option('set', '-s', { authority: 4 }) - .option('clear', '-c', { authority: 4 }) - .action(({ session, options }, name, count) => { - const { user } = session - if (options.clear) { - name ? delete user.usage[name] : user.usage = {} - return - } - - if (options.set) { - if (!count) return session.text('internal.insufficient-arguments') - user.usage[name] = count - return - } - - if (name) return session.text('.present', [name, user.usage[name] || 0]) - const output: string[] = [] - for (const name of Object.keys(user.usage).sort()) { - if (name.startsWith('_')) continue - output.push(`${name}:${user.usage[name]}`) - } - if (!output.length) return session.text('.none') - output.unshift(session.text('.list')) - return output.join('\n') - }) - - ctx.command('timer [key] [value:date]', { authority: 1, admin: { user: true } }) - .userFields(['timers']) - .option('set', '-s', { authority: 4 }) - .option('clear', '-c', { authority: 4 }) - .action(({ session, options }, name, value) => { - const { user } = session - if (options.clear) { - name ? delete user.timers[name] : user.timers = {} - return - } - - if (options.set) { - if (!value) return session.text('internal.insufficient-arguments') - user.timers[name] = +value - return - } - - const now = Date.now() - if (name) { - const delta = user.timers[name] - now - if (delta > 0) return session.text('.present', [name, delta]) - return session.text('.absent', [name]) - } - const output: string[] = [] - for (const name of Object.keys(user.timers).sort()) { - if (name.startsWith('_')) continue - output.push(session.text('.item', [name, user.timers[name] - now])) - } - if (!output.length) return session.text('.none') - output.unshift(session.text('.list')) - return output.join('\n') - }) -} diff --git a/plugins/common/rate-limit/src/index.ts b/plugins/common/rate-limit/src/index.ts deleted file mode 100644 index 02137789ac..0000000000 --- a/plugins/common/rate-limit/src/index.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Argv, Command, Computed, Context, Dict, Schema, Session, Time, User } from 'koishi' -import {} from '@koishijs/plugin-help' -import * as admin from './admin' -import zhCN from './locales/zh-CN.yml' - -declare module 'koishi' { - namespace Command { - interface Config { - /** usage identifier */ - usageName?: string - /** max usage per day */ - maxUsage?: Computed - /** min interval */ - minInterval?: Computed - /** @deprecated use filter instead */ - bypassAuthority?: Computed - } - } - - interface User { - usage: Dict - timers: Dict - } - - namespace Argv { - interface OptionConfig { - notUsage?: boolean - } - } -} - -export interface Config {} - -export const name = 'rate-limit' -export const using = ['database'] as const -export const Config: Schema = Schema.object({}) - -export function apply(ctx: Context) { - ctx.i18n.define('zh-CN', zhCN) - - ctx.model.extend('user', { - usage: 'json', - timers: 'json', - }) - - ctx.schema.extend('command', Schema.object({ - usageName: Schema.string().description('调用次数的标识符。'), - maxUsage: Schema.computed(Schema.number(), { userFields: ['authority'] }).description('每天的调用次数上限。'), - minInterval: Schema.computed(Schema.number(), { userFields: ['authority'] }).description('连续调用的最小间隔。'), - }), 800) - - ctx.schema.extend('command-option', Schema.object({ - notUsage: Schema.boolean().description('不计入调用次数。'), - }), 800) - - // add user fields - ctx.before('command/attach-user', ({ command, options = {} }, fields) => { - if (!command) return - const { maxUsage, minInterval, bypassAuthority } = command.config - let shouldFetchUsage = !!(maxUsage || minInterval) - for (const { name, notUsage } of Object.values(command._options)) { - // --help is not a usage (#772) - if (name === 'help') continue - if (name in options && notUsage) shouldFetchUsage = false - } - if (shouldFetchUsage) { - fields.add('authority') - if (maxUsage) fields.add('usage') - if (minInterval) fields.add('timers') - } - if (bypassAuthority) fields.add('authority') - }) - - function bypassRateLimit(session: Session<'authority'>, command: Command) { - if (!session.user) return true - const bypassAuthority = session.resolve(command.config.bypassAuthority) - if (session.user.authority >= bypassAuthority) return true - } - - // check user - ctx.before('command/execute', (argv: Argv<'authority' | 'usage' | 'timers'>) => { - const { session, options, command } = argv - if (bypassRateLimit(session, command)) return - - function sendHint(path: string, ...param: any[]) { - if (!command.config.showWarning) return '' - return session.text([`.${path}`, `internal.${path}`], param) - } - - let isUsage = true - for (const { name, notUsage } of Object.values(command._options)) { - if (name in options && notUsage) isUsage = false - } - - // check usage - if (isUsage) { - const name = getUsageName(command) - const minInterval = session.resolve(command.config.minInterval) - const maxUsage = session.resolve(command.config.maxUsage) - - // interval check should be performed before usage check - // https://github.com/koishijs/koishi/issues/752 - if (minInterval > 0 && checkTimer(name, session.user, minInterval)) { - return sendHint('too-frequent') - } - - if (maxUsage < Infinity && checkUsage(name, session.user, maxUsage)) { - return sendHint('usage-exhausted') - } - } - }) - - // extend command help - ctx.on('help/command', (output, command, session: Session<'authority' | 'usage' | 'timers'>) => { - if (bypassRateLimit(session, command)) return - - const name = getUsageName(command) - const maxUsage = session.resolve(command.config.maxUsage) ?? Infinity - const minInterval = session.resolve(command.config.minInterval) ?? 0 - - if (maxUsage < Infinity) { - const count = getUsage(name, session.user) - output.push(session.text('internal.command-max-usage', [Math.min(count, maxUsage), maxUsage])) - } - - if (minInterval > 0) { - const due = session.user.timers[name] - const nextUsage = due ? (Math.max(0, due - Date.now()) / 1000).toFixed() : 0 - output.push(session.text('internal.command-min-interval', [nextUsage, minInterval / 1000])) - } - }) - - // extend command option - ctx.on('help/option', (output, option, command, session: Session<'authority'>) => { - if (bypassRateLimit(session, command)) return output - const maxUsage = session.resolve(command.config.maxUsage) - if (option.notUsage && maxUsage !== Infinity) { - output += session.text('internal.option-not-usage') - } - return output - }) - - ctx.plugin(admin) -} - -export function getUsageName(command: Command) { - return command.config.usageName || command.name -} - -export function getUsage(name: string, user: Pick) { - const _date = Time.getDateNumber() - if (user.usage._date !== _date) { - user.usage = { _date } - } - return user.usage[name] || 0 -} - -export function checkUsage(name: string, user: Pick, maxUsage?: number) { - if (!user.usage) return - const count = getUsage(name, user) - if (count >= maxUsage) return true - if (maxUsage) { - user.usage[name] = count + 1 - } -} - -export function checkTimer(name: string, { timers }: Pick, offset?: number) { - const now = Date.now() - if (!(now <= timers._date)) { - for (const key in timers) { - if (now > timers[key]) delete timers[key] - } - timers._date = now + Time.day - } - if (now <= timers[name]) return true - if (offset !== undefined) { - timers[name] = now + offset - } -} diff --git a/plugins/common/rate-limit/src/locales/zh-CN.yml b/plugins/common/rate-limit/src/locales/zh-CN.yml deleted file mode 100644 index 61b0c8356e..0000000000 --- a/plugins/common/rate-limit/src/locales/zh-CN.yml +++ /dev/null @@ -1,28 +0,0 @@ -internal: - usage-exhausted: 调用次数已达上限。 - too-frequent: 调用过于频繁,请稍后再试。 - option-not-usage: ' (不计入调用)' - command-max-usage: 已调用次数:{0}/{1}。 - command-min-interval: 距离下次调用还需:{0}/{1} 秒。 - -commands: - usage: - description: 调用次数信息 - options: - set: 设置调用次数 - clear: 清空调用次数 - messages: - present: 今日 {0} 功能的调用次数为:{1} - list: 今日各功能的调用次数为: - none: 今日没有调用过消耗次数的功能。 - timer: - description: 定时器信息 - options: - set: 设置定时器 - clear: 清空定时器 - messages: - present: 定时器 {0} 的生效时间为:剩余 - absent: 定时器 {0} 当前并未生效。 - list: 各定时器的生效时间为: - item: '{0}:剩余 ' - none: 当前没有生效的定时器。 diff --git a/plugins/common/rate-limit/tests/index.spec.ts b/plugins/common/rate-limit/tests/index.spec.ts deleted file mode 100644 index 0209882767..0000000000 --- a/plugins/common/rate-limit/tests/index.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { App, Time } from 'koishi' -import mock from '@koishijs/plugin-mock' -import memory from '@koishijs/plugin-database-memory' -import { install } from '@sinonjs/fake-timers' -import * as admin from '@koishijs/plugin-admin' -import * as help from '@koishijs/plugin-help' -import * as rate from '../src' - -const app = new App() -let now = Date.now() - -app.plugin(admin) -app.plugin(help) -app.plugin(mock) -app.plugin(memory) -app.plugin(rate) - -const client1 = app.mock.client('123') -const client2 = app.mock.client('456') - -before(async () => { - await app.start() - await app.mock.initUser('123', 4, { - usage: { foo: 1, _date: Time.getDateNumber() }, - timers: { bar: now + Time.minute, _date: now + Time.day }, - }) -}) - -describe('@koishijs/plugin-rate-limit', () => { - describe('maxUsage', () => { - const cmd = app - .command('foo', '指令1', { maxUsage: 3 }) - .option('opt1', '选项1', { notUsage: true }) - .option('opt2', '选项2') - .action(() => 'test') - - it('Extended Help', async () => { - await client1.shouldReply('help foo -H', [ - '指令:foo', - '指令1', - '已调用次数:1/3。', - '可用的选项有:', - ' -h, --help 显示此信息 (不计入调用)', - ' --opt1 选项1 (不计入调用)', - ' --opt2 选项2', - ].join('\n')) - }) - - it('Runtime Check', async () => { - cmd.config.showWarning = true - await client1.shouldReply('foo', 'test') - await client1.shouldReply('foo', 'test') - await client1.shouldReply('foo', '调用次数已达上限。') - await client2.shouldReply('foo', 'test') - await client1.shouldReply('foo --opt1', 'test') - cmd.config.showWarning = false - await client1.shouldNotReply('foo') - }) - - it('Modify Usages', async () => { - await client1.shouldReply('usage', '今日各功能的调用次数为:\nfoo:3') - await client1.shouldReply('usage -c foo', '用户数据已修改。') - await client1.shouldReply('usage', '今日没有调用过消耗次数的功能。') - await client1.shouldReply('usage -s bar', '缺少参数,输入帮助以查看用法。') - await client1.shouldReply('usage -s bar nan', '参数 value 输入无效,请提供一个正整数。') - await client1.shouldReply('usage -s bar 2', '用户数据已修改。') - await client1.shouldReply('usage bar', '今日 bar 功能的调用次数为:2') - await client1.shouldReply('usage baz', '今日 baz 功能的调用次数为:0') - await client1.shouldReply('usage -c', '用户数据已修改。') - await client1.shouldReply('usage', '今日没有调用过消耗次数的功能。') - }) - }) - - describe('minInterval', () => { - const cmd = app - .command('bar', '指令2', { minInterval: 3 * Time.minute, hideOptions: true }) - .option('opt1', '选项1', { notUsage: true }) - .option('opt2', '选项2') - .action(() => 'test') - - it('Extended Help', async () => { - const clock = install({ now }) - try { - await client1.shouldReply('help bar', '指令:bar\n指令2\n距离下次调用还需:60/180 秒。') - await client2.shouldReply('help bar', '指令:bar\n指令2\n距离下次调用还需:0/180 秒。') - } finally { - clock.uninstall() - } - }) - - it('Runtime Check', async () => { - const clock = install({ now }) - try { - cmd.config.showWarning = true - await client1.shouldReply('bar', '调用过于频繁,请稍后再试。') - await client2.shouldReply('bar', 'test') - clock.tick(Time.minute + 1) - now = clock.now - await client1.shouldReply('bar', 'test') - await client1.shouldReply('bar --opt1', 'test') - cmd.config.showWarning = false - await client2.shouldNotReply('bar') - } finally { - clock.uninstall() - } - }) - - it('Modify Timers', async () => { - const clock = install({ now }) - try { - await client1.shouldReply('timer', '各定时器的生效时间为:\nbar:剩余 3 分钟') - await client1.shouldReply('timer -c bar', '用户数据已修改。') - await client1.shouldReply('timer', '当前没有生效的定时器。') - await client1.shouldReply('timer -s foo', '缺少参数,输入帮助以查看用法。') - await client1.shouldReply('timer -s foo nan', '参数 value 输入无效,请输入合法的时间。') - await client1.shouldReply('timer -s foo 2min', '用户数据已修改。') - await client1.shouldReply('timer foo', '定时器 foo 的生效时间为:剩余 2 分钟') - await client1.shouldReply('timer fox', '定时器 fox 当前并未生效。') - await client1.shouldReply('timer -c', '用户数据已修改。') - await client1.shouldReply('timer', '当前没有生效的定时器。') - } finally { - clock.uninstall() - } - }) - }) - - describe('bypassAuthority', () => { - it('bypass maxUsage', async () => { - const cmd = app - .command('qux', '指令3', { maxUsage: 1, bypassAuthority: 3 }) - .action(() => 'test') - - await client2.shouldReply('qux', 'test') - await client2.shouldReply('qux', '调用次数已达上限。') - await client1.shouldReply('qux', 'test') - await client1.shouldReply('qux', 'test') - await client1.shouldReply('qux', 'test') - }) - }) -}) diff --git a/plugins/common/rate-limit/tsconfig.json b/plugins/common/rate-limit/tsconfig.json deleted file mode 100644 index 72a43d6a02..0000000000 --- a/plugins/common/rate-limit/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib", - }, - "include": [ - "src", - ], -} \ No newline at end of file