Skip to content

Commit

Permalink
feat(common): refactor admin command
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Sep 5, 2020
1 parent 91ceff7 commit f6a7e5c
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 248 deletions.
1 change: 1 addition & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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-eval/tests/*.spec.ts',
'packages/plugin-github/tests/*.spec.ts',
'packages/plugin-teach/tests/*.spec.ts',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"docs": "cd docs && yarn dev",
"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",
Expand Down
1 change: 1 addition & 0 deletions packages/koishi-test-utils/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
350 changes: 184 additions & 166 deletions packages/plugin-common/src/admin.ts
Original file line number Diff line number Diff line change
@@ -1,184 +1,202 @@
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<T extends {}, K extends keyof T> =
(this: Context, session: Session<'authority'>, target: Observed<Pick<T, K>>, ...args: string[]) => Promise<void | string>
type AdminAction<U extends User.Field, G extends Group.Field, O extends {}, T>
= (argv: ParsedArgv<U | 'authority', G, O> & { target: T }, ...args: string[])
=> void | string | Promise<void | string>

export interface ActionItem<T extends {}> {
callback: ActionCallback<T, keyof T>
fields: (keyof T)[]
declare module 'koishi-core/dist/command' {
interface Command<U, G, O> {
adminUser(callback: AdminAction<U, G, O, User.Observed<U | 'authority'>>): this
adminGruop(callback: AdminAction<U, G, O, Group.Observed<G>>): this
}
}

export class Action<T extends {}> {
commands: Record<string, ActionItem<T>> = {}
interface FlagOptions {
set?: boolean
unset?: boolean
}

add<K extends keyof T = never>(name: string, callback: ActionCallback<T, K>, fields?: K[]) {
this.commands[paramCase(name)] = { callback, fields }
}
interface FlagArgv extends ParsedArgv<never, never, FlagOptions> {
target: User.Observed<'flag'> | Group.Observed<'flag'>
}

export const UserAction = new Action<User>()
export const GroupAction = new Action<Group>()

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 '用户权限已修改。'
}
}, ['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]
function flagAction({ target, options }: FlagArgv, ...flags: string[]) {
if (options.set || options.unset) {
const notFound = difference(flags, enumKeys(User.Flag))
if (notFound.length) return `未找到标记 ${notFound.join(', ')}。`
for (const name of flags) {
options.set ? target.flag |= User.Flag[name] : target.flag &= ~User.Flag[name]
}
return
}
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(User.Flag[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<never, never, { user?: string }>, callback) {
const command = this
.userFields(['authority'])
.option('user', '-u [user] 指定目标用户', { authority: 4 })

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 result = await callback({ ...argv, target }, ...args)
if (result) return result
if (!Object.keys(target._diff).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<never, never, { group?: string }>, callback) {
const command = this
.userFields(['authority'])
.option('group', '-g [group] 指定目标群', { authority: 4 })

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 {
target = await session.$observeGroup(fields)
}
} else {
user.timers = {}
const result = await callback({ ...argv, target }, ...args)
if (result) return result
if (!Object.keys(target._diff).length) return '群数据未改动。'
await target._update()
return '群数据已修改。'
}
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]
}
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 <action> [...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<Group>).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<User>).callback.call(ctx, session, user, ...args)
ctx.command('user', '用户管理', { authority: 3 })
ctx.command('group', '群管理', { authority: 3 })

ctx.command('user.auth <value>', '权限信息', { 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('set', '-s 添加标记', { authority: 4 })
.option('unset', '-S 删除标记', { authority: 4 })
.adminUser(flagAction)

ctx.command('user.usage [key]', '调用次数信息')
.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('user.timer [key]', '定时器信息', { authority: 3 })
.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('set', '-s 添加标记', { authority: 4 })
.option('unset', '-S 删除标记', { authority: 4 })
.adminGruop(flagAction)
}
Loading

0 comments on commit f6a7e5c

Please sign in to comment.