Skip to content

Commit

Permalink
feat(core): usage api
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 3, 2020
1 parent 99c4870 commit 797bde2
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 43 deletions.
55 changes: 33 additions & 22 deletions packages/koishi-core/src/command.ts
Expand Up @@ -41,7 +41,7 @@ export interface CommandConfig {
maxUsage?: UserType<number>
maxUsageText?: string
minInterval?: UserType<number>
showWarning?: boolean
showWarning?: boolean | number
noHelpOption?: boolean
}

Expand Down Expand Up @@ -247,17 +247,17 @@ export class Command {
if (this.config.checkArgCount) {
const nextArg = this._argsDef[args.length]
if (nextArg?.required) {
return meta.$send(messages.INSUFFICIENT_ARGUMENTS)
return this._sendHint(CommandHint.INSUFFICIENT_ARGUMENTS, meta)
}
const finalArg = this._argsDef[this._argsDef.length - 1]
if (args.length > this._argsDef.length && !finalArg.noSegment && !finalArg.variadic) {
return meta.$send(messages.REDUNANT_ARGUMENTS)
return this._sendHint(CommandHint.REDUNANT_ARGUMENTS, meta)
}
}

// check unknown options
if (this.config.checkUnknown && unknown.length) {
return meta.$send(format(messages.UNKNOWN_OPTIONS, unknown.join(', ')))
return this._sendHint(CommandHint.UNKNOWN_OPTIONS, meta, unknown.join(', '))
}

// check required options
Expand All @@ -266,12 +266,14 @@ export class Command {
return option.required && !(option.longest in options)
})
if (absent) {
return meta.$send(format(messages.REQUIRED_OPTIONS, absent.rawName))
return this._sendHint(CommandHint.REQUIRED_OPTIONS, meta, absent.rawName)
}
}

// check authority and usage
if (!await this._checkUser(meta, options)) return
const code = this._checkUser(meta, options)
this.context.logger('test').info(code)
if (code) return this._sendHint(code, meta)

// execute command
this.context.logger('command').debug('execute %s', this.name)
Expand All @@ -292,23 +294,30 @@ export class Command {
}
}

private _sendHint (code: CommandHint, meta: Meta<'message'>, ...param: any[]) {
let { showWarning } = this.config
if (typeof showWarning === 'boolean') {
showWarning = -showWarning
}
if (showWarning & code) {
return meta.$send(format(messages[CommandHint[code]], ...param))
}
}

