Skip to content

Commit

Permalink
feat(middleware): api desgin draft
Browse files Browse the repository at this point in the history
  • Loading branch information
Gcaufy committed Nov 6, 2021
1 parent bf7d4e1 commit 1d7a9de
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 10 deletions.
3 changes: 2 additions & 1 deletion src/interface/wechaty-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import type { WechatyEventListeners } from '../events/wechaty-events.js'
import type TypedEventEmitter from 'typed-emitter'
import type { WechatyMixinProtectedProperty } from '../wechaty-mixins/mod.js'
import type { TypedMiddleWareEventEmitter, WechatyMiddleWares } from '../middlewares/wechaty-middlewares.js'

type AllProtectedProperty =
| keyof EventEmitter // Huan(202110): remove all EventEmitter first, and added typed event emitter later: or will get error
Expand All @@ -21,7 +22,7 @@ type AllProtectedProperty =
// & TypedEventEmitter<WechatyEventListeners>

type WechatyInterface = Omit<WechatyImpl, AllProtectedProperty>
& TypedEventEmitter<WechatyEventListeners>
& TypedEventEmitter<WechatyEventListeners> & TypedMiddleWareEventEmitter<WechatyEventListeners, WechatyMiddleWares>

type WechatyConstructor = Constructor<
WechatyInterface,
Expand Down
79 changes: 79 additions & 0 deletions src/middlewares/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Examples

1. How to define a middleware
```
// 抽象通过能力为中间件,由中间件控制事件响应
const filterMiddleWare = (options) {
return async (message, next) => {
const room = message.room()
if (!room) return
if (message.type() !== option.type) return
if (!matchers.roomMatcher(option.room)) return
await next();
}
}
```

2. Add middleware to specified event
```
// Only room a print `banana`
bot.on('message',
filterMiddlerWare({ type: type.Message.Text, room: 'room a'}),
(message) => console.log('banana')
);
// All rooms print `apple`
bot.on('message',
(message) => console.log('apple')
);
```

3. Add global middleware
```
WechatyImpl.middleware({
message: filterMiddleWare({ type.Message.Text, room: [ 'room-d', 'room-e' ]})
})
// Only room-d, room-e print apple.
bot.on('message',
(message) => console.log('apple')
);
// only room-d, room-e print orange(50% probability)
bot.on('message', async (message, next) => {
if (Math.random() < 0.5) {
await next();
}
}, (message) => console.log('orange'))
```

4. Work with plugins
```
WechatyImpl.middleware({
message: filterMiddleWare({ type.Message.Text, room: [ 'room-d', 'room-e' ]})
})
// Only room-d, room-e has kickoff feature
WechatyImpl.use(KickOffPlugin(options));
// TODO: use support rest params, so we need to think about how design it.
// WechatyImpl.use({ message: someMiddleWare() }, KickOffPlugin(options));
```

5. Work in plugins
```
const onMessage = (message) => {
// plugin logic
}
export MyPlugin = (bot: WechatyInterface) => {
bot.on('message', [
MiddleWareA(),
MiddleWareB(),
MiddleWareC(),
], onMessage)
}
```
8 changes: 8 additions & 0 deletions src/middlewares/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const noop = (x: any) => x
// TODO: finish the middleware compose logic
export const compose = function (middlewares: any[]) {
noop(middlewares)
return (...args: any) => {
noop(args)
}
}
86 changes: 86 additions & 0 deletions src/middlewares/wechaty-middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Wechaty MiddleWare Interfaces
*/

import type {
WechatyEventListenerDong,
WechatyEventListenerError,
WechatyEventListenerFriendship,
WechatyEventListenerHeartbeat,
WechatyEventListenerLogin,
WechatyEventListenerLogout,
WechatyEventListenerMessage,
WechatyEventListenerPuppet,
WechatyEventListenerReady,
WechatyEventListenerRoomInvite,
WechatyEventListenerRoomJoin,
WechatyEventListenerRoomLeave,
WechatyEventListenerRoomTopic,
WechatyEventListenerScan,
WechatyEventListenerStartStop,
} from '../events/wechaty-events.js'

type AddFunctionParameters<
TFunction extends (...args: any) => any,
TParameters extends [...args: any]
> = (
...args: [...Parameters<TFunction>, ...TParameters]
) => ReturnType<TFunction>;

