From bcd3ed6a515f33cb6e440cff5bb0d12b719a1e43 Mon Sep 17 00:00:00 2001 From: Shigma <33423008+Shigma@users.noreply.github.com> Date: Fri, 3 Jan 2020 01:12:27 +0800 Subject: [PATCH] feat: new plugin: koishi-plugin-schedule (#9) feat(core): app.prototype.executeCommandLine --- .gitignore | 1 + packages/database-mysql/src/database.ts | 8 +-- packages/database-mysql/src/group.ts | 12 ++-- packages/database-mysql/src/user.ts | 14 ++-- packages/koishi-core/src/app.ts | 11 ++- packages/koishi-core/src/parser.ts | 2 +- packages/koishi-core/src/server.ts | 11 ++- packages/koishi-core/tests/command.spec.ts | 5 ++ packages/koishi-core/tests/parser.spec.ts | 37 +++++++--- packages/koishi-core/tests/receiver.spec.ts | 26 +++---- packages/koishi-core/tests/runtime.spec.ts | 10 +-- packages/plugin-common/src/contextify.ts | 5 +- packages/plugin-common/tests/echo.spec.ts | 41 +++++++++++ packages/plugin-common/tests/help.spec.ts | 2 +- packages/plugin-schedule/README.md | 4 ++ packages/plugin-schedule/package.json | 39 +++++++++++ packages/plugin-schedule/src/database.ts | 75 +++++++++++++++++++++ packages/plugin-schedule/src/index.ts | 56 +++++++++++++++ packages/plugin-schedule/tsconfig.json | 10 +++ packages/test-utils/src/http-server.ts | 10 +-- packages/tsconfig.base.json | 1 - 21 files changed, 319 insertions(+), 61 deletions(-) create mode 100644 packages/plugin-common/tests/echo.spec.ts create mode 100644 packages/plugin-schedule/README.md create mode 100644 packages/plugin-schedule/package.json create mode 100644 packages/plugin-schedule/src/database.ts create mode 100644 packages/plugin-schedule/src/index.ts create mode 100644 packages/plugin-schedule/tsconfig.json diff --git a/.gitignore b/.gitignore index efb2ab7913..1afa542dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ temp /packages/plugin-* !/packages/plugin-common +!/packages/plugin-schedule !/packages/plugin-teach todo.md diff --git a/packages/database-mysql/src/database.ts b/packages/database-mysql/src/database.ts index c07933b7a0..cfce98dc1f 100644 --- a/packages/database-mysql/src/database.ts +++ b/packages/database-mysql/src/database.ts @@ -1,5 +1,5 @@ import { createPool, Pool, PoolConfig, escape, escapeId } from 'mysql' -import { registerDatabase, AbstractDatabase } from 'koishi-core' +import { registerDatabase, AbstractDatabase, TableType, TableData } from 'koishi-core' import { types } from 'util' declare module 'koishi-core/dist/database' { @@ -102,7 +102,7 @@ export class MysqlDatabase implements AbstractDatabase { return this.query(`SELECT ${this.joinKeys(fields)} FROM ?? ${conditional ? ' WHERE ' + conditional : ''}`, [table, ...values]) } - create = async (table: string, data: Partial): Promise => { + async create (table: K, data: Partial): Promise { const keys = Object.keys(data) if (!keys.length) return const header = await this.query( @@ -112,7 +112,7 @@ export class MysqlDatabase implements AbstractDatabase { return { ...data, id: header.insertId } as any } - update = async (table: string, id: number | string, data: object) => { + async update (table: K, id: number | string, data: Partial) { const keys = Object.keys(data) if (!keys.length) return const header = await this.query( @@ -122,7 +122,7 @@ export class MysqlDatabase implements AbstractDatabase { return header as OkPacket } - count = async (table: string) => { + async count (table: K) { const [{ 'COUNT(*)': count }] = await this.query('SELECT COUNT(*) FROM ??', [table]) return count as number } diff --git a/packages/database-mysql/src/group.ts b/packages/database-mysql/src/group.ts index 5ce3a709a9..8a9afa1c25 100644 --- a/packages/database-mysql/src/group.ts +++ b/packages/database-mysql/src/group.ts @@ -21,14 +21,14 @@ injectMethods('mysql', 'group', { const upToDate = timestamp - cache._timestamp < (this.config.groupRefreshInterval ?? defaultRefreshInterval) if (cache && contain(Object.keys(cache), fields) && upToDate) return cache - const [data] = await this.select('groups', fields, '`id` = ?', [groupId]) + const [data] = await this.select('group', fields, '`id` = ?', [groupId]) let fallback: GroupData if (!data) { fallback = createGroup(groupId, selfId) if (selfId && groupId) { await this.query( - 'INSERT INTO `groups` (' + this.joinKeys(groupFields) + ') VALUES (' + groupFields.map(() => '?').join(', ') + ')', - this.formatValues('groups', fallback, groupFields), + 'INSERT INTO `group` (' + this.joinKeys(groupFields) + ') VALUES (' + groupFields.map(() => '?').join(', ') + ')', + this.formatValues('group', fallback, groupFields), ) } } else { @@ -53,11 +53,11 @@ injectMethods('mysql', 'group', { assignees = await getSelfIds() } if (!assignees.length) return [] - return this.select('groups', fields, `\`assignee\` IN (${assignees.join(',')})`) + return this.select('group', fields, `\`assignee\` IN (${assignees.join(',')})`) }, async setGroup (groupId, data) { - const result = await this.update('groups', groupId, data) + const result = await this.update('group', groupId, data) if (!groupCache[groupId]) { groupCache[groupId] = {} as CachedGroupData Object.defineProperty(groupCache[groupId], '_timestamp', { value: Date.now() }) @@ -86,6 +86,6 @@ injectMethods('mysql', 'group', { }, async getGroupCount () { - return this.count('groups') + return this.count('group') }, }) diff --git a/packages/database-mysql/src/user.ts b/packages/database-mysql/src/user.ts index f1d5ce7e74..9629aa26a2 100644 --- a/packages/database-mysql/src/user.ts +++ b/packages/database-mysql/src/user.ts @@ -2,13 +2,13 @@ import { injectMethods, userFields, UserData, createUser, User, UserField } from import { observe, difference } from 'koishi-utils' import { arrayTypes } from './database' -arrayTypes.push('users.endings', 'users.achievement', 'users.inference') +arrayTypes.push('user.endings', 'user.achievement', 'user.inference') injectMethods('mysql', 'user', { async getUser (userId, ...args) { const authority = typeof args[0] === 'number' ? args.shift() as number : 0 const fields = args[0] as never || userFields - const [data] = await this.select('users', fields, '`id` = ?', [userId]) + const [data] = await this.select('user', fields, '`id` = ?', [userId]) let fallback: UserData if (data) { data.id = userId @@ -18,8 +18,8 @@ injectMethods('mysql', 'user', { fallback = createUser(userId, authority) if (authority) { await this.query( - 'INSERT INTO `users` (' + this.joinKeys(userFields) + ') VALUES (' + userFields.map(() => '?').join(', ') + ')', - this.formatValues('users', fallback, userFields), + 'INSERT INTO `user` (' + this.joinKeys(userFields) + ') VALUES (' + userFields.map(() => '?').join(', ') + ')', + this.formatValues('user', fallback, userFields), ) } } @@ -38,11 +38,11 @@ injectMethods('mysql', 'user', { fields = args[0] as any } if (ids && !ids.length) return [] - return this.select('users', fields, ids && `\`id\` IN (${ids.join(', ')})`) + return this.select('user', fields, ids && `\`id\` IN (${ids.join(', ')})`) }, async setUser (userId, data) { - return this.update('users', userId, data) + return this.update('user', userId, data) }, async observeUser (user, ...args) { @@ -65,6 +65,6 @@ injectMethods('mysql', 'user', { }, async getUserCount () { - return this.count('users') + return this.count('user') }, }) diff --git a/packages/koishi-core/src/app.ts b/packages/koishi-core/src/app.ts index 7f785fd26e..0dd63cfe1f 100644 --- a/packages/koishi-core/src/app.ts +++ b/packages/koishi-core/src/app.ts @@ -7,8 +7,8 @@ import { Context, Middleware, NextFunction, ContextScope, Events, EventMap } fro import { GroupFlag, UserFlag, UserField, createDatabase, DatabaseConfig, GroupField } from './database' import { showSuggestions } from './utils' import { Meta, MessageMeta } from './meta' -import { simplify } from 'koishi-utils' -import { errors } from './messages' +import { simplify, noop } from 'koishi-utils' +import { errors, messages } from './messages' export interface AppOptions { port?: number @@ -367,6 +367,13 @@ export class App extends Context { } } + executeCommandLine (message: string, meta: MessageMeta, next: NextFunction = noop) { + if (!meta.$path) this.server.parseMeta(meta) + const argv = this.parseCommandLine(message, meta) + if (argv) return argv.command.execute(argv, next) + return next() + } + private _applyMiddlewares = async (meta: MessageMeta) => { // preparation const counter = this._middlewareCounter++ diff --git a/packages/koishi-core/src/parser.ts b/packages/koishi-core/src/parser.ts index a002ae632b..0d3b207c6e 100644 --- a/packages/koishi-core/src/parser.ts +++ b/packages/koishi-core/src/parser.ts @@ -63,7 +63,7 @@ export interface CommandOption extends OptionConfig { description: string } -export function parseOption (rawName: string, description: string, config: OptionConfig = {}, optsDef: Record): CommandOption { +export function parseOption (rawName: string, description: string, config: OptionConfig, optsDef: Record): CommandOption { config = { authority: 0, ...config } const negated: string[] = [] diff --git a/packages/koishi-core/src/server.ts b/packages/koishi-core/src/server.ts index 093d4e50ff..e735862abc 100644 --- a/packages/koishi-core/src/server.ts +++ b/packages/koishi-core/src/server.ts @@ -40,7 +40,7 @@ export abstract class Server { return meta } - async dispatchMeta (meta: Meta) { + parseMeta (meta: Meta) { // prepare prefix let ctxType: ContextType, ctxId: number if (meta.groupId) { @@ -94,7 +94,7 @@ export abstract class Server { Object.defineProperty(meta, '$ctxType', { value: ctxType }) const app = this.appMap[meta.selfId] - if (!app) return + if (!app) return events // add context properties if (meta.postType === 'message') { @@ -137,7 +137,12 @@ export abstract class Server { } } - // emit events + return events + } + + dispatchMeta (meta: Meta) { + const app = this.appMap[meta.selfId] + const events = this.parseMeta(meta) for (const event of events) { app.emitEvent(meta, paramCase(event) as any, meta) } diff --git a/packages/koishi-core/tests/command.spec.ts b/packages/koishi-core/tests/command.spec.ts index 3b4c079174..7ae5eb7cd1 100644 --- a/packages/koishi-core/tests/command.spec.ts +++ b/packages/koishi-core/tests/command.spec.ts @@ -56,6 +56,11 @@ describe('register commands', () => { app.command('g').alias('y') app.command('h').alias('y') }).toThrow(errors.DUPLICATE_COMMAND) + + expect(() => { + app.command('i').alias('z') + app.command('i').alias('z') + }).not.toThrow() }) test('remove options', () => { diff --git a/packages/koishi-core/tests/parser.spec.ts b/packages/koishi-core/tests/parser.spec.ts index 8a06559806..37f940784c 100644 --- a/packages/koishi-core/tests/parser.spec.ts +++ b/packages/koishi-core/tests/parser.spec.ts @@ -1,5 +1,5 @@ -import { App, Command } from '../src' -import { errors } from '../src/messages' +import { App, Command, errors } from '../src' +import { ParsedLine } from '../src/parser' const app = new App() @@ -12,8 +12,8 @@ const cmd2 = app .command('cmd2 [foo] [bar...]') .option('-a [alpha]', '', { isString: true }) .option('-b [beta]', '', { default: 1000 }) - .option('-C, --no-gamma') - .option('-D, --no-delta') + .option('--no-gamma, -C') + .option('--no-delta, -D') describe('arguments', () => { test('sufficient arguments', () => { @@ -45,6 +45,8 @@ describe('arguments', () => { }) describe('options', () => { + let result: ParsedLine + test('duplicate options', () => { expect(() => app .command('cmd-duplicate-options') @@ -54,35 +56,39 @@ describe('options', () => { }) test('option without parameter', () => { - const result = cmd1.parse('--alpha a') + result = cmd1.parse('--alpha a') expect(result.args).toMatchObject(['a']) expect(result.options).toMatchObject({ a: true, alpha: true }) }) test('option with parameter', () => { - const result = cmd1.parse('--beta 10') + result = cmd1.parse('--beta 10') + expect(result.options).toMatchObject({ b: 10, beta: 10 }) + result = cmd1.parse('--beta=10') expect(result.options).toMatchObject({ b: 10, beta: 10 }) }) test('quoted parameter', () => { - const result = cmd1.parse('-c "" -d') + result = cmd1.parse('-c "" -d') expect(result.options).toMatchObject({ c: '', d: true }) }) test('unknown options', () => { - const result = cmd1.parse('--unknown-gamma c -de 10') + result = cmd1.parse('--unknown-gamma b --unknown-gamma c -de 10') expect(result.unknown).toMatchObject(['unknown-gamma', 'd', 'e']) expect(result.options).toMatchObject({ unknownGamma: 'c', d: true, e: 10 }) }) test('negated options', () => { - const result = cmd2.parse('-C --no-delta -E --no-epsilon') + result = cmd2.parse('-C --no-delta -E --no-epsilon') expect(result.options).toMatchObject({ C: true, gamma: false, D: true, delta: false, E: true, epsilon: false }) }) test('option configuration', () => { - const result = cmd2.parse('-a 123 -d 456') - expect(result.options).toMatchObject({ a: '123', b: 1000, d: 456 }) + result = cmd2.parse('-ba 123') + expect(result.options).toMatchObject({ a: '123', b: 1000 }) + result = cmd2.parse('-ad 456') + expect(result.options).toMatchObject({ a: '', b: 1000, d: 456 }) }) }) @@ -95,6 +101,15 @@ describe('user fields', () => { expect(cmd._userFields).toHaveProperty('size', 3) }) +describe('group fields', () => { + const cmd = app.command('cmd-group-fields') + expect(cmd._groupFields).toHaveProperty('size', 0) + cmd.groupFields(['id', 'assignee']) + expect(cmd._groupFields).toHaveProperty('size', 2) + cmd.groupFields(new Set(['id', 'flag'])) + expect(cmd._groupFields).toHaveProperty('size', 3) +}) + describe('edge cases', () => { let cmd3: Command diff --git a/packages/koishi-core/tests/receiver.spec.ts b/packages/koishi-core/tests/receiver.spec.ts index b735210f81..8996f7680d 100644 --- a/packages/koishi-core/tests/receiver.spec.ts +++ b/packages/koishi-core/tests/receiver.spec.ts @@ -37,79 +37,79 @@ afterAll(() => { describe('meta.$path', () => { test('user/*/message/friend', async () => { const meta = createMeta('message', 'private', 'friend', { userId: 10000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/user/10000/message/friend') }) test('user/*/friend_add', async () => { const meta = createMeta('notice', 'friend_add', null, { userId: 10000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/user/10000/friend_add') }) test('user/*/request/friend', async () => { const meta = createMeta('request', 'friend', null, { userId: 10000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/user/10000/request/friend') }) test('group/*/message/normal', async () => { const meta = createMeta('message', 'group', 'normal', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/message/normal') }) test('group/*/group_upload', async () => { const meta = createMeta('notice', 'group_upload', null, { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/group_upload') }) test('group/*/group_admin/unset', async () => { const meta = createMeta('notice', 'group_admin', 'unset', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/group_admin/unset') }) test('group/*/group_decrease/kick', async () => { const meta = createMeta('notice', 'group_decrease', 'kick', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/group_decrease/kick') }) test('group/*/group_increase/invite', async () => { const meta = createMeta('notice', 'group_increase', 'invite', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/group_increase/invite') }) test('group/*/group_ban/ban', async () => { const meta = createMeta('notice', 'group_ban', 'ban', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/group_ban/ban') }) test('group/*/request/group/invite', async () => { const meta = createMeta('request', 'group', 'invite', { groupId: 20000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/group/20000/request/group/invite') }) test('discuss/*/message', async () => { const meta = createMeta('message', 'discuss', null, { discussId: 30000 }) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/discuss/30000/message') }) test('lifecycle/enable', async () => { const meta = createMeta('meta_event', 'lifecycle', 'enable') - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/lifecycle/enable') }) test('heartbeat', async () => { const meta = createMeta('meta_event', 'heartbeat', null) - await app.server.dispatchMeta(meta) + app.server.parseMeta(meta) expect(meta.$path).toBe('/heartbeat') }) }) diff --git a/packages/koishi-core/tests/runtime.spec.ts b/packages/koishi-core/tests/runtime.spec.ts index f146ec7d3e..f7800a8dd2 100644 --- a/packages/koishi-core/tests/runtime.spec.ts +++ b/packages/koishi-core/tests/runtime.spec.ts @@ -174,7 +174,7 @@ describe('command execution', () => { }) test('excute command', () => { - app.runCommand('foo', meta, ['bar']) + app.executeCommandLine('foo bar', meta) expect(mock).toBeCalledTimes(1) expect(mock).toBeCalledWith('foobar') }) @@ -184,7 +184,7 @@ describe('command execution', () => { const mock2 = jest.fn() app.receiver.on('error', mock1) app.receiver.on('error/command', mock2) - app.runCommand('err', meta) + app.executeCommandLine('err', meta) expect(mock1).toBeCalledTimes(1) expect(mock1.mock.calls[0][0]).toHaveProperty('message', 'command error') expect(mock2).toBeCalledTimes(1) @@ -195,16 +195,18 @@ describe('command execution', () => { app.runCommand('bar', meta, ['foo']) expect(mock).toBeCalledTimes(1) expect(mock).toBeCalledWith(messages.COMMAND_NOT_FOUND) + app.executeCommandLine('bar', meta, mock) + expect(mock).toBeCalledTimes(2) }) test('insufficient arguments', () => { - app.runCommand('foo', meta) + app.executeCommandLine('foo', meta) expect(mock).toBeCalledTimes(1) expect(mock).toBeCalledWith(messages.INSUFFICIENT_ARGUMENTS) }) test('redunant arguments', () => { - app.runCommand('foo', meta, ['bar', 'baz']) + app.executeCommandLine('foo bar baz', meta) expect(mock).toBeCalledTimes(1) expect(mock).toBeCalledWith(messages.REDUNANT_ARGUMENTS) }) diff --git a/packages/plugin-common/src/contextify.ts b/packages/plugin-common/src/contextify.ts index 995e563573..a4a793ed73 100644 --- a/packages/plugin-common/src/contextify.ts +++ b/packages/plugin-common/src/contextify.ts @@ -1,5 +1,4 @@ import { Context, CommandConfig } from 'koishi-core' -import { CQCode } from 'koishi-utils' export default function apply (ctx: Context, config: CommandConfig = {}) { ctx.command('contextify ', '在特定上下文中触发指令', { authority: 3, ...config }) @@ -47,8 +46,6 @@ export default function apply (ctx: Context, config: CommandConfig = {}) { newMeta.subType = options.type || 'other' } - newMeta.message = message - newMeta.rawMessage = CQCode.unescape(message) - await ctx.app.server.dispatchMeta(newMeta) + return ctx.app.executeCommandLine(message, newMeta) }) } diff --git a/packages/plugin-common/tests/echo.spec.ts b/packages/plugin-common/tests/echo.spec.ts new file mode 100644 index 0000000000..3f6d816800 --- /dev/null +++ b/packages/plugin-common/tests/echo.spec.ts @@ -0,0 +1,41 @@ +import { httpServer } from 'koishi-test-utils' +import echo from '../src/echo' + +const { createApp, createServer, postMeta, createMeta, waitFor } = httpServer + +const server = createServer() +const app = createApp() + +app.plugin(echo) + +jest.setTimeout(1000) + +beforeAll(() => { + return app.start() +}) + +afterAll(() => { + server.close() + return app.stop() +}) + +describe('echo command', () => { + test('basic support', async () => { + await postMeta(createMeta('message', 'private', 'friend', { message: 'echo foo', userId: 123 })) + await expect(waitFor('send_private_msg')).resolves.toMatchObject({ message: 'foo', user_id: '123' }) + await postMeta(createMeta('message', 'group', 'normal', { message: 'echo foo', groupId: 123 })) + await expect(waitFor('send_group_msg')).resolves.toMatchObject({ message: 'foo', group_id: '123' }) + await postMeta(createMeta('message', 'discuss', null, { message: 'echo foo', discussId: 123 })) + await expect(waitFor('send_discuss_msg')).resolves.toMatchObject({ message: 'foo', discuss_id: '123' }) + }) + + test('send to multiple contexts', async () => { + await postMeta(createMeta('message', 'private', 'friend', { message: 'echo -u 456 foo', userId: 123 })) + await expect(waitFor('send_private_msg')).resolves.toMatchObject({ message: 'foo', user_id: '456' }) + await postMeta(createMeta('message', 'private', 'friend', { message: 'echo -g 456 -d 789 foo', userId: 123 })) + await Promise.all([ + expect(waitFor('send_group_msg')).resolves.toMatchObject({ message: 'foo', group_id: '456' }), + expect(waitFor('send_discuss_msg')).resolves.toMatchObject({ message: 'foo', discuss_id: '789' }), + ]) + }) +}) diff --git a/packages/plugin-common/tests/help.spec.ts b/packages/plugin-common/tests/help.spec.ts index d5130aa929..a4f9a89ab0 100644 --- a/packages/plugin-common/tests/help.spec.ts +++ b/packages/plugin-common/tests/help.spec.ts @@ -6,7 +6,7 @@ import del from 'del' import help from '../src/help' import 'koishi-database-level' -const { createApp, createServer, ServerSession } = httpServer +const { createApp, createServer, ServerSession } = httpServer const path = resolve(__dirname, '../temp') diff --git a/packages/plugin-schedule/README.md b/packages/plugin-schedule/README.md new file mode 100644 index 0000000000..7de7bcb6e9 --- /dev/null +++ b/packages/plugin-schedule/README.md @@ -0,0 +1,4 @@ +# [koishi-plugin-schedule](https://koishijs.github.io/plugins/schedule.html) + +[![npm](https://img.shields.io/npm/v/koishi-plugin-schedule?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-schedule) + diff --git a/packages/plugin-schedule/package.json b/packages/plugin-schedule/package.json new file mode 100644 index 0000000000..a101ef1d9f --- /dev/null +++ b/packages/plugin-schedule/package.json @@ -0,0 +1,39 @@ +{ + "name": "koishi-plugin-schedule", + "description": "Schedule plugin for Koishi", + "version": "1.0.0-alpha.0", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "author": "Shigma <1700011071@pku.edu.cn>", + "license": "MIT", + "scripts": { + "build": "tsc -b", + "lint": "eslint src --ext .ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/koishijs/koishi.git" + }, + "bugs": { + "url": "https://github.com/koishijs/koishi/issues" + }, + "homepage": "https://github.com/koishijs/koishi/packages/plugin-schedule#readme", + "keywords": [ + "bot", + "qqbot", + "cqhttp", + "coolq", + "chatbot", + "koishi", + "plugin", + "schedule", + "task" + ], + "devDependencies": { + "koishi-database-level": "^1.0.1", + "koishi-database-mysql": "^1.0.1" + }, + "dependencies": { + "koishi-core": "^1.0.1" + } +} diff --git a/packages/plugin-schedule/src/database.ts b/packages/plugin-schedule/src/database.ts new file mode 100644 index 0000000000..723488b83f --- /dev/null +++ b/packages/plugin-schedule/src/database.ts @@ -0,0 +1,75 @@ +import { MessageMeta, getSelfIds, injectMethods } from 'koishi-core' +import {} from 'koishi-database-mysql' +import {} from 'koishi-database-level' + +declare module 'koishi-core/dist/database' { + interface TableMethods { + schedule?: ScheduleMethods + } + + interface TableData { + schedule?: Schedule + } +} + +interface ScheduleMethods { + createSchedule (time: number, assignee: number, interval: number, command: string, meta: MessageMeta): Promise + removeSchedule (id: number): Promise + getSchedule (id: number): Promise + getAllSchedules (assignees?: number[]): Promise +} + +export interface Schedule { + id: number + assignee: number + time: number + interval: number + command: string + meta: MessageMeta +} + +injectMethods('mysql', 'schedule', { + createSchedule (time, assignee, interval, command, meta) { + return this.create('schedule', { time, assignee, interval, command, meta }) + }, + + removeSchedule (id) { + return this.query('DELETE FROM `schedule` WHERE `id` = ?', [id]) + }, + + async getSchedule (id) { + const data = await this.query('SELECT * FROM `schedule` WHERE `id` = ?', [id]) + return data[0] + }, + + async getAllSchedules (assignees) { + let queryString = 'SELECT * FROM `schedule`' + if (!assignees) assignees = await getSelfIds() + queryString += ` WHERE \`assignee\` IN (${assignees.join(',')})` + return this.query(queryString) + }, +}) + +injectMethods('level', 'schedule', { + createSchedule (time, assignee, interval, command, meta) { + return this.create('schedule', { time, assignee, interval, command, meta }) + }, + + removeSchedule (id) { + return this.remove('schedule', id) + }, + + getSchedule (id) { + return this.tables.schedule.get(id) + }, + + async getAllSchedules (assignees) { + if (!assignees) assignees = await getSelfIds() + return new Promise((resolve) => { + const data: Schedule[] = [] + this.tables.schedule.createValueStream() + .on('data', item => assignees.includes(item.assignee) ? data.push(item) : null) + .on('end', () => resolve(data)) + }) + }, +}) diff --git a/packages/plugin-schedule/src/index.ts b/packages/plugin-schedule/src/index.ts new file mode 100644 index 0000000000..31d494c88a --- /dev/null +++ b/packages/plugin-schedule/src/index.ts @@ -0,0 +1,56 @@ +import { Context, onStart, appMap } from 'koishi-core' +import { Schedule } from './database' + +function inspectSchedule ({ id, assignee, meta, interval, command, time }: Schedule, now = Date.now()) { + if (!appMap[assignee]) return + const app = appMap[assignee] + + if (!interval) { + if (time < now) { + return app.database.removeSchedule(id) + } else { + setTimeout(async () => { + if (!await app.database.getSchedule(id)) return + app.executeCommandLine(command, meta) + app.database.removeSchedule(id) + }, time - now) + } + } else { + const timeout = time < now ? interval - (now - time) % interval : time - now + setTimeout(async () => { + if (!await app.database.getSchedule(id)) return + const timer = setInterval(async () => { + if (!await app.database.getSchedule(id)) { + return clearInterval(timer) + } + app.executeCommandLine(command, meta) + }, interval) + app.executeCommandLine(command, meta) + }, timeout) + } +} + +export function apply (ctx: Context) { + onStart(async ({ database }) => { + const now = Date.now() + const schedules = await database.getAllSchedules() + schedules.forEach(schedule => inspectSchedule(schedule, now)) + }) + + ctx.command('advanced') + .subcommand('schedule