Files
whatsapp-bridge/src/userscript/index.ts
T
2026-09-14 17:31:46 +08:00

258 lines
5.6 KiB
TypeScript

import type { BridgeRequest } from "../shared/protocol";
import {
serialize,
serializeChat,
serializeContact,
serializeGroupMetadata,
serializeMessage,
} from "./serializer";
const WS_URL = "wss://wa-bridge.rul.sh/ws";
let socket: WebSocket | undefined;
function send(message: unknown) {
if (socket?.readyState !== WebSocket.OPEN) {
throw new Error("WebSocket is not connected");
}
socket.send(JSON.stringify(message));
}
function reply(id: string, result: unknown) {
send({
type: "response",
id,
ok: true,
result: serialize(result),
});
}
function error(id: string, err: unknown) {
send({
type: "response",
id,
ok: false,
error: {
code: "ERROR",
message: err instanceof Error ? err.message : String(err),
},
});
}
async function handleRequest(request: BridgeRequest) {
switch (request.method) {
case "status":
return {
authenticated: WPP.conn.isAuthenticated(),
ready: WPP.isReady,
};
case "listChats": {
const chats = await WPP.chat.list({
...(request.params?.limit != null
? { count: request.params.limit }
: {}),
...(request.params?.groupsOnly ? { onlyGroups: true } : {}),
...(request.params?.usersOnly ? { onlyUsers: true } : {}),
});
return chats.filter(Boolean).map(serializeChat);
}
case "listContacts": {
const contacts = await WPP.contact.getAllContacts();
const result = request.params?.limit
? contacts.slice(0, request.params.limit)
: contacts;
return result.filter(Boolean).map(serializeContact);
}
case "listGroups": {
const groups = await WPP.group.getAllGroups();
return groups.filter(Boolean).map(serializeGroupMetadata);
}
case "getMessages": {
const messages = await WPP.chat.getMessages(request.params.chatId, {
count: request.params.count ?? 50,
direction: request.params.direction,
id: request.params.id,
});
return messages.filter(Boolean).map(serializeMessage);
}
case "searchMessages":
return searchMessages(
request.params.query,
request.params.chatId,
request.params.limit ?? 100,
);
case "sendText": {
const result = await WPP.chat.sendTextMessage(
request.params.chatId,
request.params.text,
{
delay: request.params.options?.delay,
quotedMsg: request.params.options?.quotedMsg,
mentionedList: request.params.options?.mentionedList,
waitForAck: request.params.options?.waitForAck,
},
);
return serialize(result);
}
default:
throw new Error(`Unknown method: ${(request as any).method}`);
}
}
/**
* WA-JS doesn't need a separate "global search" API for our
* first implementation. Search the messages loaded from chats.
*
* Later we can optimize this using WhatsApp's internal message
* store/index if necessary.
*/
async function searchMessages(query: string, chatId?: string, limit = 100) {
const normalized = query.toLowerCase();
let chats;
if (chatId) {
const chat = WPP.chat.get(chatId);
chats = chat ? [chat] : [];
} else {
chats = await WPP.chat.list();
}
const results: any[] = [];
for (const chat of chats) {
if (results.length >= limit) {
break;
}
const id = chat?.id?._serialized ?? chat?.id ?? chat?.wid?._serialized;
if (!id) {
continue;
}
try {
const messages = await WPP.chat.getMessages(id, {
count: 100,
});
for (const message of messages) {
const body = String(message.body ?? message.caption ?? "");
if (body.toLowerCase().includes(normalized)) {
results.push({
chatId: id,
message: serialize(message),
});
if (results.length >= limit) {
break;
}
}
}
} catch (err) {
console.warn("Failed searching chat", id, err);
}
}
return results;
}
function connect() {
if (
socket &&
(socket.readyState === WebSocket.OPEN ||
socket.readyState === WebSocket.CONNECTING)
) {
return;
}
socket = new WebSocket(WS_URL);
socket.onopen = () => {
console.log("[WA Bridge] connected");
send({
type: "event",
event: "ready",
data: {
authenticated: WPP.conn.isAuthenticated(),
},
});
};
socket.onmessage = async (event) => {
try {
const request = JSON.parse(event.data) as BridgeRequest;
if (request.type !== "request") {
return;
}
try {
const result = await handleRequest(request);
reply(request.id, JSON.stringify(result));
} catch (err) {
error(request.id, err);
}
} catch (err) {
console.error("[WA Bridge] invalid message", err);
}
};
socket.onclose = (event) => {
console.log("[WA Bridge] disconnected", {
code: event.code,
reason: event.reason,
wasClean: event.wasClean,
});
socket = undefined;
setTimeout(connect, 3000);
};
socket.onerror = (event) => {
console.error("[WA Bridge] websocket error", event);
};
}
function start() {
WPP.loader.onReady(() => {
console.log("[WA Bridge] WA-JS ready");
connect();
/*
* Forward new-message events to Node.
*/
WPP.chat.on("chat.new_message", (message: unknown) => {
if (socket?.readyState !== WebSocket.OPEN) {
return;
}
send({
type: "event",
event: "message",
data: serialize(message),
});
});
});
}
start();