223 lines
4.7 KiB
TypeScript
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}`);
|