Files
whatsapp-bridge/src/server/main.ts
T
2026-09-15 17:55:48 +08:00

223 lines
4.7 KiB
TypeScript

import { WebSocketServer, WebSocket } from "ws";
import type { BridgeRequest, RpcResponse } from "../shared/protocol";
import { initDb, insertMessage } from "./db";
import type { MessageDTO } from "../shared/dto";
const PORT = 8787;
const wss = new WebSocketServer({
host: "0.0.0.0",
port: PORT,
});
let whatsapp: WebSocket | undefined;
const pending = new Map<
string,
{
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
timer: NodeJS.Timeout;
}
>();
function requestId() {
return crypto.randomUUID();
}
function call<T>(
method: BridgeRequest["method"],
params?: unknown,
): Promise<T> {
if (!whatsapp || whatsapp.readyState !== WebSocket.OPEN) {
throw new Error("WhatsApp Web is not connected");
}
const id = requestId();
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`WhatsApp request timed out: ${method}`));
}, 30_000);
pending.set(id, {
resolve: resolve as never,
reject,
timer,
});
whatsapp!.send(
JSON.stringify({
type: "request",
id,
method,
params,
}),
);
});
}
function handleWhatsAppMessage(data: string) {
const message = JSON.parse(data) as
| RpcResponse
| {
type: "event";
event: string;
data: unknown;
};
if (message.type === "response") {
const request = pending.get(message.id);
if (!request) {
return;
}
pending.delete(message.id);
clearTimeout(request.timer);
if (message.ok) {
const res = JSON.parse(message.result as string);
request.resolve(res);
} else {
request.reject(
new Error(message.error?.message ?? "Unknown WhatsApp error"),
);
}
return;
}
if (message.type === "event") {
// console.log("[WhatsApp event]", message.event, message.data);
if (message.event === "message") {
const m = message.data as MessageDTO;
insertMessage({
id: m.id.id || m.id._serialized || "",
chatId: m.chatId || "",
from: typeof m.from === "string" ? m.from : m.from?._serialized || "",
to: typeof m.to === "string" ? m.to : m.to?._serialized || "",
fromMe: !!m.fromMe,
data: m,
});
}
}
}
wss.on("connection", (ws) => {
console.log("[WS] client connected");
/*
* First connection from Tampermonkey is the
* WhatsApp bridge.
*/
if (!whatsapp && !ws.url.includes("?client=1")) {
whatsapp = ws;
console.log("[WS] WhatsApp bridge attached");
ws.on("message", (data) => {
handleWhatsAppMessage(data.toString());
});
ws.on("close", () => {
if (whatsapp === ws) {
whatsapp = undefined;
}
console.log("[WS] WhatsApp disconnected");
});
return;
}
/*
* This is an external application/client.
*
* Example:
* my backend
* CLI
* React application
* another service
*/
ws.on("message", async (data) => {
let request: any;
try {
request = JSON.parse(data.toString()) as BridgeRequest;
if (request.type !== "request") {
return;
}
let result: unknown;
switch (request.method) {
case "status":
result = await call("status", request.params);
break;
case "listChats":
result = await call("listChats", request.params);
break;
case "listContacts":
result = await call("listContacts", request.params);
break;
case "listGroups":
result = await call("listGroups", request.params);
break;
case "getMessages":
result = await call("getMessages", request.params);
break;
case "searchMessages":
result = await call("searchMessages", request.params);
break;
case "sendText":
result = await call("sendText", request.params);
break;
default:
throw new Error(`Unsupported method: ${(request as any).method}`);
}
ws.send(
JSON.stringify({
type: "response",
id: request.id,
ok: true,
result,
}),
);
} catch (err) {
ws.send(
JSON.stringify({
type: "response",
id: request?.id || null,
ok: false,
error: {
code: "ERROR",
message: err instanceof Error ? err.message : String(err),
},
}),
);
}
});
ws.on("close", () => {
console.log("[WS] external client disconnected");
});
});
initDb();
console.log(`WhatsApp bridge listening on ws://127.0.0.1:${PORT}`);