258 lines
5.6 KiB
TypeScript
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();
|