initial commit

This commit is contained in:
2026-09-14 17:15:00 +08:00
commit c78b75297f
19 changed files with 2421 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import { WhatsAppClient } from "./whatsapp";
const wa = new WhatsAppClient("ws://127.0.0.1:8787");
await wa.connect();
const chats = await wa.listChats({
limit: 10,
groupsOnly: true,
// usersOnly: true,
});
console.log(
chats.map((i) => ({
id: typeof i.id === "object" ? i.id?._serialized : i.id,
lid: i.accountLid,
name: i.formattedTitle,
})),
);
const messages = await wa.getMessages({
chatId: "[email protected]",
count: 5,
});
console.log(
messages.map((i) => ({
id: i.id.id,
from: i.from,
text: i.body,
})),
);
// const result = await wa.searchMessages({
// query: "pasien",
// chatId: "[email protected]",
// limit: 20,
// });
// await wa.sendText({
// chatId: "[email protected]",
// text: "Hello from Node!",
// });
+138
View File
@@ -0,0 +1,138 @@
import WebSocket from "ws";
import type {
GetMessagesParams,
ListChatsParams,
SearchMessagesParams,
SendTextParams,
} from "../shared/protocol";
import type {
ChatDTO,
ContactDTO,
GroupMetadataDTO,
MessageDTO,
} from "../shared/dto";
export class WhatsAppClient {
private url: string = "";
private isConnected: boolean = false;
private socket: WebSocket | null = null;
private pending = new Map<
string,
{
resolve: (value: any) => void;
reject: (error: Error) => void;
timer: NodeJS.Timeout;
}
>();
constructor(url = "ws://127.0.0.1:8787") {
this.url = url;
}
async connect() {
return new Promise<void>((resolve, reject) => {
if (this.socket) return;
this.socket = new WebSocket(this.url);
this.socket.on("open", () => {
console.log("Connected to bridge");
this.isConnected = true;
resolve();
});
this.socket.on("message", (data) => {
const message = JSON.parse(data.toString());
if (message.type !== "response") {
return;
}
const pending = this.pending.get(message.id);
if (!pending) {
return;
}
this.pending.delete(message.id);
clearTimeout(pending.timer);
if (message.ok) {
pending.resolve(message.result);
} else {
pending.reject(new Error(message.error?.message ?? "Unknown error"));
}
});
this.socket.on("close", () => {
this.socket = null;
this.isConnected = false;
setTimeout(() => this.connect(), 3000);
});
});
}
private request<T>(method: string, params?: unknown): Promise<T> {
if (!this.isConnected || !this.socket) {
throw new Error("WhatsApp is not connected");
}
const id = crypto.randomUUID();
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Request timeout: ${method}`));
}, 30_000);
this.pending.set(id, {
resolve,
reject,
timer,
});
this.socket!.send(
JSON.stringify({
type: "request",
id,
method,
params,
}),
);
});
}
status() {
return this.request<{
authenticated: boolean;
ready: boolean;
}>("status");
}
listChats(params?: ListChatsParams) {
return this.request<ChatDTO[]>("listChats", params);
}
listContacts(params?: { limit?: number }) {
return this.request<ContactDTO[]>("listContacts", params);
}
listGroups() {
return this.request<GroupMetadataDTO[]>("listGroups");
}
getMessages(params: GetMessagesParams) {
return this.request<MessageDTO[]>("getMessages", params);
}
searchMessages(params: SearchMessagesParams) {
return this.request<MessageDTO[]>("searchMessages", params);
}
sendText(params: SendTextParams) {
return this.request<any>("sendText", params);
}
}
+206
View File
@@ -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}`);
+199
View File
@@ -0,0 +1,199 @@
export type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| {
[key: string]: JsonValue;
};
export interface SerializedId {
_serialized?: string;
user?: string;
server?: string;
domain?: string;
device?: string;
agent?: string;
[key: string]: JsonValue | undefined;
}
export interface ContactDTO {
id: SerializedId | string | null;
accountLid?: string | null;
name?: string | null;
pushname?: string | null;
shortName?: string | null;
formattedName?: string | null;
number?: string | null;
isMe?: boolean;
isUser?: boolean;
isBusiness?: boolean;
isEnterprise?: boolean;
isGroup?: boolean;
isMyContact?: boolean;
isBlocked?: boolean;
profilePicThumbObj?: unknown;
}
export interface ChatDTO {
id: SerializedId | string | null;
accountLid?: string | null;
name?: string | null;
formattedTitle?: string | null;
isGroup?: boolean;
isUser?: boolean;
isNewsletter?: boolean;
isCommunity?: boolean;
isReadOnly?: boolean;
isAnnounceGrpRestrict?: boolean;
archived?: boolean;
pinned?: boolean;
muted?: boolean;
unreadCount?: number;
unreadMentionsCount?: number;
timestamp?: number | null;
lastMessage?: MessageDTO | null;
contact?: ContactDTO | null;
groupMetadata?: GroupMetadataDTO | null;
}
export interface GroupMetadataDTO {
id: SerializedId | string | null;
subject?: string | null;
subjectOwner?: SerializedId | string | null;
subjectTime?: number | null;
creation?: number | null;
owner?: SerializedId | string | null;
participants?: GroupParticipantDTO[];
size?: number;
restrict?: boolean;
announce?: boolean;
isCommunity?: boolean;
isCommunityAnnounce?: boolean;
}
export interface GroupParticipantDTO {
id: SerializedId | string | null;
isAdmin?: boolean;
isSuperAdmin?: boolean;
isSuperParticipant?: boolean;
isBusiness?: boolean;
}
export interface MessageDTO {
id: MessageIdDTO;
chatId?: string | null;
from?: SerializedId | string | null;
to?: SerializedId | string | null;
author?: SerializedId | string | null;
fromMe?: boolean;
body?: string | null;
caption?: string | null;
type?: string | null;
subtype?: string | null;
timestamp?: number | null;
ack?: number | null;
hasMedia?: boolean;
mediaKey?: string | null;
isForwarded?: boolean;
forwardingScore?: number;
isStarred?: boolean;
broadcast?: boolean;
mentionedIds?: (SerializedId | string)[];
quotedMsgId?: MessageIdDTO | null;
quotedMsg?: MessageDTO | null;
links?: MessageLinkDTO[];
location?: LocationDTO | null;
vCard?: string | null;
vCards?: string[];
filename?: string | null;
mimetype?: string | null;
size?: number | null;
duration?: number | null;
}
export interface MessageIdDTO {
id?: string | null;
_serialized?: string | null;
fromMe?: boolean;
remote?: SerializedId | string | null;
participant?: SerializedId | string | null;
}
export interface MessageLinkDTO {
link?: string | null;
isSuspicious?: boolean;
}
export interface LocationDTO {
latitude?: number | null;
longitude?: number | null;
description?: string | null;
address?: string | null;
url?: string | null;
name?: string | null;
}
export interface SentMessageDTO {
id: MessageIdDTO | null;
ack?: number | null;
timestamp?: number | null;
chatId?: string | null;
from?: SerializedId | string | null;
to?: SerializedId | string | null;
body?: string | null;
type?: string | null;
}
+71
View File
@@ -0,0 +1,71 @@
export type RequestId = string;
export interface RpcRequest<M extends string = string, P = unknown> {
type: "request";
id: RequestId;
method: M;
params: P;
}
export interface RpcResponse<T = unknown> {
type: "response";
id: RequestId;
ok: boolean;
result?: T;
error?: {
code: string;
message: string;
};
}
export interface RpcEvent<T = unknown> {
type: "event";
event: string;
data: T;
}
export type ServerMessage = RpcResponse | RpcEvent;
export interface ListChatsParams {
limit?: number;
groupsOnly?: boolean;
usersOnly?: boolean;
}
export interface GetMessagesParams {
chatId: string;
count?: number;
direction?: "before" | "after";
id?: string;
}
export interface SearchMessagesParams {
query: string;
chatId?: string;
limit?: number;
}
export interface SendTextParams {
chatId: string;
text: string;
options?: {
delay?: number;
quotedMsg?: string;
mentionedList?: string[];
waitForAck?: boolean;
};
}
export interface GetContactsParams {
limit?: number;
}
export type BridgeRequest =
| RpcRequest<"status">
| RpcRequest<"listChats", ListChatsParams>
| RpcRequest<"listContacts", GetContactsParams>
| RpcRequest<"listGroups">
| RpcRequest<"getMessages", GetMessagesParams>
| RpcRequest<"searchMessages", SearchMessagesParams>
| RpcRequest<"sendText", SendTextParams>;
+257
View File
@@ -0,0 +1,257 @@
import type { BridgeRequest } from "../shared/protocol";
import {
serialize,
serializeChat,
serializeContact,
serializeGroupMetadata,
serializeMessage,
} from "./serializer";
const WS_URL = "ws://127.0.0.1:8787";
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();
+298
View File
@@ -0,0 +1,298 @@
import type {
ChatDTO,
ContactDTO,
GroupMetadataDTO,
GroupParticipantDTO,
LocationDTO,
MessageDTO,
MessageIdDTO,
SerializedId,
} from "../shared/dto";
/**
* WA models contain a lot of internal properties and sometimes
* circular references. Never JSON.stringify them directly.
*/
export function serialize(value: any): any {
if (value == null) {
return value;
}
if (Array.isArray(value)) {
return value.map(serialize);
}
if (typeof value !== "object") {
return value;
}
const result: Record<string, unknown> = {};
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<T>(object: unknown, key: string): T | undefined {
try {
if (object && typeof object === "object") {
return (object as any)[key];
}
} catch {
return undefined;
}
return undefined;
}
export function safeGet<T = unknown>(
object: unknown,
key: string,
): T | undefined {
if (object == null || typeof object !== "object") {
return undefined;
}
try {
return (object as any)[key];
} catch {
return undefined;
}
}
export function pick<T extends object>(
source: any,
keys: readonly string[],
): Partial<T> {
const result: Record<string, unknown> = {};
for (const key of keys) {
const value = safeGet(source, key);
if (value !== undefined) {
result[key] = serialize(value);
}
}
return result as Partial<T>;
}
export function serializeId(id: any): SerializedId | string | null {
if (id == null) {
return null;
}
if (typeof id === "string") {
return id;
}
if (typeof id !== "object") {
return String(id);
}
const result: SerializedId = {};
for (const key of [
"_serialized",
"user",
"server",
"domain",
"device",
"agent",
"fromMe",
"remote",
"participant",
]) {
const value = safeGet(id, key);
if (value !== undefined) {
result[key] = serialize(value) as any;
}
}
// Some WA objects expose useful fields that aren't
// enumerable.
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);
}
export function serializeChat(chat: any): ChatDTO {
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,
};
}
export function serializeContact(contact: any): ContactDTO {
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,
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)
: undefined,
};
}
export function serializeGroupParticipant(
participant: any,
): GroupParticipantDTO {
return {
id: serializeId(participant.id),
isAdmin: participant.isAdmin ?? false,
isSuperAdmin: participant.isSuperAdmin ?? false,
isSuperParticipant: participant.isSuperParticipant ?? false,
isBusiness: participant.isBusiness ?? false,
};
}
export function serializeGroupMetadata(metadata: any): GroupMetadataDTO {
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) as GroupParticipantDTO[])
: [],
size: metadata.size ?? metadata.participants?.length ?? 0,
restrict: metadata.restrict ?? false,
announce: metadata.announce ?? false,
isCommunity: metadata.isCommunity ?? false,
isCommunityAnnounce: metadata.isCommunityAnnounce ?? false,
};
}
export function serializeMessageId(id: any): MessageIdDTO | null {
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),
};
}
export function serializeLocation(location: any): LocationDTO | null {
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,
};
}
export function serializeMessageLink(link: any): any {
return {
link: link?.link ?? null,
isSuspicious: link?.isSuspicious ?? false,
};
}
export function serializeMessage(message: any): MessageDTO {
const quotedMsgId = safeProperty<any>(message, "quotedMsgId");
const hasQuoted = !!quotedMsgId;
let quotedMsg = null;
if (hasQuoted) {
const rawQuoted = safeProperty<any>(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,
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,
};
}
+62
View File
@@ -0,0 +1,62 @@
declare const WPP: {
isReady: boolean;
loader: {
onReady(callback: () => void): void;
};
conn: {
isAuthenticated(): boolean;
};
chat: {
list(options?: {
count?: number;
onlyUsers?: boolean;
onlyGroups?: boolean;
onlyCommunities?: boolean;
onlyNewsletter?: boolean;
onlyArchived?: boolean;
}): Promise<any[]>;
get(chatId: string): any | undefined;
getMessages(
chatId: string,
options?: {
count?: number;
direction?: "before" | "after";
id?: string;
includeCallMessages?: boolean;
media?: "image" | "document" | "url" | "all";
onlyUnread?: boolean;
},
): Promise<any[]>;
sendTextMessage(
chatId: string,
text: string,
options?: {
delay?: number;
detectMentioned?: boolean;
markIsRead?: boolean;
mentionedList?: string[];
messageId?: string;
quotedMsg?: string;
quotedMsgPayload?: string;
waitForAck?: boolean;
},
): Promise<any>;
on(event: string, callback: (...args: any[]) => void): void;
};
contact: {
getAllContacts(): Promise<any[]>;
get(contactId: string): any | undefined;
};
group: {
getAllGroups(): Promise<any[]>;
};
};
View File