@manybot/manybot 5.5.4 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/client/cache.js +1 -1
- package/dist/client/store.js +9 -0
- package/dist/config.js +230 -12
- package/dist/drivers/baileys/adapter.js +556 -0
- package/dist/drivers/{whatsapp → baileys}/api/index.js +533 -386
- package/dist/drivers/baileys/index.js +560 -0
- package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
- package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
- package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +0 -29
- package/dist/drivers/jid.js +31 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/whatsmeow/client.js +203 -0
- package/dist/drivers/whatsmeow/index.js +79 -0
- package/dist/drivers/whatsmeow/installer.js +70 -0
- package/dist/drivers/whatsmeow/supervisor.js +309 -0
- package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
- package/dist/i18n/index.js +8 -12
- package/dist/kernel/alerts.js +190 -0
- package/dist/kernel/contactAutoSave.js +200 -0
- package/dist/kernel/driverManager.js +117 -0
- package/dist/kernel/pluginApi.js +25 -7
- package/dist/kernel/pluginLoader.js +12 -12
- package/dist/kernel/sendFallbackGuard.js +173 -0
- package/dist/kernel/sendGuard.js +143 -33
- package/dist/kernel/statusServer.js +39 -0
- package/dist/kernel/updateCheck.js +88 -0
- package/dist/kernel/waContract.js +16 -0
- package/dist/locales/en.json +15 -1
- package/dist/locales/es.json +15 -1
- package/dist/locales/pt.json +15 -1
- package/dist/main.js +77 -5
- package/dist/types.js +18 -11
- package/package.json +6 -8
- package/dist/core/adapter.js +0 -12
- package/dist/core/capabilities.js +0 -16
- package/dist/core/types.js +0 -6
- package/dist/drivers/index.js +0 -14
- package/dist/drivers/whatsapp/adapter.js +0 -7
- package/dist/drivers/whatsapp/index.js +0 -382
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* drivers/
|
|
2
|
+
* drivers/baileys/messageHandler.ts
|
|
3
3
|
*
|
|
4
4
|
* WhatsApp message pipeline.
|
|
5
5
|
* Moved from kernel/messageHandler.ts to keep all WhatsApp logic together.
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Each plugin decides whether to act or ignore.
|
|
13
13
|
*/
|
|
14
|
-
import { CHATS } from "#config";
|
|
15
|
-
import { buildApi, buildChatFromMsg } from "./api/index.js";
|
|
14
|
+
import { CHATS, EXCLUDE_CHATS } from "#config";
|
|
15
|
+
import { buildApi, buildChatFromMsg, buildMessageContext } from "./api/index.js";
|
|
16
16
|
import { pluginRegistry } from "#kernel/pluginLoader.js";
|
|
17
17
|
import { runPlugin } from "#kernel/pluginGuard.js";
|
|
18
|
-
import {
|
|
18
|
+
import { acquireChatSlot } from "#sendguard";
|
|
19
|
+
import { trackIncomingForContactSave } from "#kernel/contactAutoSave.js";
|
|
20
|
+
import { normalizeJid } from "#drivers/jid.js";
|
|
19
21
|
const INCOMING_DEBOUNCE_MS = 0;
|
|
20
22
|
const lastProcessedAt = new Map();
|
|
21
23
|
// ── Dedup of already-processed messages ────────────────────────────────────
|
|
@@ -40,21 +42,34 @@ function alreadyProcessed(id) {
|
|
|
40
42
|
return false;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {
|
|
45
|
-
* @param {
|
|
45
|
+
* @param {BotMessage} msg - driver-neutral incoming message envelope
|
|
46
|
+
* @param {WaContract} contract - driver-neutral contract (replaces WASocket)
|
|
47
|
+
* @param {BotStore} store - in-memory store
|
|
46
48
|
*/
|
|
47
|
-
export async function handleMessage(msg,
|
|
48
|
-
const rawJid = msg.
|
|
49
|
-
const jid = normalizeJid(rawJid);
|
|
49
|
+
export async function handleMessage(msg, contract, store) {
|
|
50
|
+
const rawJid = msg.chatId;
|
|
51
|
+
const jid = normalizeJid(store.resolveJid(rawJid));
|
|
50
52
|
if (CHATS.length > 0 && !CHATS.includes(jid)) {
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
|
-
if (
|
|
55
|
+
if (EXCLUDE_CHATS.includes(jid)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (alreadyProcessed(msg.id)) {
|
|
54
59
|
return;
|
|
55
60
|
}
|
|
56
61
|
// Mark as read/delivered to reduce the chance WhatsApp resends it
|
|
57
|
-
|
|
62
|
+
// (`msg.quotedKey`/`fromLid`/`fromPn` carry the LID/PN parts so the
|
|
63
|
+
// contract can reconstruct a proper key on each driver).
|
|
64
|
+
const rawKey = msg.id ? {
|
|
65
|
+
id: msg.id,
|
|
66
|
+
remoteJid: msg.chatId,
|
|
67
|
+
fromMe: false,
|
|
68
|
+
participant: msg.fromPn ?? msg.fromLid ?? undefined,
|
|
69
|
+
} : undefined;
|
|
70
|
+
if (rawKey) {
|
|
71
|
+
contract.readMessages([rawKey]).catch(() => { });
|
|
72
|
+
}
|
|
58
73
|
// Debounce rapid bursts per chat
|
|
59
74
|
if (INCOMING_DEBOUNCE_MS > 0) {
|
|
60
75
|
const now = Date.now();
|
|
@@ -67,12 +82,27 @@ export async function handleMessage(msg, sock, store) {
|
|
|
67
82
|
lastProcessedAt.set(jid, Date.now());
|
|
68
83
|
}
|
|
69
84
|
// Build a WAChat adapter from the message metadata
|
|
70
|
-
const chat = await buildChatFromMsg(msg, store,
|
|
85
|
+
const chat = await buildChatFromMsg(msg, store, contract);
|
|
86
|
+
// Gradual contact-saving (best-effort, never blocks message handling)
|
|
87
|
+
const msgCtx = buildMessageContext(msg, contract, store);
|
|
88
|
+
const isGroup = jid.endsWith("@g.us");
|
|
89
|
+
trackIncomingForContactSave(contract, msg, msgCtx.sender, isGroup, msgCtx.hasPrefix)
|
|
90
|
+
.catch(() => { });
|
|
91
|
+
// Caps how many chats get answered at the same time — see SECURITY_LEVEL.
|
|
92
|
+
const releaseChatSlot = await acquireChatSlot(jid);
|
|
93
|
+
try {
|
|
94
|
+
await runPluginsForMessage(msg, chat, contract, store, rawJid);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
releaseChatSlot();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function runPluginsForMessage(msg, chat, contract, store, rawJid) {
|
|
71
101
|
for (const plugin of pluginRegistry.values()) {
|
|
72
102
|
const ctx = buildApi({
|
|
73
103
|
msg,
|
|
74
104
|
chat,
|
|
75
|
-
|
|
105
|
+
contract,
|
|
76
106
|
store,
|
|
77
107
|
pluginRegistry,
|
|
78
108
|
pluginName: plugin.name,
|
|
@@ -83,7 +113,7 @@ export async function handleMessage(msg, sock, store) {
|
|
|
83
113
|
if (useTyping) {
|
|
84
114
|
// Refresh presence every 4s so WhatsApp doesn't auto-clear it
|
|
85
115
|
typingInterval = setInterval(() => {
|
|
86
|
-
|
|
116
|
+
contract.sendPresenceUpdate("composing", rawJid).catch(() => { });
|
|
87
117
|
}, 4000);
|
|
88
118
|
}
|
|
89
119
|
try {
|
|
@@ -92,7 +122,7 @@ export async function handleMessage(msg, sock, store) {
|
|
|
92
122
|
finally {
|
|
93
123
|
if (useTyping) {
|
|
94
124
|
clearInterval(typingInterval);
|
|
95
|
-
|
|
125
|
+
contract.sendPresenceUpdate("paused", rawJid).catch(() => { });
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
128
|
}
|
|
@@ -13,7 +13,6 @@ import { resolveLoginMethod } from "../loginPrompt.js";
|
|
|
13
13
|
import { logger } from "#logger";
|
|
14
14
|
import { t } from "#i18n";
|
|
15
15
|
import { createStore } from "#client/store.js";
|
|
16
|
-
import { CapabilitySet } from "#core/capabilities.js";
|
|
17
16
|
import pino from "pino";
|
|
18
17
|
// ── Auth path ─────────────────────────────────────────────────────────────────
|
|
19
18
|
// Baileys' sock.ev wraps an internal EventEmitter that no longer exposes
|
|
@@ -122,31 +121,3 @@ export async function createSocket(authDirName = CLIENT_ID) {
|
|
|
122
121
|
}
|
|
123
122
|
return { sock, store };
|
|
124
123
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Normalize a Baileys JID to the @c.us format used in ManyBot configs.
|
|
127
|
-
* Groups (@g.us) and broadcasts are passed through unchanged.
|
|
128
|
-
*
|
|
129
|
-
* @param {string} jid
|
|
130
|
-
* @returns {string}
|
|
131
|
-
*/
|
|
132
|
-
export function normalizeJid(jid) {
|
|
133
|
-
if (!jid)
|
|
134
|
-
return jid;
|
|
135
|
-
return jid
|
|
136
|
-
.replace(/@s\.whatsapp\.net$/, "@c.us")
|
|
137
|
-
.replace(/:\d+@/, "@");
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Minimal PresenceCapable view over a raw socket. Transitional shim used
|
|
141
|
-
* by kernel code that still works with a raw sock instead of a full
|
|
142
|
-
* PlatformAdapter — goes away once that code is migrated.
|
|
143
|
-
*
|
|
144
|
-
* @param {WASocket} sock
|
|
145
|
-
* @returns {PresenceCapable}
|
|
146
|
-
*/
|
|
147
|
-
export function toPresenceCapable(sock) {
|
|
148
|
-
return {
|
|
149
|
-
capabilities: new CapabilitySet(["presence"]),
|
|
150
|
-
setPresence: (chatId, state) => sock.sendPresenceUpdate(state, chatId),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Neutral JID utilities used by both Baileys and WhatsMeow drivers.
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a JID to the internal "@c.us" format used in ManyBot configs.
|
|
4
|
+
* Groups (@g.us) and broadcasts are passed through unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeJid(jid) {
|
|
7
|
+
if (!jid)
|
|
8
|
+
return jid;
|
|
9
|
+
return jid
|
|
10
|
+
.replace(/@s\.whatsapp\.net$/, "@c.us")
|
|
11
|
+
.replace(/:\d+@/, "@");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reverse of normalizeJid – convert back to the raw wire format "@s.whatsapp.net".
|
|
15
|
+
*/
|
|
16
|
+
export function denormalizeJid(jid) {
|
|
17
|
+
return jid.replace(/@c\.us$/, "@s.whatsapp.net");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convert any identifier (phone number, already‑wire JID, or framework JID) to the
|
|
21
|
+
* wire JID format WhatsApp expects ("...@s.whatsapp.net").
|
|
22
|
+
*/
|
|
23
|
+
export function toWireJid(id) {
|
|
24
|
+
const trimmed = id.trim();
|
|
25
|
+
if (/@(s\.whatsapp\.net|lid|g\.us)$/.test(trimmed))
|
|
26
|
+
return trimmed;
|
|
27
|
+
if (trimmed.endsWith("@c.us"))
|
|
28
|
+
return denormalizeJid(trimmed);
|
|
29
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
30
|
+
return `${digits}@s.whatsapp.net`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/drivers/types.ts
|
|
3
|
+
*
|
|
4
|
+
* Driver-neutral envelope types shared by every WhatsApp driver
|
|
5
|
+
* implementation (Baileys today, whatsmeow in a later phase). The
|
|
6
|
+
* full driver surface (send, react, presence, contacts, groups,
|
|
7
|
+
* profile, media) is declared as `WaContract` in `#kernel/waContract.js`
|
|
8
|
+
* — every driver implements it. This module holds only the message
|
|
9
|
+
* shapes that flow across the driver boundary.
|
|
10
|
+
*
|
|
11
|
+
* Plugins depend on these types through the `WaContract` re-exports,
|
|
12
|
+
* never on a specific driver's Baileys/grpc types.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { logger } from "#logger";
|
|
2
|
+
import { CONFIG } from "#config";
|
|
3
|
+
import * as grpc from "@grpc/grpc-js";
|
|
4
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
/**
|
|
8
|
+
* Whatsmeow gRPC client implementing the WaContract interface.
|
|
9
|
+
*
|
|
10
|
+
* Phase 1 scope: only the send path (sendText) and the
|
|
11
|
+
* verification primitives (getHistory) are fully wired. Every other
|
|
12
|
+
* WaContract method throws "not implemented" — the kernel loads plugins
|
|
13
|
+
* only after `connection.update === "open"`, and a plugin that calls e.g.
|
|
14
|
+
* `groupMetadata` on a whatsmeow-primary bot will surface a clear error
|
|
15
|
+
* to the caller, not a silent no-op.
|
|
16
|
+
*
|
|
17
|
+
* Connects to the address defined in config (default localhost:50051).
|
|
18
|
+
*/
|
|
19
|
+
class WhatsmeowClient {
|
|
20
|
+
name = "whatsmeow";
|
|
21
|
+
client; // grpc client stub
|
|
22
|
+
ready = false;
|
|
23
|
+
handlers = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the .proto path relative to this compiled module so it works
|
|
26
|
+
* in ESM (where __dirname doesn't exist), under `node dist/main.js`
|
|
27
|
+
* (proto is copied to dist/drivers/whatsmeow/whatsmeow.proto by the
|
|
28
|
+
* build), and under a global npm install (the proto ships alongside
|
|
29
|
+
* the JS in the package's `dist/` per `files` in package.json). In
|
|
30
|
+
* dev (`tsx src/main.ts`) the proto already lives next to this source
|
|
31
|
+
* file, so the same relative path resolves correctly there too.
|
|
32
|
+
*/
|
|
33
|
+
resolveProtoPath() {
|
|
34
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
return path.resolve(here, "whatsmeow.proto");
|
|
36
|
+
}
|
|
37
|
+
loadProto() {
|
|
38
|
+
const protoPath = this.resolveProtoPath();
|
|
39
|
+
const packageDef = protoLoader.loadSync(protoPath, {
|
|
40
|
+
keepCase: true,
|
|
41
|
+
longs: String,
|
|
42
|
+
enums: String,
|
|
43
|
+
defaults: true,
|
|
44
|
+
oneofs: true,
|
|
45
|
+
});
|
|
46
|
+
const grpcObj = grpc.loadPackageDefinition(packageDef);
|
|
47
|
+
return grpcObj.whatsmeow.WhatsmeowService;
|
|
48
|
+
}
|
|
49
|
+
async connect() {
|
|
50
|
+
const address = CONFIG.drivers.whatsmeow.grpcAddress ?? "localhost:50051";
|
|
51
|
+
const Service = this.loadProto();
|
|
52
|
+
this.client = new Service(address, grpc.credentials.createInsecure());
|
|
53
|
+
// Perform a health check to confirm service is ready
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
this.client.HealthCheck({}, (err, resp) => {
|
|
56
|
+
if (err)
|
|
57
|
+
return reject(err);
|
|
58
|
+
this.ready = !!resp?.ready;
|
|
59
|
+
if (this.ready)
|
|
60
|
+
resolve();
|
|
61
|
+
else
|
|
62
|
+
reject(new Error("Whatsmeow service not ready"));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
logger.info("[whatsmeow] connected via gRPC");
|
|
66
|
+
// Open the server-streaming event subscription. Each WaEvent that
|
|
67
|
+
// arrives is fanned out to the local on() subscribers in the
|
|
68
|
+
// neutral envelope shape.
|
|
69
|
+
const stream = this.client.SubscribeEvents({});
|
|
70
|
+
stream.on("data", (raw) => {
|
|
71
|
+
try {
|
|
72
|
+
if (raw.connState) {
|
|
73
|
+
const state = raw.connState.state ?? "connecting";
|
|
74
|
+
this.dispatch("connection.update", {
|
|
75
|
+
connection: state === "open" ? "open" : state === "close" ? "close" : "connecting",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (raw.message) {
|
|
79
|
+
this.dispatch("messages.upsert", {
|
|
80
|
+
messages: [raw.message],
|
|
81
|
+
type: "notify",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
logger.debug(`[whatsmeow] event dispatch failed: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
stream.on("error", (err) => {
|
|
90
|
+
logger.warn(`[whatsmeow] event stream error: ${err.message}`);
|
|
91
|
+
this.ready = false;
|
|
92
|
+
});
|
|
93
|
+
stream.on("end", () => {
|
|
94
|
+
logger.warn(`[whatsmeow] event stream ended`);
|
|
95
|
+
this.ready = false;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async disconnect() {
|
|
99
|
+
if (this.client) {
|
|
100
|
+
try {
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
this.client.Disconnect({}, () => resolve());
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch { }
|
|
106
|
+
this.client.close();
|
|
107
|
+
}
|
|
108
|
+
this.ready = false;
|
|
109
|
+
}
|
|
110
|
+
isReady() {
|
|
111
|
+
return this.ready;
|
|
112
|
+
}
|
|
113
|
+
// ── Event fan-out ─────────────────────────────────────────────────────────
|
|
114
|
+
on(event, handler) {
|
|
115
|
+
let set = this.handlers.get(event);
|
|
116
|
+
if (!set) {
|
|
117
|
+
set = new Set();
|
|
118
|
+
this.handlers.set(event, set);
|
|
119
|
+
}
|
|
120
|
+
set.add(handler);
|
|
121
|
+
return () => set.delete(handler);
|
|
122
|
+
}
|
|
123
|
+
dispatch(event, payload) {
|
|
124
|
+
const set = this.handlers.get(event);
|
|
125
|
+
if (!set)
|
|
126
|
+
return;
|
|
127
|
+
for (const h of set) {
|
|
128
|
+
try {
|
|
129
|
+
h(payload);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
logger.debug(`[whatsmeow] handler for "${event}" threw: ${e.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── Send ───────────────────────────────────────────────────────────────────
|
|
137
|
+
// Only sendText is in phase-1 scope. Media fallback is
|
|
138
|
+
// documented in the interface but explicitly deferred.
|
|
139
|
+
async sendText(jid, text, opts) {
|
|
140
|
+
const req = {
|
|
141
|
+
jid,
|
|
142
|
+
text,
|
|
143
|
+
quotedId: opts?.quoted?.id ?? "",
|
|
144
|
+
mentions: opts?.mentions ?? [],
|
|
145
|
+
};
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
this.client.SendText(req, (err, resp) => {
|
|
148
|
+
if (err)
|
|
149
|
+
return reject(err);
|
|
150
|
+
resolve({ id: resp.id, chatId: resp.chatId, timestamp: Number(resp.timestamp) });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// ── Verification primitive ─────────────────────────────────────────────────
|
|
155
|
+
async getHistory(jid, opts) {
|
|
156
|
+
const req = { jid, limit: opts?.limit ?? 5 };
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
this.client.GetHistory(req, (err, resp) => {
|
|
159
|
+
if (err)
|
|
160
|
+
return reject(err);
|
|
161
|
+
resolve(resp.messages ?? []);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// ── All other WaContract methods: stubbed for now ─────────────────────────
|
|
166
|
+
// These throw a clear error so a plugin calling them on a whatsmeow-
|
|
167
|
+
// primary bot fails loudly instead of silently no-op'ing. Coverage will
|
|
168
|
+
// grow in later phases as the whatsmeow .proto grows.
|
|
169
|
+
unimplemented(method) {
|
|
170
|
+
throw new Error(`[whatsmeow] ${method} not implemented in whatsmeow driver yet`);
|
|
171
|
+
}
|
|
172
|
+
async resolveLid(_lid) { return null; }
|
|
173
|
+
async sendImage(_jid, _buffer, _opts) { this.unimplemented("sendImage"); }
|
|
174
|
+
async sendVideo(_jid, _buffer, _opts) { this.unimplemented("sendVideo"); }
|
|
175
|
+
async sendAudio(_jid, _buffer, _opts) { this.unimplemented("sendAudio"); }
|
|
176
|
+
async sendSticker(_jid, _buffer, _opts) { this.unimplemented("sendSticker"); }
|
|
177
|
+
async sendDocument(_jid, _buffer, _filename, _mimetype, _opts) { this.unimplemented("sendDocument"); }
|
|
178
|
+
async sendPoll(_jid, _opts) { this.unimplemented("sendPoll"); }
|
|
179
|
+
async react(_jid, _target, _emoji) { this.unimplemented("react"); }
|
|
180
|
+
async deleteMessage(_jid, _target, _forEveryone) { this.unimplemented("deleteMessage"); }
|
|
181
|
+
async editMessage(_jid, _target, _text) { this.unimplemented("editMessage"); }
|
|
182
|
+
async sendPresenceUpdate(_state, _jid) { this.unimplemented("sendPresenceUpdate"); }
|
|
183
|
+
async readMessages(_keys) { this.unimplemented("readMessages"); }
|
|
184
|
+
async onWhatsApp(_jid) { this.unimplemented("onWhatsApp"); }
|
|
185
|
+
async getBusinessProfile(_jid) { this.unimplemented("getBusinessProfile"); }
|
|
186
|
+
async profilePictureUrl(_jid) { this.unimplemented("profilePictureUrl"); }
|
|
187
|
+
async fetchStatus(_jid) { this.unimplemented("fetchStatus"); }
|
|
188
|
+
async updateBlockStatus(_jid, _action) { this.unimplemented("updateBlockStatus"); }
|
|
189
|
+
async addOrEditContact(_jid, _info) { this.unimplemented("addOrEditContact"); }
|
|
190
|
+
async removeContact(_jid) { this.unimplemented("removeContact"); }
|
|
191
|
+
async groupMetadata(_jid) { this.unimplemented("groupMetadata"); }
|
|
192
|
+
async groupParticipantsUpdate(_jid, _users, _action) { this.unimplemented("groupParticipantsUpdate"); }
|
|
193
|
+
async groupUpdateSubject(_jid, _subject) { this.unimplemented("groupUpdateSubject"); }
|
|
194
|
+
async groupUpdateDescription(_jid, _description) { this.unimplemented("groupUpdateDescription"); }
|
|
195
|
+
async groupInviteCode(_jid) { this.unimplemented("groupInviteCode"); }
|
|
196
|
+
async groupRevokeInvite(_jid) { this.unimplemented("groupRevokeInvite"); }
|
|
197
|
+
async updateProfilePicture(_jid, _buffer) { this.unimplemented("updateProfilePicture"); }
|
|
198
|
+
async updateProfileName(_name) { this.unimplemented("updateProfileName"); }
|
|
199
|
+
async updateProfileStatus(_status) { this.unimplemented("updateProfileStatus"); }
|
|
200
|
+
me() { this.unimplemented("me"); }
|
|
201
|
+
async downloadMedia(_msg, _opts) { this.unimplemented("downloadMedia"); }
|
|
202
|
+
}
|
|
203
|
+
export const whatsmeowContract = new WhatsmeowClient();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drivers/whatsmeow/index.ts
|
|
3
|
+
*
|
|
4
|
+
* Public surface of the whatsmeow driver:
|
|
5
|
+
* - whatsmeowContract : the raw contract (test-only / fallback path)
|
|
6
|
+
* - wrapWithSupervisor(...) : returns a contract whose lifecycle
|
|
7
|
+
* methods (connect / disconnect / isReady)
|
|
8
|
+
* are gated on the supervisor state
|
|
9
|
+
*
|
|
10
|
+
* The supervisor is the lifecycle authority for the subprocess; this
|
|
11
|
+
* proxy exists so the DriverManager sees one contract whose `isReady()`
|
|
12
|
+
* never lies — true only when both the gRPC client is up AND the
|
|
13
|
+
* subprocess has answered HealthCheck{ready:true}.
|
|
14
|
+
*/
|
|
15
|
+
import { whatsmeowContract } from "./client.js";
|
|
16
|
+
export { whatsmeowContract };
|
|
17
|
+
export { startWhatsmeowSupervisor } from "./supervisor.js";
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a raw whatsmeow contract so its lifecycle methods delegate to
|
|
20
|
+
* the supervisor. Send/event methods still pass through unchanged —
|
|
21
|
+
* the contract already knows how to talk gRPC; the supervisor only
|
|
22
|
+
* owns "is it safe to use right now?".
|
|
23
|
+
*/
|
|
24
|
+
export function wrapWithSupervisor(contract, supervisor) {
|
|
25
|
+
return {
|
|
26
|
+
name: contract.name,
|
|
27
|
+
async connect() {
|
|
28
|
+
await supervisor.whenReady();
|
|
29
|
+
await contract.connect();
|
|
30
|
+
},
|
|
31
|
+
async disconnect() {
|
|
32
|
+
// Try to stop the subprocess too — disconnecting the contract
|
|
33
|
+
// alone would leave the Go process running until the bot shuts
|
|
34
|
+
// down. Idempotent; safe to call multiple times.
|
|
35
|
+
await Promise.allSettled([
|
|
36
|
+
contract.disconnect(),
|
|
37
|
+
supervisor.shutdown(),
|
|
38
|
+
]);
|
|
39
|
+
},
|
|
40
|
+
isReady: () => supervisor.isReady() && contract.isReady(),
|
|
41
|
+
on: (...args) => contract.on(...args),
|
|
42
|
+
resolveLid: contract.resolveLid
|
|
43
|
+
? (lid) => contract.resolveLid(lid)
|
|
44
|
+
: undefined,
|
|
45
|
+
sendText: (...args) => contract.sendText(...args),
|
|
46
|
+
sendImage: (...args) => contract.sendImage(...args),
|
|
47
|
+
sendVideo: (...args) => contract.sendVideo(...args),
|
|
48
|
+
sendAudio: (...args) => contract.sendAudio(...args),
|
|
49
|
+
sendSticker: (...args) => contract.sendSticker(...args),
|
|
50
|
+
sendDocument: (...args) => contract.sendDocument(...args),
|
|
51
|
+
sendPoll: (...args) => contract.sendPoll(...args),
|
|
52
|
+
react: (...args) => contract.react(...args),
|
|
53
|
+
deleteMessage: (...args) => contract.deleteMessage(...args),
|
|
54
|
+
editMessage: (...args) => contract.editMessage(...args),
|
|
55
|
+
sendPresenceUpdate: (...args) => contract.sendPresenceUpdate(...args),
|
|
56
|
+
readMessages: (...args) => contract.readMessages(...args),
|
|
57
|
+
onWhatsApp: (...args) => contract.onWhatsApp(...args),
|
|
58
|
+
getBusinessProfile: (...args) => contract.getBusinessProfile(...args),
|
|
59
|
+
profilePictureUrl: (...args) => contract.profilePictureUrl(...args),
|
|
60
|
+
fetchStatus: (...args) => contract.fetchStatus(...args),
|
|
61
|
+
updateBlockStatus: (...args) => contract.updateBlockStatus(...args),
|
|
62
|
+
addOrEditContact: (...args) => contract.addOrEditContact(...args),
|
|
63
|
+
removeContact: (...args) => contract.removeContact(...args),
|
|
64
|
+
groupMetadata: (...args) => contract.groupMetadata(...args),
|
|
65
|
+
groupParticipantsUpdate: (...args) => contract.groupParticipantsUpdate(...args),
|
|
66
|
+
groupUpdateSubject: (...args) => contract.groupUpdateSubject(...args),
|
|
67
|
+
groupUpdateDescription: (...args) => contract.groupUpdateDescription(...args),
|
|
68
|
+
groupInviteCode: (...args) => contract.groupInviteCode(...args),
|
|
69
|
+
groupRevokeInvite: (...args) => contract.groupRevokeInvite(...args),
|
|
70
|
+
updateProfilePicture: (...args) => contract.updateProfilePicture(...args),
|
|
71
|
+
updateProfileName: (...args) => contract.updateProfileName(...args),
|
|
72
|
+
updateProfileStatus: (...args) => contract.updateProfileStatus(...args),
|
|
73
|
+
me: () => contract.me(),
|
|
74
|
+
downloadMedia: (...args) => contract.downloadMedia(...args),
|
|
75
|
+
getHistory: contract.getHistory
|
|
76
|
+
? (...args) => contract.getHistory(...args)
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
4
|
+
import { persistConfigValue } from "#config";
|
|
5
|
+
import { t } from "#i18n";
|
|
6
|
+
const SUPPORTED = [
|
|
7
|
+
{ os: "linux", arch: "x64", name: "whatsmeow-service-linux-x64" },
|
|
8
|
+
{ os: "linux", arch: "arm64", name: "whatsmeow-service-linux-arm64" },
|
|
9
|
+
{ os: "win32", arch: "x64", name: "whatsmeow-service-windows-x64.exe" },
|
|
10
|
+
];
|
|
11
|
+
function detectTarget() {
|
|
12
|
+
return SUPPORTED.find((t) => t.os === process.platform && t.arch === process.arch) ?? null;
|
|
13
|
+
}
|
|
14
|
+
function str(val) {
|
|
15
|
+
return typeof val === "string" ? val : String(val);
|
|
16
|
+
}
|
|
17
|
+
async function fetchLatestTag() {
|
|
18
|
+
const res = await fetch("https://api.github.com/repos/many-bot/manybot/releases/latest");
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
throw new Error(`GitHub API: ${res.status}`);
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
return data.tag_name ?? "v5.6.1";
|
|
23
|
+
}
|
|
24
|
+
function binaryDir() {
|
|
25
|
+
return path.resolve(process.cwd(), "whatsmeow-service", "bin");
|
|
26
|
+
}
|
|
27
|
+
export async function promptWhatsmeowInstall() {
|
|
28
|
+
const target = detectTarget();
|
|
29
|
+
if (!target) {
|
|
30
|
+
clack.log.warn(str(t("whatsmeow.unsupportedArch", { os: process.platform, arch: process.arch })));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const choice = await clack.confirm({
|
|
34
|
+
message: str(t("whatsmeow.installPrompt")),
|
|
35
|
+
initialValue: false,
|
|
36
|
+
});
|
|
37
|
+
if (clack.isCancel(choice) || !choice)
|
|
38
|
+
return;
|
|
39
|
+
const spin = clack.spinner();
|
|
40
|
+
spin.start(str(t("whatsmeow.fetchingTag")));
|
|
41
|
+
let tag;
|
|
42
|
+
try {
|
|
43
|
+
tag = await fetchLatestTag();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
spin.stop(str(t("whatsmeow.fetchFailed")));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const url = `https://github.com/many-bot/manybot/releases/download/${tag}/${target.name}`;
|
|
50
|
+
spin.message(str(t("whatsmeow.downloading", { url })));
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(url);
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`${res.status}`);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
spin.stop(str(t("whatsmeow.downloadFailed", { reason: e.message })));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
62
|
+
const dir = binaryDir();
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
const outPath = path.join(dir, "whatsmeow-service");
|
|
65
|
+
writeFileSync(outPath, buffer);
|
|
66
|
+
chmodSync(outPath, 0o755);
|
|
67
|
+
await persistConfigValue("driver_whatsmeow_enabled", "true");
|
|
68
|
+
spin.stop(str(t("whatsmeow.installed", { path: outPath })));
|
|
69
|
+
clack.note(str(t("whatsmeow.restartNotice")), str(t("whatsmeow.installTitle")));
|
|
70
|
+
}
|