diff --git a/package.json b/package.json index b598bd9e..7acf7d70 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@types/jest": "^29.5.12", "@types/node": "^22.0.0", "@types/qrcode-terminal": "^0.12.2", - "@types/ws": "^8.5.10", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "eslint": "^8.57.0", @@ -46,6 +46,7 @@ }, "dependencies": { "ajv": "^8.13.0", + "async-mutex": "^0.5.0", "chalk": "^5.3.0", "commander": "^12.1.0", "cors": "^2.8.5", @@ -59,6 +60,6 @@ "qrcode-terminal": "^0.12.0", "silk-wasm": "^3.6.1", "strtok3": "8.0.1", - "ws": "^8.16.0" + "ws": "^8.18.0" } } diff --git a/src/onebot/network/index.ts b/src/onebot/network/index.ts index 18d2840f..673c4e4d 100644 --- a/src/onebot/network/index.ts +++ b/src/onebot/network/index.ts @@ -4,4 +4,6 @@ import { OB11BaseEvent } from '@/onebot/event/OB11BaseEvent'; export interface IOB11NetworkAdapter { registerAction, P, R>(action: T): void; onEvent(event: T): void; + open(): void | Promise; + close(): void | Promise; } diff --git a/src/onebot/network/passive-websocket.ts b/src/onebot/network/passive-websocket.ts new file mode 100644 index 00000000..da321a61 --- /dev/null +++ b/src/onebot/network/passive-websocket.ts @@ -0,0 +1,72 @@ +import { IOB11NetworkAdapter } from './index'; +import { OB11BaseEvent } from '@/onebot/event/OB11BaseEvent'; +import BaseAction from '@/onebot/action/BaseAction'; +import { WebSocket, WebSocketServer } from 'ws'; +import { Mutex } from 'async-mutex'; + +export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { + wsServer: WebSocketServer; + wsClients: WebSocket[] = []; + wsClientsMutex = new Mutex(); + isOpen: boolean = false; + hasBeenClosed: boolean = false; + + private actionMap: Map> = new Map(); + + constructor(ip: string, port: number, token: string) { + this.wsServer = new WebSocketServer({ port: port, host: ip }); + this.wsServer.on('connection', async (wsClient) => { + if (!this.isOpen) { + wsClient.close(); + return; + } + if (token) { + const incomingToken = wsClient.url.split('?')[1]?.split('=')[1]; + if (incomingToken !== token) { + wsClient.close(); + return; + } + } + wsClient.on('message', (message) => { + // TODO: extract action name and payload from the message, then call the corresponding action. + }); + wsClient.once('close', () => { + this.wsClientsMutex.runExclusive(async () => { + const index = this.wsClients.indexOf(wsClient); + if (index !== -1) { + this.wsClients.splice(index, 1); + } + }); + }); + await this.wsClientsMutex.runExclusive(async () => { + this.wsClients.push(wsClient); + }); + }); + } + + registerAction, P, R>(action: T) { + this.actionMap.set(action.actionName, action); + } + + onEvent(event: T) { + this.wsClientsMutex.runExclusive(async () => { + this.wsClients.forEach((wsClient) => { + // wsClient.send(JSON.stringify(event)); + // TODO: wrap the event, and send the wrapped to the client. + }); + }); + } + + open() { + if (this.hasBeenClosed) { + throw new Error('Cannot open a closed WebSocket server'); + } + this.isOpen = true; + } + + async close() { + this.isOpen = false; + this.hasBeenClosed = true; + this.wsServer.close(); + } +}