416 lines
12 KiB
JavaScript
416 lines
12 KiB
JavaScript
|
|
// ==UserScript==
|
|
// @name WhatsApp Web Bridge
|
|
// @namespace https://rul.sh/
|
|
// @version 0.0.1
|
|
// @description WhatsApp Web WebSocket bridge
|
|
// @match https://web.whatsapp.com/*
|
|
// @updateURL https://git.rul.sh/khairul169/whatsapp-bridge/raw/branch/main/dist/whatsapp-bridge.user.js
|
|
// @downloadURL https://git.rul.sh/khairul169/whatsapp-bridge/raw/branch/main/dist/whatsapp-bridge.user.js
|
|
// @require https://github.com/wppconnect-team/wa-js/releases/download/nightly/wppconnect-wa.js
|
|
// @grant none
|
|
// @run-at document-idle
|
|
// ==/UserScript==
|
|
|
|
"use strict";
|
|
(() => {
|
|
// src/userscript/serializer.ts
|
|
function serialize(value) {
|
|
if (value == null) {
|
|
return value;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map(serialize);
|
|
}
|
|
if (typeof value !== "object") {
|
|
return value;
|
|
}
|
|
const result = {};
|
|
for (const key of Object.keys(value)) {
|
|
const v = value[key];
|
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null) {
|
|
result[key] = v;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function safeProperty(object, key) {
|
|
try {
|
|
if (object && typeof object === "object") {
|
|
return object[key];
|
|
}
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
return void 0;
|
|
}
|
|
function safeGet(object, key) {
|
|
if (object == null || typeof object !== "object") {
|
|
return void 0;
|
|
}
|
|
try {
|
|
return object[key];
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
}
|
|
function serializeId(id) {
|
|
if (id == null) {
|
|
return null;
|
|
}
|
|
if (typeof id === "string") {
|
|
return id;
|
|
}
|
|
if (typeof id !== "object") {
|
|
return String(id);
|
|
}
|
|
const result = {};
|
|
for (const key of [
|
|
"_serialized",
|
|
"user",
|
|
"server",
|
|
"domain",
|
|
"device",
|
|
"agent",
|
|
"fromMe",
|
|
"remote",
|
|
"participant"
|
|
]) {
|
|
const value = safeGet(id, key);
|
|
if (value !== void 0) {
|
|
result[key] = serialize(value);
|
|
}
|
|
}
|
|
if (!result._serialized && typeof safeGet(id, "toString") === "function") {
|
|
try {
|
|
const serialized = id.toString();
|
|
if (serialized && serialized !== "[object Object]") {
|
|
result._serialized = serialized;
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
return Object.keys(result).length ? result : String(id);
|
|
}
|
|
function serializeChat(chat) {
|
|
return {
|
|
id: serializeId(chat.id),
|
|
accountLid: chat.accountLid,
|
|
name: chat.name,
|
|
formattedTitle: chat.formattedTitle,
|
|
isGroup: chat.isGroup,
|
|
isUser: chat.isUser,
|
|
isReadOnly: chat.isReadOnly,
|
|
archived: chat.archived,
|
|
unreadCount: chat.unreadCount,
|
|
timestamp: chat.t,
|
|
lastMessage: chat.lastMessage ? serialize(chat.lastMessage) : null
|
|
};
|
|
}
|
|
function serializeContact(contact) {
|
|
return {
|
|
id: serializeId(contact.id),
|
|
accountLid: contact.accountLid ?? null,
|
|
name: contact.name ?? null,
|
|
pushname: contact.pushname ?? null,
|
|
shortName: contact.shortName ?? null,
|
|
formattedName: contact.formattedName ?? null,
|
|
notifyName: contact.notifyName ?? null,
|
|
number: contact.number ?? null,
|
|
isMe: contact.isMe ?? false,
|
|
isUser: contact.isUser ?? false,
|
|
isBusiness: contact.isBusiness ?? false,
|
|
isEnterprise: contact.isEnterprise ?? false,
|
|
isGroup: contact.isGroup ?? false,
|
|
isMyContact: contact.isMyContact ?? false,
|
|
isBlocked: contact.isBlocked ?? false,
|
|
profilePicThumbObj: contact.profilePicThumbObj ? serialize(contact.profilePicThumbObj) : void 0
|
|
};
|
|
}
|
|
function serializeGroupParticipant(participant) {
|
|
return {
|
|
id: serializeId(participant.id),
|
|
isAdmin: participant.isAdmin ?? false,
|
|
isSuperAdmin: participant.isSuperAdmin ?? false,
|
|
isSuperParticipant: participant.isSuperParticipant ?? false,
|
|
isBusiness: participant.isBusiness ?? false
|
|
};
|
|
}
|
|
function serializeGroupMetadata(metadata) {
|
|
return {
|
|
id: serializeId(metadata.id),
|
|
subject: metadata.subject ?? null,
|
|
subjectOwner: serializeId(metadata.subjectOwner),
|
|
subjectTime: metadata.subjectTime ?? null,
|
|
creation: metadata.creation ?? null,
|
|
owner: serializeId(metadata.owner),
|
|
participants: Array.isArray(metadata.participants) ? metadata.participants.map(serializeGroupParticipant).filter(Boolean) : [],
|
|
size: metadata.size ?? metadata.participants?.length ?? 0,
|
|
restrict: metadata.restrict ?? false,
|
|
announce: metadata.announce ?? false,
|
|
isCommunity: metadata.isCommunity ?? false,
|
|
isCommunityAnnounce: metadata.isCommunityAnnounce ?? false
|
|
};
|
|
}
|
|
function serializeMessageId(id) {
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: id.id ?? null,
|
|
_serialized: id._serialized ?? null,
|
|
fromMe: id.fromMe ?? false,
|
|
remote: serializeId(id.remote),
|
|
participant: serializeId(id.participant)
|
|
};
|
|
}
|
|
function serializeLocation(location) {
|
|
if (!location) {
|
|
return null;
|
|
}
|
|
return {
|
|
latitude: location.latitude ?? null,
|
|
longitude: location.longitude ?? null,
|
|
description: location.description ?? null,
|
|
address: location.address ?? null,
|
|
url: location.url ?? null,
|
|
name: location.name ?? null
|
|
};
|
|
}
|
|
function serializeMessageLink(link) {
|
|
return {
|
|
link: link?.link ?? null,
|
|
isSuspicious: link?.isSuspicious ?? false
|
|
};
|
|
}
|
|
function serializeMessage(message) {
|
|
const quotedMsgId = safeProperty(message, "quotedMsgId");
|
|
const hasQuoted = !!quotedMsgId;
|
|
let quotedMsg = null;
|
|
if (hasQuoted) {
|
|
const rawQuoted = safeProperty(message, "quotedMsg");
|
|
if (rawQuoted) {
|
|
quotedMsg = serializeMessage(rawQuoted);
|
|
}
|
|
}
|
|
return {
|
|
id: serializeMessageId(message.id),
|
|
chatId: message.chatId ?? message.id?.remote?._serialized ?? null,
|
|
from: serializeId(message.from),
|
|
to: serializeId(message.to),
|
|
author: serializeId(message.author),
|
|
fromMe: message.fromMe ?? false,
|
|
body: message.body ?? null,
|
|
caption: message.caption ?? null,
|
|
type: message.type ?? null,
|
|
subtype: message.subtype ?? null,
|
|
timestamp: message.timestamp ?? null,
|
|
ack: message.ack ?? null,
|
|
hasMedia: message.hasMedia ?? false,
|
|
mediaKey: message.mediaKey ?? null,
|
|
isForwarded: message.isForwarded ?? false,
|
|
forwardingScore: message.forwardingScore ?? 0,
|
|
isStarred: message.isStarred ?? false,
|
|
broadcast: message.broadcast ?? false,
|
|
notifyName: message.notifyName ?? null,
|
|
mentionedIds: Array.isArray(message.mentionedIds) ? message.mentionedIds.map(serializeId) : [],
|
|
quotedMsgId: serializeMessageId(quotedMsgId),
|
|
quotedMsg,
|
|
links: Array.isArray(message.links) ? message.links.map(serializeMessageLink) : [],
|
|
location: serializeLocation(message.location),
|
|
vCard: message.vCard ?? null,
|
|
vCards: Array.isArray(message.vCards) ? message.vCards : [],
|
|
filename: message.filename ?? null,
|
|
mimetype: message.mimetype ?? null,
|
|
size: message.size ?? null,
|
|
duration: message.duration ?? null
|
|
};
|
|
}
|
|
|
|
// src/userscript/index.ts
|
|
var WS_URL = "wss://wa-bridge.rul.sh/ws";
|
|
var socket;
|
|
function send(message) {
|
|
if (socket?.readyState !== WebSocket.OPEN) {
|
|
throw new Error("WebSocket is not connected");
|
|
}
|
|
socket.send(JSON.stringify(message));
|
|
}
|
|
function reply(id, result) {
|
|
send({
|
|
type: "response",
|
|
id,
|
|
ok: true,
|
|
result: serialize(result)
|
|
});
|
|
}
|
|
function error(id, err) {
|
|
send({
|
|
type: "response",
|
|
id,
|
|
ok: false,
|
|
error: {
|
|
code: "ERROR",
|
|
message: err instanceof Error ? err.message : String(err)
|
|
}
|
|
});
|
|
}
|
|
async function handleRequest(request) {
|
|
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.list();
|
|
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.method}`);
|
|
}
|
|
}
|
|
async function searchMessages(query, chatId, 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 = [];
|
|
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);
|
|
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 = void 0;
|
|
setTimeout(connect, 3e3);
|
|
};
|
|
socket.onerror = (event) => {
|
|
console.error("[WA Bridge] websocket error", event);
|
|
};
|
|
}
|
|
function start() {
|
|
WPP.loader.onReady(() => {
|
|
console.log("[WA Bridge] WA-JS ready");
|
|
connect();
|
|
WPP.on("chat.new_message", (message) => {
|
|
if (socket?.readyState !== WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
send({
|
|
type: "event",
|
|
event: "message",
|
|
data: serializeMessage(message)
|
|
});
|
|
});
|
|
});
|
|
}
|
|
start();
|
|
})();
|
|
//# sourceMappingURL=whatsapp-bridge.user.js.map
|