type WechatyMiddleWareDong = AddFunctionParameters<WechatyEventListenerDong, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareError = AddFunctionParameters<WechatyEventListenerError, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareFriendship = AddFunctionParameters<WechatyEventListenerFriendship, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareHeartbeat = AddFunctionParameters<WechatyEventListenerHeartbeat, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareLogin = AddFunctionParameters<WechatyEventListenerLogin, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareLogout = AddFunctionParameters<WechatyEventListenerLogout, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareMessage = AddFunctionParameters<WechatyEventListenerMessage, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWarePuppet = AddFunctionParameters<WechatyEventListenerPuppet, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareReady = AddFunctionParameters<WechatyEventListenerReady, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareRoomInvite = AddFunctionParameters<WechatyEventListenerRoomInvite, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareRoomJoin = AddFunctionParameters<WechatyEventListenerRoomJoin, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareRoomLeave = AddFunctionParameters<WechatyEventListenerRoomLeave, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareRoomTopic = AddFunctionParameters<WechatyEventListenerRoomTopic, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareScan = AddFunctionParameters<WechatyEventListenerScan, [ next: () => void | Promise<void> ]>;
type WechatyMiddleWareStartStop = AddFunctionParameters<WechatyEventListenerStartStop, [ next: () => void | Promise<void> ]>;

interface WechatyMiddleWares {
'room-invite' : WechatyMiddleWareRoomInvite
'room-join' : WechatyMiddleWareRoomJoin
'room-leave' : WechatyMiddleWareRoomLeave
'room-topic' : WechatyMiddleWareRoomTopic
dong : WechatyMiddleWareDong
error : WechatyMiddleWareError
friendship : WechatyMiddleWareFriendship
heartbeat : WechatyMiddleWareHeartbeat
login : WechatyMiddleWareLogin
logout : WechatyMiddleWareLogout
message : WechatyMiddleWareMessage
puppet : WechatyMiddleWarePuppet
ready : WechatyMiddleWareReady
scan : WechatyMiddleWareScan
start : WechatyMiddleWareStartStop
stop : WechatyMiddleWareStartStop
}

export interface TypedMiddleWareEventEmitter<Events, MiddleWares> {
on<E extends (keyof Events & keyof MiddleWares)> (event: E, middleware: MiddleWares[E] | MiddleWares[E][], listener: Events[E]): this
}

export type {
WechatyMiddleWares,
WechatyMiddleWareDong,
WechatyMiddleWareError,
WechatyMiddleWareFriendship,
WechatyMiddleWareHeartbeat,
WechatyMiddleWareLogin,
WechatyMiddleWareLogout,
WechatyMiddleWareMessage,
WechatyMiddleWarePuppet,
WechatyMiddleWareReady,
WechatyMiddleWareRoomInvite,
WechatyMiddleWareRoomJoin,
WechatyMiddleWareRoomLeave,
WechatyMiddleWareRoomTopic,
WechatyMiddleWareScan,
WechatyMiddleWareStartStop,
}
102 changes: 102 additions & 0 deletions src/wechaty-mixins/middleware-mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { log } from 'wechaty-puppet'
import type { WechatyConstructor } from '../interface/wechaty-interface.js'
import type { WechatySkelton } from './wechaty-skelton.js'

import type {
WechatyMiddleWareDong,
WechatyMiddleWareError,
WechatyMiddleWareFriendship,
WechatyMiddleWareHeartbeat,
WechatyMiddleWareLogin,
WechatyMiddleWareLogout,
WechatyMiddleWareMessage,
WechatyMiddleWarePuppet,
WechatyMiddleWareReady,
WechatyMiddleWareRoomInvite,
WechatyMiddleWareRoomJoin,
WechatyMiddleWareRoomLeave,
WechatyMiddleWareRoomTopic,
WechatyMiddleWareScan,
WechatyMiddleWareStartStop,
} from '../middlewares/wechaty-middlewares.js'

type TOrArrayT<T> = T | T[]

interface WechatyGlobalMiddleWares {
'room-invite'? : TOrArrayT<WechatyMiddleWareRoomInvite>
'room-join'? : TOrArrayT<WechatyMiddleWareRoomJoin>
'room-leave'? : TOrArrayT<WechatyMiddleWareRoomLeave>
'room-topic'? : TOrArrayT<WechatyMiddleWareRoomTopic>
dong? : TOrArrayT<WechatyMiddleWareDong>
error? : TOrArrayT<WechatyMiddleWareError>
friendship? : TOrArrayT<WechatyMiddleWareFriendship>
heartbeat? : TOrArrayT<WechatyMiddleWareHeartbeat>
login? : TOrArrayT<WechatyMiddleWareLogin>
logout? : TOrArrayT<WechatyMiddleWareLogout>
message? : TOrArrayT<WechatyMiddleWareMessage>
puppet? : TOrArrayT<WechatyMiddleWarePuppet>
ready? : TOrArrayT<WechatyMiddleWareReady>
scan? : TOrArrayT<WechatyMiddleWareScan>
start? : TOrArrayT<WechatyMiddleWareStartStop>
stop? : TOrArrayT<WechatyMiddleWareStartStop>
}

