SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { WebSocket, type RawData } from 'ws';
|
||||
import { SnowLumaConnectionError, SnowLumaParseError } from '../errors.js';
|
||||
import { isJsonObject } from '../internal/json.js';
|
||||
import type { WsRole } from '../types/index.js';
|
||||
|
||||
/** API and Event channels share one account session and pending-request owner. */
|
||||
export class ReverseWebSocketTransport extends EventEmitter {
|
||||
readyState = WebSocket.OPEN as number;
|
||||
private readonly channels = new Map<WsRole, WebSocket>();
|
||||
|
||||
constructor(
|
||||
readonly selfId: string,
|
||||
private readonly maxBufferedBytes: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get canCallActions(): boolean {
|
||||
return this.apiSocket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
get canReceiveEvents(): boolean {
|
||||
return (
|
||||
(this.channels.get('Universal') ?? this.channels.get('Event'))?.readyState ===
|
||||
WebSocket.OPEN
|
||||
);
|
||||
}
|
||||
|
||||
canAttach(role: WsRole): boolean {
|
||||
return (
|
||||
this.readyState === WebSocket.OPEN &&
|
||||
!this.channels.has(role) &&
|
||||
!this.channels.has('Universal') &&
|
||||
(role !== 'Universal' || this.channels.size === 0)
|
||||
);
|
||||
}
|
||||
|
||||
attach(role: WsRole, socket: WebSocket): void {
|
||||
if (!this.canAttach(role)) {
|
||||
throw new SnowLumaConnectionError('Duplicate reverse WebSocket channel');
|
||||
}
|
||||
|
||||
this.channels.set(role, socket);
|
||||
socket.on('message', (data: RawData) => this.receive(role, data));
|
||||
socket.on('error', (error: Error) => {
|
||||
this.close(1011, 'connection failed');
|
||||
this.emit('error', error);
|
||||
});
|
||||
socket.once('close', (code: number, reason: Buffer) => {
|
||||
this.close(code, reason.toString('utf8'));
|
||||
});
|
||||
}
|
||||
|
||||
send(data: string): void {
|
||||
const socket = this.apiSocket;
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN || socket?.readyState !== WebSocket.OPEN) {
|
||||
throw new SnowLumaConnectionError('This reverse session has no connected API channel');
|
||||
}
|
||||
|
||||
if (socket.bufferedAmount + Buffer.byteLength(data) > this.maxBufferedBytes) {
|
||||
throw new SnowLumaConnectionError('Reverse WebSocket outbound buffer limit exceeded');
|
||||
}
|
||||
|
||||
socket.send(data, (error) => {
|
||||
if (error) {
|
||||
this.close(1011, 'send failed');
|
||||
this.emit('error', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(code = 1000, reason = 'normal'): void {
|
||||
if (this.readyState === WebSocket.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
|
||||
for (const socket of this.channels.values()) {
|
||||
if (socket.readyState === WebSocket.CLOSED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A peer's abnormal close code cannot be sent on the wire. Closing
|
||||
// either half also ends its sibling: pending actions never survive
|
||||
// into a replacement connection with uncertain execution state.
|
||||
socket.close(code === 1005 || code === 1006 || code === 1015 ? 1001 : code, reason);
|
||||
const timer = setTimeout(() => socket.terminate(), 1000);
|
||||
timer.unref();
|
||||
socket.once('close', () => clearTimeout(timer));
|
||||
}
|
||||
|
||||
this.channels.clear();
|
||||
this.emit('close', code, reason);
|
||||
}
|
||||
|
||||
private get apiSocket(): WebSocket | undefined {
|
||||
return this.channels.get('Universal') ?? this.channels.get('Api');
|
||||
}
|
||||
|
||||
private receive(role: WsRole, data: RawData): void {
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = Buffer.isBuffer(data)
|
||||
? data.toString('utf8')
|
||||
: Array.isArray(data)
|
||||
? Buffer.concat(data).toString('utf8')
|
||||
: Buffer.from(data).toString('utf8');
|
||||
let parsed: unknown;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
this.fail(new SnowLumaParseError('Reverse peer sent invalid JSON'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isJsonObject(parsed)) {
|
||||
this.fail(new SnowLumaParseError('Reverse peer frame must be a JSON object'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof parsed.post_type === 'string') {
|
||||
if (role === 'Api' || String(parsed.self_id) !== this.selfId) {
|
||||
this.fail(
|
||||
new SnowLumaParseError(
|
||||
'Reverse event account or channel does not match its handshake',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
role === 'Event' ||
|
||||
parsed.echo === undefined ||
|
||||
(parsed.status !== 'ok' && parsed.status !== 'failed')
|
||||
) {
|
||||
this.fail(new SnowLumaParseError('Reverse peer sent an unexpected action response'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('message', text);
|
||||
}
|
||||
|
||||
private fail(error: Error): void {
|
||||
// Preserve an error observer until all underlying socket callbacks end.
|
||||
// The owner receives diagnostics independently of SDK close cleanup.
|
||||
try {
|
||||
this.emit('error', error);
|
||||
} finally {
|
||||
this.close(1008, 'invalid OneBot frame');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user