initial commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
|
||||
import type { BridgeRequest, RpcResponse } from "../shared/protocol";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
wss.on("connection", (ws) => {
|
||||
console.log("[WS] client connected");
|
||||
|
||||
/*
|
||||
* First connection from Tampermonkey is the
|
||||
* WhatsApp bridge.
|
||||
*/
|
||||
if (!whatsapp) {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`WhatsApp bridge listening on ws://127.0.0.1:${PORT}`);
|
||||
Reference in New Issue
Block a user