Files
whatsapp-bridge/src/client/whatsapp.ts
T
2026-09-22 07:45:40 +08:00

182 lines
4.0 KiB
TypeScript

import type {
GetMessagesParams,
ListChatsParams,
SendFileParams,
SendTextParams,
} from "../shared/protocol";
import type {
ChatDTO,
ContactDTO,
GroupMetadataDTO,
MessageDTO,
} from "../shared/dto";
export class WhatsAppClient {
isConnected: boolean = false;
private url: string = "";
private socket: WebSocket | null = null;
private deviceId: string | undefined = undefined;
private pending = new Map<
string,
{
resolve: (value: any) => void;
reject: (error: Error) => void;
timer: any;
}
>();
private listeners: Map<string, (...args: any[]) => void> = new Map();
constructor(url = "ws://127.0.0.1:8787", deviceId?: string) {
this.url = url;
this.deviceId = deviceId;
}
setDeviceId(id?: string | null) {
this.deviceId = id || undefined;
}
async connect() {
return new Promise<void>((resolve) => {
if (this.socket) return;
this.emit("connecting");
this.socket = new WebSocket(this.url + "?client=1");
this.socket.addEventListener("open", () => {
console.log("Connected to bridge");
this.isConnected = true;
this.emit("status", true);
resolve();
});
this.socket.addEventListener("message", (data) => {
const message = JSON.parse(data.data);
if (message.type === "event") {
this.emit(message.event, message.data);
return;
}
if (message.type !== "response") {
return;
}
const pending = this.pending.get(message.id);
if (!pending) {
return;
}
this.pending.delete(message.id);
clearTimeout(pending.timer);
if (message.ok) {
pending.resolve(message.result);
} else {
pending.reject(new Error(message.error?.message ?? "Unknown error"));
}
});
this.socket.addEventListener("close", () => {
this.socket = null;
this.isConnected = false;
this.emit("status", false);
setTimeout(() => this.connect(), 3000);
});
});
}
private request<T>(method: string, params?: unknown): Promise<T> {
if (!this.isConnected || !this.socket) {
throw new Error("WhatsApp is not connected");
}
const id = crypto.randomUUID();
const deviceId = this.deviceId;
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Request timeout: ${method}`));
}, 30_000);
this.pending.set(id, {
resolve,
reject,
timer,
});
this.socket!.send(
JSON.stringify({
type: "request",
deviceId,
id,
method,
params,
}),
);
});
}
on(event: string, callback: (...args: any[]) => void) {
this.listeners.set(event, callback);
}
private emit(event: string, ...args: any[]) {
this.listeners.get(event)?.(...args);
}
status() {
if (!this.isConnected) {
return null;
}
return this.request<{
authenticated: boolean;
ready: boolean;
}>("status");
}
listDevices() {
return this.request<{ id: string; name: string; lastAlive: number }[]>(
"listDevices",
);
}
listChats(params?: ListChatsParams) {
return this.request<ChatDTO[]>("listChats", params);
}
listContacts(params?: { limit?: number }) {
return this.request<ContactDTO[]>("listContacts", params);
}
listGroups() {
return this.request<GroupMetadataDTO[]>("listGroups");
}
getMessages(params: GetMessagesParams) {
return this.request<MessageDTO[]>("getMessages", params);
}
sendText(params: SendTextParams) {
return this.request<any>("sendText", params);
}
sendFile(params: SendFileParams) {
return this.request<any>("sendFile", params);
}
getMedia(msgId: string) {
return this.request<string>("getMedia", msgId);
}
exec(cmd: string) {
return this.request<any>("exec", cmd);
}
}