diff --git a/packages/core/src/bot.ts b/packages/core/src/bot.ts index 0c44cae59c..4bd5aa37de 100644 --- a/packages/core/src/bot.ts +++ b/packages/core/src/bot.ts @@ -21,18 +21,18 @@ defineProperty(Adapter, 'filter', false) Bot.prototype.getGuildMemberMap = async function getGuildMemberMap(this: Bot, guildId) { const result: Dict = {} for await (const member of this.getGuildMemberIter(guildId)) { - result[member.user.id] = member.nickname || member.user.nick || member.user.name + result[member.user.id] = member.name || member.user.name } return result } Bot.prototype.broadcast = async function broadcast(this: Bot, channels, content, delay = this.ctx.root.config.delay.broadcast) { - const messageIds: string[] = [] + const ids: string[] = [] for (let index = 0; index < channels.length; index++) { if (index && delay) await sleep(delay) try { const value = channels[index] - messageIds.push(...typeof value === 'string' + ids.push(...typeof value === 'string' ? await this.sendMessage(value, content) : Array.isArray(value) ? await this.sendMessage(value[0], content, value[1]) @@ -41,5 +41,5 @@ Bot.prototype.broadcast = async function broadcast(this: Bot, channels, content, this.ctx.logger('bot').warn(error) } } - return messageIds + return ids } diff --git a/packages/core/src/command/index.ts b/packages/core/src/command/index.ts index 5db07a2e41..4eefd6f494 100644 --- a/packages/core/src/command/index.ts +++ b/packages/core/src/command/index.ts @@ -61,8 +61,8 @@ export class Commander extends Map { }) ctx.on('interaction/command', (session) => { - if (session.body?.argv) { - const { name, options, arguments: args } = session.body.argv + if (session.event?.argv) { + const { name, options, arguments: args } = session.event.argv session.execute({ name, args, options }) } else { defineProperty(session, 'argv', ctx.bail('before-parse', session.content, session)) diff --git a/packages/core/src/middleware.ts b/packages/core/src/middleware.ts index 8f1a2c3e0e..6f638cb58c 100644 --- a/packages/core/src/middleware.ts +++ b/packages/core/src/middleware.ts @@ -80,7 +80,7 @@ export class Processor { defineProperty(this, Context.current, ctx) // bind built-in event listeners - this.middleware(this._process.bind(this), true) + this.middleware(this.attach.bind(this), true) ctx.on('message', this._handleMessage.bind(this)) ctx.before('attach-user', (session, fields) => { @@ -203,7 +203,7 @@ export class Processor { } } - private async _process(session: Session, next: Next) { + private async attach(session: Session, next: Next) { this.ctx.emit(session, 'before-attach', session) if (this.ctx.database) { diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 67498a3d55..f61f91cf3b 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -183,6 +183,7 @@ extend(Session.prototype as Session.Private, { get stripped() { const self = this as Session.Private if (self._stripped) return self._stripped + if (!self.elements) return {} as Stripped // strip mentions let atSelf = false, appel = false diff --git a/packages/core/tests/filter.spec.ts b/packages/core/tests/filter.spec.ts index d3f90641b4..1cf3c086fe 100644 --- a/packages/core/tests/filter.spec.ts +++ b/packages/core/tests/filter.spec.ts @@ -8,57 +8,65 @@ app.plugin(Bot, { selfId: '514', }) -const guildSession = app.bots[0].session({ userId: '123', guildId: '456', subtype: 'group' }) -const privateSession = app.bots[0].session({ userId: '123', subtype: 'private' }) +const session1 = app.bots[0].session() +session1.userId = '123' +session1.channelId = '456' +session1.guildId = '456' +session1.isDirect = false + +const session2 = app.bots[0].session() +session2.userId = '123' +session2.channelId = '123' +session2.isDirect = true describe('Selector API', () => { it('root context', () => { - expect(app.filter(guildSession)).to.be.true - expect(app.filter(privateSession)).to.be.true + expect(app.filter(session1)).to.be.true + expect(app.filter(session2)).to.be.true }) it('context.prototype.user', () => { - expect(app.user().filter(guildSession)).to.be.true - expect(app.user().filter(privateSession)).to.be.true - expect(app.user('123').filter(guildSession)).to.be.true - expect(app.user('123').filter(privateSession)).to.be.true - expect(app.user('456').filter(guildSession)).to.be.false - expect(app.user('456').filter(privateSession)).to.be.false + expect(app.user().filter(session1)).to.be.true + expect(app.user().filter(session2)).to.be.true + expect(app.user('123').filter(session1)).to.be.true + expect(app.user('123').filter(session2)).to.be.true + expect(app.user('456').filter(session1)).to.be.false + expect(app.user('456').filter(session2)).to.be.false }) it('context.prototype.private', () => { - expect(app.private().filter(guildSession)).to.be.false - expect(app.private().filter(privateSession)).to.be.true - expect(app.private().user('123').filter(guildSession)).to.be.false - expect(app.private().user('123').filter(privateSession)).to.be.true - expect(app.private().user('456').filter(guildSession)).to.be.false - expect(app.private().user('456').filter(privateSession)).to.be.false + expect(app.private().filter(session1)).to.be.false + expect(app.private().filter(session2)).to.be.true + expect(app.private().user('123').filter(session1)).to.be.false + expect(app.private().user('123').filter(session2)).to.be.true + expect(app.private().user('456').filter(session1)).to.be.false + expect(app.private().user('456').filter(session2)).to.be.false }) it('context.prototype.guild', () => { - expect(app.guild().filter(guildSession)).to.be.true - expect(app.guild().filter(privateSession)).to.be.false - expect(app.guild('123').filter(guildSession)).to.be.false - expect(app.guild('123').filter(privateSession)).to.be.false - expect(app.guild('456').filter(guildSession)).to.be.true - expect(app.guild('456').filter(privateSession)).to.be.false + expect(app.guild().filter(session1)).to.be.true + expect(app.guild().filter(session2)).to.be.false + expect(app.guild('123').filter(session1)).to.be.false + expect(app.guild('123').filter(session2)).to.be.false + expect(app.guild('456').filter(session1)).to.be.true + expect(app.guild('456').filter(session2)).to.be.false }) it('context chaining', () => { - expect(app.guild('456').user('123').filter(guildSession)).to.be.true - expect(app.guild('456').user('456').filter(guildSession)).to.be.false - expect(app.guild('123').user('123').filter(guildSession)).to.be.false - expect(app.user('123').guild('456').filter(guildSession)).to.be.true - expect(app.user('456').guild('456').filter(guildSession)).to.be.false - expect(app.user('123').guild('123').filter(guildSession)).to.be.false + expect(app.guild('456').user('123').filter(session1)).to.be.true + expect(app.guild('456').user('456').filter(session1)).to.be.false + expect(app.guild('123').user('123').filter(session1)).to.be.false + expect(app.user('123').guild('456').filter(session1)).to.be.true + expect(app.user('456').guild('456').filter(session1)).to.be.false + expect(app.user('123').guild('123').filter(session1)).to.be.false }) it('context intersection', () => { - expect(app.guild('456', '789').guild('123', '456').filter(guildSession)).to.be.true - expect(app.guild('456', '789').guild('123', '789').filter(guildSession)).to.be.false - expect(app.guild('123', '789').guild('123', '456').filter(guildSession)).to.be.false - expect(app.user('123', '789').user('123', '456').filter(guildSession)).to.be.true - expect(app.user('456', '789').user('123', '456').filter(guildSession)).to.be.false - expect(app.user('123', '789').user('456', '789').filter(guildSession)).to.be.false + expect(app.guild('456', '789').guild('123', '456').filter(session1)).to.be.true + expect(app.guild('456', '789').guild('123', '789').filter(session1)).to.be.false + expect(app.guild('123', '789').guild('123', '456').filter(session1)).to.be.false + expect(app.user('123', '789').user('123', '456').filter(session1)).to.be.true + expect(app.user('456', '789').user('123', '456').filter(session1)).to.be.false + expect(app.user('123', '789').user('456', '789').filter(session1)).to.be.false }) }) diff --git a/packages/loader/tests/loader.spec.ts b/packages/loader/tests/loader.spec.ts index c9725e58c2..029f0d8a35 100644 --- a/packages/loader/tests/loader.spec.ts +++ b/packages/loader/tests/loader.spec.ts @@ -95,15 +95,15 @@ describe('@koishijs/loader', () => { expect(bar.mock.calls).to.have.length(0) expect(baz.mock.calls).to.have.length(0) - let { body } = app.mock.client('123', '456') - app.emit(app.mock.session(body), 'test/bar' as any) - app.emit(app.mock.session(body), 'test/baz' as any) + let { event } = app.mock.client('123', '456') + app.emit(app.mock.session(event), 'test/bar' as any) + app.emit(app.mock.session(event), 'test/baz' as any) expect(bar.mock.calls).to.have.length(0) expect(baz.mock.calls).to.have.length(1) - body = app.mock.client('321', '456').body - app.emit(app.mock.session(body), 'test/bar' as any) - app.emit(app.mock.session(body), 'test/baz' as any) + event = app.mock.client('321', '456').event + app.emit(app.mock.session(event), 'test/bar' as any) + app.emit(app.mock.session(event), 'test/baz' as any) expect(bar.mock.calls).to.have.length(0) expect(baz.mock.calls).to.have.length(1) }) diff --git a/plugins/mock/src/adapter.ts b/plugins/mock/src/adapter.ts index a46c9f63fc..514db1c17f 100644 --- a/plugins/mock/src/adapter.ts +++ b/plugins/mock/src/adapter.ts @@ -33,11 +33,12 @@ export class MockBot extends Bot { return new MessageClient(this, userId, channelId) } - receive(client: MessageClient, body: Partial) { - const session = this.session(body) - session.send = function (this: Session, fragment, options = {}) { + receive(event: Partial, client?: MessageClient) { + const session = this.session(event) + session.send = async function (this: Session, fragment, options = {}) { options.session = this - return new MockMessenger(client, options).send(fragment) + const messages = await new MockMessenger(client, options).send(fragment) + return messages.map(messages => messages.id) } this.dispatch(session) return session.id @@ -76,11 +77,15 @@ export class MockAdapter extends Adapter { } client(userId: string, channelId?: string) { - return new MessageClient(this.bots[0], userId, channelId) + return this.bots[0].client(userId, channelId) } - session(meta: Partial) { - return this.bots[0].session(meta) + receive(event: Partial, client?: MessageClient) { + return this.bots[0].receive(event, client) + } + + session(event: Partial) { + return this.bots[0].session(event) } } diff --git a/plugins/mock/src/client.ts b/plugins/mock/src/client.ts index 5cf3b68500..d41e84e9bc 100644 --- a/plugins/mock/src/client.ts +++ b/plugins/mock/src/client.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { Context, h, hyphenate, isNullable, Messenger, Universal } from 'koishi' +import { clone, Context, Dict, h, hyphenate, isNullable, Messenger, Universal } from 'koishi' import { format } from 'util' import { MockBot } from './adapter' @@ -13,14 +13,13 @@ export class MockMessenger extends Messenger { private buffer = '' constructor(private client: MessageClient, options?: Universal.SendOptions) { - super(client.bot, client.body.channel.id, client.body.guild?.id, options) + super(client.bot, client.event.channel.id, client.event.guild?.id, options) } async flush() { this.buffer = this.buffer.trim() if (!this.buffer) return - this.client.replies.push(this.buffer) - this.client.resolve(true) + this.client.flush(this.buffer) this.buffer = '' } @@ -60,43 +59,61 @@ export class MockMessenger extends Messenger { } } +interface Hook { + count: number + done?: boolean + resolve?: (replies: string[]) => void +} + export class MessageClient { public app: Context - public body: Partial - public resolve: (checkLength?: boolean) => void = () => {} - public replies: string[] = [] + public event: Universal.Event + + private replies: string[] = [] + private hooks: Dict = {} constructor(public bot: MockBot, public userId: string, public channelId?: string) { this.app = bot.ctx.root - this.body = { + this.event = { platform: 'mock', type: 'message', selfId: bot.selfId, user: { id: userId, name: '' + userId }, - } + } as Universal.Event if (channelId) { - this.body.guild = { id: channelId } - this.body.channel = { id: channelId, type: Universal.Channel.Type.TEXT } + this.event.guild = { id: channelId } + this.event.channel = { id: channelId, type: Universal.Channel.Type.TEXT } } else { - this.body.channel = { id: 'private:' + userId, type: Universal.Channel.Type.DIRECT } + this.event.channel = { id: 'private:' + userId, type: Universal.Channel.Type.DIRECT } + } + + this.app.on('middleware', (session) => { + const hook = this.hooks[session.id] + if (!hook) return + hook.done = true + if (!hook.resolve) delete this.hooks[session.id] + if (Object.values(this.hooks).every(hook => hook.done)) { + this.flush() + this.hooks = {} + } + }) + } + + flush(buffer?: string) { + if (buffer) this.replies.push(buffer) + for (const id in this.hooks) { + const hook = this.hooks[id] + if (!hook.resolve || buffer && this.replies.length < hook.count) continue + hook.resolve(this.replies) + hook.resolve = undefined + hook.count = Infinity + this.replies = [] } } async receive(content: string, count = Infinity) { - return new Promise((resolve) => { - let resolved = false - this.resolve = (checkLength = false) => { - if (resolved) return - if (checkLength && this.replies.length < count) return - resolved = true - dispose() - resolve(this.replies) - this.replies = [] - } - const dispose = this.app.on('middleware', (session) => { - if (session.id === uuid) process.nextTick(this.resolve) - }) + const result = await new Promise((resolve) => { let quote: Universal.Message const elements = h.parse(content) if (elements[0]?.type === 'quote') { @@ -104,8 +121,16 @@ export class MessageClient { quote = { id: attrs.id, messageId: attrs.id, elements: children, content: children.join('') } content = elements.join('') } - const uuid = this.bot.receive(this, { ...this.body, message: { content, elements, quote } }) + const id = this.bot.receive({ + ...clone(this.event), + message: { content, elements, quote }, + }, this) + this.hooks[id] = { resolve, count } }) + // Await for next tick to ensure subsequent operations are executed. + // Do not use `setTimeout` because it may break tests with mocked timers. + await new Promise(process.nextTick) + return result } async shouldReply(message: string, reply?: string | RegExp | (string | RegExp)[]) {