/** check authority and usage */
private async _checkUser (meta: Meta<'message'>, options: Record<string, any>) {
private _checkUser (meta: Meta<'message'>, options: Record<string, any>) {
const user = meta.$user
if (!user) return true
if (!user) return
let isUsage = true

// check authority
if (this.config.authority > user.authority) {
return meta.$send(messages.LOW_AUTHORITY)
return CommandHint.LOW_AUTHORITY
}
for (const option of this._options) {
if (option.camels[0] in options) {
if (option.authority > user.authority) {
if (this.config.showWarning) {
await meta.$send(messages.LOW_AUTHORITY)
}
return
return CommandHint.LOW_AUTHORITY
}
if (option.notUsage) isUsage = false
}
Expand All @@ -320,20 +329,22 @@ export class Command {
const maxUsage = this.getConfig('maxUsage', meta)

if (maxUsage < Infinity || minInterval > 0) {
const message = updateUsage(this.usageName, user, maxUsage, minInterval)
if (message) {
if (this.config.showWarning) {
await meta.$send(message)
}
return
}
return updateUsage(this.usageName, user, { maxUsage, minInterval })
}
}

return true
}

end () {
return this.context
}
}

export enum CommandHint {
USAGE_EXHAUSTED = 1,
TOO_FREQUENT = 2,
LOW_AUTHORITY = 4,
INSUFFICIENT_ARGUMENTS = 8,
REDUNANT_ARGUMENTS = 16,
UNKNOWN_OPTIONS = 32,
REQUIRED_OPTIONS = 64,
}
42 changes: 29 additions & 13 deletions packages/koishi-core/src/utils.ts
@@ -1,7 +1,7 @@
import { isInteger, getDateNumber } from 'koishi-utils'
import { UserField, GroupField, UserData } from './database'
import { NextFunction } from './context'
import { Command } from './command'
import { Command, CommandHint } from './command'
import { Meta } from './meta'
import { messages } from './messages'
import { format } from 'util'
Expand All @@ -18,31 +18,47 @@ export function getTargetId (target: string | number) {
return qq
}

export function getUsage (name: string, user: Pick<UserData, 'usage'>, time = new Date()) {
const ONE_DAY = 86400000

export function getUsage (name: string, user: Pick<UserData, 'usage'>, time = Date.now()) {
const _date = getDateNumber(time)
if (user.usage._date !== _date) {
user.usage = { _date } as any
const oldUsage = user.usage
const newUsage = { _date } as any
for (const key in oldUsage) {
if (key === '_date') continue
const { last } = oldUsage[key]
if (time.valueOf() - last < ONE_DAY) {
newUsage[key] = { last, count: 0 }
}
}
user.usage = newUsage
}

if (!user.usage[name]) {
user.usage[name] = { count: 0 }
}
return user.usage[name]
}

export function updateUsage (name: string, user: Pick<UserData, 'usage'>, maxUsage: number, minInterval?: number) {
const date = new Date()
const usage = getUsage(name, user, date)
interface UsageOptions {
maxUsage?: number
minInterval?: number
timestamp?: number
}

if (minInterval > 0) {
const now = date.valueOf()
if (now - usage.last <= minInterval) {
return messages.TOO_FREQUENT
}
usage.last = now
export function updateUsage (name: string, user: Pick<UserData, 'usage'>, options: UsageOptions = {}) {
const now = Date.now()
const { maxUsage = Infinity, minInterval = 0, timestamp = now } = options
const usage = getUsage(name, user, now)

if (now - usage.last <= minInterval) {
return CommandHint.TOO_FREQUENT
}
usage.last = timestamp

if (usage.count >= maxUsage) {
return messages.USAGE_EXHAUSTED
return CommandHint.USAGE_EXHAUSTED
} else {
usage.count++
}
Expand Down
39 changes: 38 additions & 1 deletion packages/koishi-core/tests/utils.spec.ts
@@ -1,4 +1,5 @@
import { getTargetId } from 'koishi-core'
import { utils } from 'koishi-test-utils'
import { getTargetId, getUsage, updateUsage, createUser } from 'koishi-core'

describe('getTargetId', () => {
test('with id', () => {
Expand All @@ -16,3 +17,39 @@ describe('getTargetId', () => {
expect(getTargetId('foo123')).toBeFalsy()
})
})

const user = createUser(123, 1)
const realDateNow = Date.now
const timestamp = 123456789
const mockedDateNow = Date.now = jest.fn().mockReturnValue(timestamp)

describe('getUsage', () => {
test('empty usage', () => {
utils.getDateNumber.mockReturnValue(10000)
const usage = getUsage('foo', user)
expect(usage.count).toBe(0)
expect(usage.last).toBeUndefined()
})

test('update usage', () => {
expect(updateUsage('foo', user)).toBeFalsy()
const usage = getUsage('foo', user)
expect(usage.count).toBe(1)
expect(usage.last).toBe(timestamp)
})

test('another day', () => {
utils.getDateNumber.mockReturnValue(10001)
const usage = getUsage('foo', user)
expect(usage.count).toBe(0)
expect(usage.last).toBe(timestamp)
})

test('10 days later', () => {
utils.getDateNumber.mockReturnValue(10010)
mockedDateNow.mockReturnValue(864000000 + timestamp)
const usage = getUsage('foo', user)
expect(usage.count).toBe(0)
expect(usage.last).toBeUndefined()
})
})
12 changes: 6 additions & 6 deletions packages/koishi-core/tests/validation.spec.ts
Expand Up @@ -70,28 +70,28 @@ describe('middleware validation', () => {

describe('command validation', () => {
test('check authority', async () => {
app.command('cmd1', { showWarning: true })
app.command('cmd1', { showWarning: -1 })
await session2.shouldHaveReply('cmd1', messages.LOW_AUTHORITY)
await session1.shouldHaveReply('cmd1 --bar', messages.LOW_AUTHORITY)
app.command('cmd1', { showWarning: false })
app.command('cmd1', { showWarning: 0 })
await session1.shouldHaveNoReply('cmd1 --bar')
})

test('check usage', async () => {
app.command('cmd1', { showWarning: true })
app.command('cmd1', { showWarning: -1 })
await session1.shouldHaveReply('cmd1', 'cmd1:123')
await session1.shouldHaveReply('cmd1 --baz', 'cmd1:123')
await session1.shouldHaveReply('cmd1', messages.USAGE_EXHAUSTED)
await session1.shouldHaveReply('cmd1 --baz', 'cmd1:123')
app.command('cmd1', { showWarning: false })
app.command('cmd1', { showWarning: 0 })
await session1.shouldHaveNoReply('cmd1')
})

test('check frequency', async () => {
app.command('cmd2', { showWarning: true })
app.command('cmd2', { showWarning: -1 })
await session2.shouldHaveReply('cmd2', 'cmd2:456')
await session2.shouldHaveReply('cmd2', messages.TOO_FREQUENT)
app.command('cmd2', { showWarning: false })
app.command('cmd2', { showWarning: 0 })
await session2.shouldHaveNoReply('cmd2')
})
})
4 changes: 3 additions & 1 deletion packages/plugin-common/tests/help.spec.ts
@@ -1,4 +1,4 @@
import { Session, MockedApp } from 'koishi-test-utils'
import { Session, MockedApp, utils } from 'koishi-test-utils'
import { noop } from 'koishi-utils'
import { help } from '../src'
import 'koishi-database-memory'
Expand All @@ -21,6 +21,8 @@ function prepare (app: MockedApp) {
.shortcut('baz-shortcut')
}

utils.getDateNumber.mockReturnValue(10000)

describe('help command', () => {
let app: MockedApp, session1: Session, session2: Session

Expand Down

0 comments on commit 797bde2

Please sign in to comment.