const middleWareMixin = <MixinBase extends typeof WechatySkelton> (mixinBase: MixinBase) => {
log.verbose('WechatyMiddleWareMixin', 'middlewareMixin(%s)', mixinBase.name)

abstract class MiddleWareMixin extends mixinBase {

static _globalMiddleWares: WechatyGlobalMiddleWares = {}

/**
* @param {WechatyGlobalMiddleWare[]} middlewares - The global middlewares you want to use
*
* @return {WechatyInterface} - this for chaining,
*
* @desc
* For wechaty ecosystem, allow user to define a 3rd party middleware for the all wechaty instances
*
* @example
*
* // Random catch all chat message.
*
* function RandomMessageMiddleWare(options: { rate: number}) {
* return function (this: Wechaty, message: Message, next: async () => void | Promise<void>) {
* if (Math.random() > options.rate) {
* await next();
* }
* }
* }
*
* wechaty.middleware({
* message: RandomMessageMiddleWare({ rate: 0.5 }),
* })
*
* bot.on('message', async (message: Message) => {
* await message.say('Bingo');
* })
*/

// TODO: I prefer `use` for middlewares, and `install` for plugins. But it's an incompatible API change. so that we can use middleware instead.
static middleware (
middleware: WechatyGlobalMiddleWares,
): WechatyConstructor {
this._globalMiddleWares = middleware
// Huan(202110): TODO: remove any
return this as any
}

}

return MiddleWareMixin
}

type MiddleWareMixin = ReturnType<typeof middleWareMixin>

export type {
WechatyGlobalMiddleWares,
MiddleWareMixin,
}
export {
middleWareMixin,
}
17 changes: 16 additions & 1 deletion src/wechaty-mixins/plugin-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,22 @@ const pluginMixin = <MixinBase extends typeof WechatySkelton> (mixinBase: MixinB
): WechatyInterface {
const pluginList = plugins.flat() as WechatyPlugin[]
const uninstallerList = pluginList
.map(plugin => plugin(this as any)) // <- Huan(202110): TODO: remove any
.map(plugin => {
const onFunc = this.on as any

// Temporarily overwite the on event before install plugin
this.on = (event: any, listener: any) =>
// All plugins should run after middlewares.
onFunc.bind(this)(
event,
(WechatyImpl._globalMiddleWares as any)[event] || [],
listener,
)
const rst = plugin(this as any)
// Restore
this.on = onFunc
return rst
}) // <- Huan(202110): TODO: remove any
.filter(isWechatyPluginUninstaller)

this._pluginUninstallerList.push(
Expand Down
35 changes: 27 additions & 8 deletions src/wechaty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import type {
ContactSelfInterface,
ContactSelfImpl,
} from './user-modules/mod.js'
import { compose } from './middlewares/compose.js'
import { middleWareMixin } from './wechaty-mixins/middleware-mixin.js'

export interface WechatyOptions {
memory? : MemoryCard,
Expand All @@ -72,11 +74,13 @@ export interface WechatyOptions {
}

const mixinBase = serviceCtlMixin('Wechaty', { log })(
pluginMixin(
puppetMixin(
wechatifyUserModuleMixin(
gErrorMixin(
WechatySkelton,
middleWareMixin(
pluginMixin(
puppetMixin(
wechatifyUserModuleMixin(
gErrorMixin(
WechatySkelton,
),
),
),
),
Expand Down Expand Up @@ -215,13 +219,28 @@ class WechatyImpl extends mixinBase implements SayableSayer {
return this._options.name || 'wechaty'
}

override on (event: WechatyEventName, listener: (...args: any[]) => any): this {
override on (event: WechatyEventName, listener: (...args: any[]) => any): this
override on (event: WechatyEventName, middleware: any[], listener: (...args: any[]) => any): this
override on (event: WechatyEventName, middlewareOrListener: any[] | ((...args: any[]) => any), listener?: (...args: any[]) => any): this {
log.verbose('Wechaty', 'on(%s, listener) registering... listenerCount: %s',
event,
this.listenerCount(event),
)

return super.on(event, listener)
if (typeof middlewareOrListener === 'function') {
listener = middlewareOrListener
middlewareOrListener = []
}
if (listener && typeof listener === 'function') {
const listenerWithMiddleWare = compose(
// global middlewares -> event middlewares -> listener
(WechatyImpl._globalMiddleWares[event] as any || [])
.concat(middlewareOrListener as any)
.concat(listener as any),
)
return super.on(event, listenerWithMiddleWare)
}
// TODO: Do we need check the params not match issue? e.g. if they are not using ts(or use any) and give some invalid params.
throw new Error('Paramters does not match')
}

override async onStart (): Promise<void> {
Expand Down

0 comments on commit 1d7a9de

Please sign in to comment.