beast-agent 1.9.0 → 2.0.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/LICENSE +21 -21
- package/README.md +130 -130
- package/bin/beast-agent.js +137 -137
- package/package.json +1 -1
- package/scripts/fix-electron.js +138 -138
- package/scripts/release.js +137 -131
- package/scripts/swap-electron.js +40 -40
- package/src/agent/agentdefs.js +122 -122
- package/src/agent/bots.js +580 -580
- package/src/agent/bus.js +389 -389
- package/src/agent/computeruse.js +200 -200
- package/src/agent/config.js +284 -284
- package/src/agent/discord.js +332 -332
- package/src/agent/engine.js +75 -10
- package/src/agent/kb.js +123 -123
- package/src/agent/llm.js +430 -430
- package/src/agent/logger.js +90 -90
- package/src/agent/mcp.js +427 -427
- package/src/agent/mem0.js +605 -605
- package/src/agent/memory.js +427 -427
- package/src/agent/mqueue.js +124 -124
- package/src/agent/pdf.js +20 -20
- package/src/agent/research.js +133 -133
- package/src/agent/scripts/news.py +113 -113
- package/src/agent/scripts/stealthsearch.py +30 -30
- package/src/agent/scripts/websearch.py +225 -225
- package/src/agent/searxng.js +325 -325
- package/src/agent/seeds/brainstorming/SKILL.md +90 -90
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
- package/src/agent/seeds/executing-plans/SKILL.md +60 -60
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
- package/src/agent/seeds/writing-plans/SKILL.md +162 -162
- package/src/agent/seeds/writing-skills/SKILL.md +229 -229
- package/src/agent/skills.js +652 -652
- package/src/agent/store.js +378 -378
- package/src/agent/telegram.js +155 -155
- package/src/agent/tokens.js +39 -39
- package/src/agent/usage.js +125 -125
- package/src/agent/watchers.js +312 -312
- package/src/agent/watext.js +80 -80
- package/src/agent/whatsapp.js +555 -555
- package/src/cron.js +255 -255
- package/src/main.js +348 -6
- package/src/preload.js +9 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +448 -409
- package/src/renderer/renderer.js +824 -41
- package/src/renderer/style.css +264 -5
package/src/agent/whatsapp.js
CHANGED
|
@@ -1,555 +1,555 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* WhatsApp bridge — Baileys tabanlı. QR ile eşleme, auth %APPDATA%\beast\wa-auth,
|
|
4
|
-
gelen özel mesajlar onIncoming(jid, text)'e düşer, send(jid,text) cevap döner. */
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const QRCode = require('qrcode');
|
|
9
|
-
|
|
10
|
-
let baileys = null;
|
|
11
|
-
try {
|
|
12
|
-
baileys = require('@whiskeysockets/baileys');
|
|
13
|
-
} catch (e) {
|
|
14
|
-
baileys = null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* WAMessageStatus: 0=ERROR 1=PENDING 2=SERVER_ACK(gönderildi)
|
|
18
|
-
3=DELIVERY_ACK(teslim) 4=READ(okundu) 5=PLAYED(ses çalındı) */
|
|
19
|
-
const STATUS_LABELS = {
|
|
20
|
-
0: 'hata',
|
|
21
|
-
1: 'bekliyor',
|
|
22
|
-
2: 'gönderildi ✓',
|
|
23
|
-
3: 'teslim ✓✓',
|
|
24
|
-
4: 'okundu ✓✓',
|
|
25
|
-
5: 'çalındı ✓✓',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function statusLabel(status) {
|
|
29
|
-
return STATUS_LABELS[status] || `bilinmeyen(${status})`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/* Baileys kendi debug çıktılarını bastırmak için main enjekte etmezse no-op */
|
|
33
|
-
function waLogSafe(line) {
|
|
34
|
-
try { console.log(`[WA] ${line}`); } catch {}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const TRACK_CAP = 500;
|
|
38
|
-
|
|
39
|
-
/* lastKnownPresence etiketleri */
|
|
40
|
-
const PRESENCE_LABELS = {
|
|
41
|
-
available: 'çevrimiçi',
|
|
42
|
-
composing: 'yazıyor',
|
|
43
|
-
recording: 'ses kaydediyor',
|
|
44
|
-
paused: '',
|
|
45
|
-
unavailable: 'çevrimdışı',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
class WhatsAppBridge {
|
|
49
|
-
constructor({ authDir, emit, onIncoming, onReaction }) {
|
|
50
|
-
this.authDir = authDir;
|
|
51
|
-
this.emit = emit || (() => {});
|
|
52
|
-
this.onIncoming = onIncoming || null;
|
|
53
|
-
this.onReaction = onReaction || null;
|
|
54
|
-
this.sock = null;
|
|
55
|
-
this.connected = false;
|
|
56
|
-
this.user = null;
|
|
57
|
-
this.stopping = false;
|
|
58
|
-
this.reconnectTimer = null;
|
|
59
|
-
this._tracked = new Map(); // msgId -> { jid, preview, ts, status, receiptDetail }
|
|
60
|
-
this._watchJids = new Set(); // presence aboneliği tutulacak sohbetler
|
|
61
|
-
this._seenIds = new Set(); // işlenen mesaj id'leri — offline replay + notify çift işlemeyi önler
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* main tarafı waChats anahtarlarını bildirir; bağlantı varsa anında abone olunur */
|
|
65
|
-
setWatchJids(jids) {
|
|
66
|
-
this._watchJids = new Set(jids || []);
|
|
67
|
-
if (this.sock && this.connected) {
|
|
68
|
-
this._subscribePresence().catch(() => {});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async _subscribePresence() {
|
|
73
|
-
if (!this.sock || typeof this.sock.presenceSubscribe !== 'function') return;
|
|
74
|
-
for (const jid of this._watchJids) {
|
|
75
|
-
try {
|
|
76
|
-
await this.sock.presenceSubscribe(jid);
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* Baileys medya akışını buffer'a dök */
|
|
82
|
-
async _mediaBuffer(desc, type) {
|
|
83
|
-
if (!baileys || typeof baileys.downloadContentFromMessage !== 'function') {
|
|
84
|
-
throw new Error('medya indirme desteklenmiyor');
|
|
85
|
-
}
|
|
86
|
-
const stream = await baileys.downloadContentFromMessage(desc, type);
|
|
87
|
-
const chunks = [];
|
|
88
|
-
for await (const chunk of stream) chunks.push(chunk);
|
|
89
|
-
return Buffer.concat(chunks);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
get available() {
|
|
93
|
-
return !!baileys;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
snapshot() {
|
|
97
|
-
return {
|
|
98
|
-
status: this.connected ? 'connected' : this.sock ? 'connecting' : 'disconnected',
|
|
99
|
-
user: this.user,
|
|
100
|
-
available: this.available,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
_emitStatus(s) {
|
|
105
|
-
this.emit({ type: 'status', ...s });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async start() {
|
|
109
|
-
if (!baileys) throw new Error('Baileys kurulu değil: npm i @whiskeysockets/baileys qrcode');
|
|
110
|
-
if (this.sock) return;
|
|
111
|
-
this.stopping = false;
|
|
112
|
-
|
|
113
|
-
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = baileys;
|
|
114
|
-
fs.mkdirSync(this.authDir, { recursive: true });
|
|
115
|
-
|
|
116
|
-
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
|
|
117
|
-
let version;
|
|
118
|
-
try {
|
|
119
|
-
({ version } = await fetchLatestBaileysVersion());
|
|
120
|
-
} catch {
|
|
121
|
-
version = undefined;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
this.sock = makeWASocket({
|
|
125
|
-
version,
|
|
126
|
-
auth: state,
|
|
127
|
-
printQRInTerminal: false,
|
|
128
|
-
browser: ['Beast Agent', 'Chrome', '1.0.0'],
|
|
129
|
-
syncFullHistory: false,
|
|
130
|
-
/* false OLMALI: bot "çevrimiçi" işaretlenirse WhatsApp sunucusu kesintide
|
|
131
|
-
gelen mesajları bu cihaz için kuyruklamayı aksatıyor. false ile kesintideki
|
|
132
|
-
mesajlar sunucuda bekler, bağlantı dönünce 'append' upsert'iyle iletilir. */
|
|
133
|
-
markOnlineOnConnect: false,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
this.sock.ev.on('creds.update', saveCreds);
|
|
137
|
-
|
|
138
|
-
this.sock.ev.on('connection.update', async (u) => {
|
|
139
|
-
try {
|
|
140
|
-
if (u.qr) {
|
|
141
|
-
const dataUrl = await QRCode.toDataURL(u.qr, { margin: 1, width: 240 });
|
|
142
|
-
this.connected = false;
|
|
143
|
-
this._emitStatus({ status: 'qr', qr: dataUrl });
|
|
144
|
-
}
|
|
145
|
-
if (u.connection === 'open') {
|
|
146
|
-
this.connected = true;
|
|
147
|
-
const su = this.sock.user || {};
|
|
148
|
-
this.user = [su.name || su.verifiedName || '', su.id ? String(su.id).split(':')[0] : '']
|
|
149
|
-
.filter(Boolean)
|
|
150
|
-
.join(' · ');
|
|
151
|
-
/* mention/reply kontrolü için ham id (örn 1234:56@s.whatsapp.net) */
|
|
152
|
-
this._userIdRaw = String(su.id || '');
|
|
153
|
-
this._emitStatus({ status: 'connected', user: this.user });
|
|
154
|
-
this.sock.sendPresenceUpdate('available').catch(() => {});
|
|
155
|
-
this._subscribePresence().catch(() => {});
|
|
156
|
-
}
|
|
157
|
-
if (u.connection === 'close') {
|
|
158
|
-
const code = u.lastDisconnect && u.lastDisconnect.error &&
|
|
159
|
-
u.lastDisconnect.error.output && u.lastDisconnect.error.output.statusCode;
|
|
160
|
-
const loggedOut = code === DisconnectReason.loggedOut;
|
|
161
|
-
this.sock = null;
|
|
162
|
-
if (this.stopping) {
|
|
163
|
-
this.connected = false;
|
|
164
|
-
this._emitStatus({ status: 'disconnected' });
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (loggedOut) {
|
|
168
|
-
try {
|
|
169
|
-
fs.rmSync(this.authDir, { recursive: true, force: true });
|
|
170
|
-
} catch {}
|
|
171
|
-
this.connected = false;
|
|
172
|
-
this.user = null;
|
|
173
|
-
this._emitStatus({ status: 'logged-out' });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
this._emitStatus({ status: 'reconnecting' });
|
|
177
|
-
clearTimeout(this.reconnectTimer);
|
|
178
|
-
this.reconnectTimer = setTimeout(() => this.start().catch(() => {}), 3000);
|
|
179
|
-
}
|
|
180
|
-
} catch (e) {
|
|
181
|
-
this._emitStatus({ status: 'error', error: String((e && e.message) || e) });
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
this.sock.ev.on('messages.upsert', (m) => this._handleMessages(m));
|
|
186
|
-
|
|
187
|
-
/* Teslim/okundu takibi — sadece kendi gönderdiklerimiz izlenir */
|
|
188
|
-
this.sock.ev.on('messages.update', (ups) => this._handleStatusUpdates(ups));
|
|
189
|
-
this.sock.ev.on('message-receipt.update', (rs) => this._handleReceipts(rs));
|
|
190
|
-
|
|
191
|
-
/* Karşı tarafın çevrimiçi/yazıyor durumu — izlenen sohbetler için */
|
|
192
|
-
this.sock.ev.on('presence.update', (u) => this._handlePresence(u));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
_handlePresence(u) {
|
|
196
|
-
try {
|
|
197
|
-
if (!u || !u.id) return;
|
|
198
|
-
const presences = u.presences || {};
|
|
199
|
-
for (const [pid, p] of Object.entries(presences)) {
|
|
200
|
-
const st = p && p.lastKnownPresence;
|
|
201
|
-
if (!st || !(st in PRESENCE_LABELS)) continue;
|
|
202
|
-
this.emit({
|
|
203
|
-
type: 'presence',
|
|
204
|
-
jid: u.id,
|
|
205
|
-
participant: pid,
|
|
206
|
-
presence: st,
|
|
207
|
-
label: PRESENCE_LABELS[st],
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
} catch {}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
_trackOutgoing(jid, preview, ret) {
|
|
214
|
-
try {
|
|
215
|
-
const id = ret && ret.key && ret.key.id;
|
|
216
|
-
if (!id) return;
|
|
217
|
-
if (this._tracked.size >= TRACK_CAP) {
|
|
218
|
-
const first = this._tracked.keys().next().value;
|
|
219
|
-
this._tracked.delete(first);
|
|
220
|
-
}
|
|
221
|
-
this._tracked.set(id, { jid: String(jid || ''), preview: String(preview || '').slice(0, 40), ts: Date.now(), status: 1, receiptDetail: '' });
|
|
222
|
-
this.emit({ type: 'send', id, jid, preview: this._tracked.get(id).preview });
|
|
223
|
-
} catch {}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_handleStatusUpdates(ups) {
|
|
227
|
-
for (const up of ups || []) {
|
|
228
|
-
try {
|
|
229
|
-
/* bazı Baileys sürümleri tepkiyi update içinde taşır */
|
|
230
|
-
const rmUp = (up && up.update && (up.update.reactionMessage ||
|
|
231
|
-
(up.update.message && up.update.message.reactionMessage))) || null;
|
|
232
|
-
if (rmUp) {
|
|
233
|
-
this._handleReaction({ key: up.key }, rmUp);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const st = up && up.update && up.update.status;
|
|
237
|
-
const id = up && up.key && up.key.id;
|
|
238
|
-
if (typeof st !== 'number' || !id) continue;
|
|
239
|
-
const t = this._tracked.get(id);
|
|
240
|
-
if (!t || st === t.status) continue; // yalnızca bizim mesajlarımız + değişim varsa
|
|
241
|
-
t.status = st;
|
|
242
|
-
this.emit({ type: 'tick', id, jid: t.jid, status: st, label: statusLabel(st), preview: t.preview });
|
|
243
|
-
} catch {}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
_handleReceipts(rs) {
|
|
248
|
-
for (const r of rs || []) {
|
|
249
|
-
try {
|
|
250
|
-
const rc = r && r.receipt;
|
|
251
|
-
const id = r && r.key && r.key.id;
|
|
252
|
-
const t = id && this._tracked.get(id);
|
|
253
|
-
if (!t || !rc) continue;
|
|
254
|
-
const bits = [];
|
|
255
|
-
if (rc.deliveryTimestamp) bits.push(`teslim=${new Date(rc.deliveryTimestamp * 1000).toISOString()}`);
|
|
256
|
-
else if (rc.deliveredAt) bits.push(`teslim=${new Date(rc.deliveredAt).toISOString()}`);
|
|
257
|
-
if (rc.readTimestamp) bits.push(`okundu=${new Date(rc.readTimestamp * 1000).toISOString()}`);
|
|
258
|
-
else if (rc.readAt) bits.push(`okundu=${new Date(rc.readAt).toISOString()}`);
|
|
259
|
-
if (rc.playedTimestamp) bits.push(`çalındı=${new Date(rc.playedTimestamp * 1000).toISOString()}`);
|
|
260
|
-
else if (rc.playedAt) bits.push(`çalındı=${new Date(rc.playedAt).toISOString()}`);
|
|
261
|
-
const detail = bits.join(' ');
|
|
262
|
-
if (!detail || detail === t.receiptDetail) continue;
|
|
263
|
-
t.receiptDetail = detail;
|
|
264
|
-
this.emit({ type: 'receipt', id, jid: t.jid, preview: t.preview, detail });
|
|
265
|
-
} catch {}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
_handleMessages(m) {
|
|
270
|
-
/* 'notify' = canlı mesaj akışı
|
|
271
|
-
'append' = bağlantı kesintisinde KAÇIRILMIŞ mesajlar — bağlantı dönünce
|
|
272
|
-
sunucu bunları oynatır; FALLOUT sorunu tam olarak burasıydı:
|
|
273
|
-
yalnızca 'notify' işlendiği için kesintideki mesajlar sessizce
|
|
274
|
-
düşüyor ve cevapsız kalıyordu. 'append' içindeki eski history
|
|
275
|
-
kayıtlarını elemek için son 15 dk tazelik filtresi kullanılır. */
|
|
276
|
-
if (m.type !== 'notify' && m.type !== 'append') return;
|
|
277
|
-
const nowMs = Date.now();
|
|
278
|
-
for (const msg of m.messages || []) {
|
|
279
|
-
/* aynı mesaj hem 'append' hem 'notify' ile gelebilir — ID dedup */
|
|
280
|
-
const mid = msg.key && msg.key.id;
|
|
281
|
-
if (mid) {
|
|
282
|
-
if (this._seenIds.has(mid)) continue;
|
|
283
|
-
this._seenIds.add(mid);
|
|
284
|
-
if (this._seenIds.size > 600) {
|
|
285
|
-
for (const k of [...this._seenIds].slice(0, 300)) this._seenIds.delete(k);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
/* tepkiler ayrı kanal: onay kapısı bunları dinler */
|
|
289
|
-
const rm = msg.message && msg.message.reactionMessage;
|
|
290
|
-
if (rm) {
|
|
291
|
-
this._handleReaction(msg, rm);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (m.type === 'append') {
|
|
295
|
-
const ts = Number(msg.messageTimestamp) || 0;
|
|
296
|
-
const tsMs = ts > 1e12 ? ts : ts * 1000; // saniye/ms normalizasyonu
|
|
297
|
-
if (!tsMs || nowMs - tsMs > 15 * 60 * 1000) continue; // eski history — işleme
|
|
298
|
-
try {
|
|
299
|
-
waLogSafe(`offline/append mesaj işleniyor (kesintiden kalan, ${new Date(tsMs).toISOString()})`);
|
|
300
|
-
} catch {}
|
|
301
|
-
}
|
|
302
|
-
this._processIncoming(msg).catch(() => {});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
_handleReaction(msg, rm) {
|
|
307
|
-
try {
|
|
308
|
-
if (!rm || !rm.key || !rm.key.id) return;
|
|
309
|
-
if (!this.onReaction) {
|
|
310
|
-
this.emit({ type: 'reaction-unhandled', targetId: String(rm.key.id), reason: 'onReaction kanca yok' });
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
/* Baileys bazı sürümlerde tepkiyi kendi mesajı gibi işaretleyebiliyor;
|
|
314
|
-
bizim onay kartımıza gelen tepkilerden kendi attıklarımız zaten yoktur */
|
|
315
|
-
const jid = String(rm.key.remoteJid || msg.key.remoteJid || '');
|
|
316
|
-
if (!jid) return;
|
|
317
|
-
let senderNum = '';
|
|
318
|
-
const raw = msg.key.participant || msg.key.remoteJidAlt || jid;
|
|
319
|
-
const num = String(raw).split('@')[0].split(':')[0];
|
|
320
|
-
if (/^\d+$/.test(num)) senderNum = num;
|
|
321
|
-
const emoji = String(rm.text || '');
|
|
322
|
-
waLogSafe(`reaction event ← target=${String(rm.key.id)} emoji=${JSON.stringify(emoji)} chat=${jid} sender=+${senderNum}`);
|
|
323
|
-
this.emit({
|
|
324
|
-
type: 'reaction',
|
|
325
|
-
jid,
|
|
326
|
-
targetId: String(rm.key.id),
|
|
327
|
-
emoji,
|
|
328
|
-
sender: senderNum,
|
|
329
|
-
});
|
|
330
|
-
this.onReaction(jid, String(rm.key.id), emoji, senderNum);
|
|
331
|
-
} catch {}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/* Tek mesajı işle: metin + medya (resim/ses/belge) çıkarımı.
|
|
335
|
-
Grup sohbetleri (@g.us) de kabul edilir; payload.isGroup ile işaretlenir,
|
|
336
|
-
bot @mention edilmişse payload.mentioned=true gelir. */
|
|
337
|
-
async _processIncoming(msg) {
|
|
338
|
-
try {
|
|
339
|
-
const jid = msg.key && msg.key.remoteJid;
|
|
340
|
-
if (!jid) return;
|
|
341
|
-
if (jid === 'status@broadcast' || jid.endsWith('@newsletter')) return;
|
|
342
|
-
/* botun kendi kimliği: telefon base'i + LID base'i (LID dönemi) */
|
|
343
|
-
const meBase = String(this._userIdRaw || '').split('@')[0].split(':')[0];
|
|
344
|
-
const lidBase = String((this.sock && this.sock.user && this.sock.user.lid) || '').split('@')[0].split(':')[0];
|
|
345
|
-
/* KENDİ KENDİNE SOHBET (Saved Messages): kullanıcı kendi numarasını izinliye
|
|
346
|
-
ekleyip kendi sohbetinden bota yazabilsin. Bu sohbetteki HER mesaj fromMe
|
|
347
|
-
gelir; botun KENDİ gönderdiği mesajlar (_tracked) filtrelenir → döngü yok.
|
|
348
|
-
Diğer sohbetlerdeki elle yazılan fromMe mesajları bot etkilenmesin diye yutulur. */
|
|
349
|
-
const selfChat = !!(meBase && (jid === meBase + '@s.whatsapp.net' || (lidBase && jid === lidBase + '@lid')));
|
|
350
|
-
if (msg.key.fromMe) {
|
|
351
|
-
if (this._tracked.has(String(msg.key.id))) return; // botun kendi cevabı — echo
|
|
352
|
-
if (!selfChat) return;
|
|
353
|
-
}
|
|
354
|
-
const isGroup = jid.endsWith('@g.us');
|
|
355
|
-
const participantJid = isGroup && msg.key.participant ? String(msg.key.participant) : '';
|
|
356
|
-
const mm = msg.message || {};
|
|
357
|
-
const text =
|
|
358
|
-
mm.conversation ||
|
|
359
|
-
(mm.extendedTextMessage && mm.extendedTextMessage.text) ||
|
|
360
|
-
(mm.imageMessage && mm.imageMessage.caption) ||
|
|
361
|
-
(mm.documentMessage && mm.documentMessage.caption) ||
|
|
362
|
-
'';
|
|
363
|
-
const t = String(text || '').trim();
|
|
364
|
-
|
|
365
|
-
// gruplarda bot'un kendisi @mention edilmiş mi
|
|
366
|
-
/* LİD DÖNEMİ UYARISI: mentionedJid artık çoğu zaman botun @lid numarasıyla
|
|
367
|
-
gelir (telefon JID'i DEĞİL) — bu yüzden yalnız meBase ile karşılaştırma
|
|
368
|
-
mention'ı ıskalıyordu. Şimdi: botun telefon base'i + LID base'i ikisiyle
|
|
369
|
-
de eşitlik aranır; contextInfo görsel/video/belge caption'larından da alınır. */
|
|
370
|
-
let mentioned = false;
|
|
371
|
-
if (isGroup) {
|
|
372
|
-
const ci =
|
|
373
|
-
(mm.extendedTextMessage && mm.extendedTextMessage.contextInfo) ||
|
|
374
|
-
(mm.imageMessage && mm.imageMessage.contextInfo) ||
|
|
375
|
-
(mm.videoMessage && mm.videoMessage.contextInfo) ||
|
|
376
|
-
(mm.documentMessage && mm.documentMessage.contextInfo) ||
|
|
377
|
-
null;
|
|
378
|
-
const men = (ci && ci.mentionedJid) || [];
|
|
379
|
-
const baseOf = (j) => String(j || '').split('?')[0].split(':')[0].split('@')[0];
|
|
380
|
-
for (const m of men) {
|
|
381
|
-
const base = baseOf(m);
|
|
382
|
-
if (base && ((meBase && base === meBase) || (lidBase && base === lidBase))) {
|
|
383
|
-
mentioned = true;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (!mentioned && ci && ci.participant) {
|
|
388
|
-
const pBase = baseOf(ci.participant);
|
|
389
|
-
if ((meBase && pBase === meBase) || (lidBase && pBase === lidBase)) {
|
|
390
|
-
mentioned = true; // mesajımıza reply verilmiş
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
/* metin yedeği: @<bot numarası> elle yazılmışsa da mention sayılır */
|
|
394
|
-
if (!mentioned && meBase && t.includes('@' + meBase)) mentioned = true;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// medya çıkarımı (varsa)
|
|
398
|
-
let media = null;
|
|
399
|
-
const MAX_MEDIA = 20 * 1024 * 1024;
|
|
400
|
-
try {
|
|
401
|
-
if (mm.imageMessage) {
|
|
402
|
-
const buf = await this._mediaBuffer(mm.imageMessage, 'image');
|
|
403
|
-
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
404
|
-
media = { kind: 'image', buf, mimetype: mm.imageMessage.mimetype || 'image/jpeg', name: 'gorsel.jpg' };
|
|
405
|
-
}
|
|
406
|
-
} else if (mm.audioMessage) {
|
|
407
|
-
const buf = await this._mediaBuffer(mm.audioMessage, 'audio');
|
|
408
|
-
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
409
|
-
media = { kind: 'audio', buf, mimetype: mm.audioMessage.mimetype || 'audio/ogg; codecs=opus' };
|
|
410
|
-
}
|
|
411
|
-
} else if (mm.documentMessage) {
|
|
412
|
-
const buf = await this._mediaBuffer(mm.documentMessage, 'document');
|
|
413
|
-
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
414
|
-
media = { kind: 'document', buf, mimetype: mm.documentMessage.mimetype || 'application/octet-stream', name: mm.documentMessage.fileName || 'dosya' };
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
} catch {}
|
|
418
|
-
|
|
419
|
-
if (!t && !media) return;
|
|
420
|
-
// Yeni WA LID sistemi: gerçek numara remoteJidAlt'ta geliyor
|
|
421
|
-
const alt =
|
|
422
|
-
msg.key.remoteJidAlt ||
|
|
423
|
-
(msg.message && msg.message.extendedTextMessage && msg.message.extendedTextMessage.remoteJidAlt) ||
|
|
424
|
-
'';
|
|
425
|
-
let senderNum = '';
|
|
426
|
-
const pnSrc = String(alt || '').startsWith('alt:') ? String(alt).slice(4) : String(alt || '');
|
|
427
|
-
if (/^\d+@s\.whatsapp\.net$/.test(pnSrc)) {
|
|
428
|
-
senderNum = pnSrc.split('@')[0];
|
|
429
|
-
} else if (/^\d+@(s\.whatsapp\.net)?$/.test(jid)) {
|
|
430
|
-
senderNum = jid.split('@')[0].split(':')[0];
|
|
431
|
-
}
|
|
432
|
-
if (!senderNum) {
|
|
433
|
-
senderNum = (participantJid || jid).split('@')[0].split(':')[0];
|
|
434
|
-
if (!/^\d+$/.test(senderNum)) senderNum = '';
|
|
435
|
-
}
|
|
436
|
-
/* kendi kendine sohbette gönderen = botun telefon numarası — izin listesi
|
|
437
|
-
telefon numarasıyla eşleşsin (LID self-chat'te jid @lid olur) */
|
|
438
|
-
if (selfChat && meBase) senderNum = meBase;
|
|
439
|
-
this.onIncoming(jid, {
|
|
440
|
-
text: t,
|
|
441
|
-
media,
|
|
442
|
-
isGroup,
|
|
443
|
-
participant: participantJid,
|
|
444
|
-
/* LID çağında gerçek telefon: participantAlt; varsa WA kullanıcı adı */
|
|
445
|
-
participantPn: isGroup ? String(msg.key.participantAlt || '') : '',
|
|
446
|
-
participantUsername: isGroup ? String(msg.key.participantUsername || '') : '',
|
|
447
|
-
mentioned,
|
|
448
|
-
}, senderNum);
|
|
449
|
-
} catch {}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/* Cevap: başarı → { id }, başarısızlık → false (hata emit edilir, yutulmaz) */
|
|
453
|
-
async send(jid, text) {
|
|
454
|
-
if (!this.sock || !this.connected) return false;
|
|
455
|
-
const txt = String(text).slice(0, 3500);
|
|
456
|
-
let ret;
|
|
457
|
-
try {
|
|
458
|
-
ret = await this.sock.sendMessage(jid, { text: txt });
|
|
459
|
-
} catch (e) {
|
|
460
|
-
this.emit({
|
|
461
|
-
type: 'send-error',
|
|
462
|
-
jid,
|
|
463
|
-
preview: txt.slice(0, 40),
|
|
464
|
-
error: String((e && e.message) || e),
|
|
465
|
-
});
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
this._trackOutgoing(jid, txt, ret);
|
|
469
|
-
const id = ret && ret.key && ret.key.id ? String(ret.key.id) : '';
|
|
470
|
-
return id ? { id } : true;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer) */
|
|
474
|
-
async sendAudio(jid, audioBuf) {
|
|
475
|
-
if (!this.sock || !this.connected || !audioBuf) return false;
|
|
476
|
-
try {
|
|
477
|
-
const ret = await this.sock.sendMessage(jid, { audio: audioBuf, ptt: true, mimetype: 'audio/mpeg' });
|
|
478
|
-
this._trackOutgoing(jid, '[sesli yanıt]', ret);
|
|
479
|
-
return true;
|
|
480
|
-
} catch (e) {
|
|
481
|
-
this.emit({ type: 'send-error', jid, preview: '[ses]', error: String((e && e.message) || e) });
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/* Görsel gönder (jpeg/png buffer) — /screenshot vb. için */
|
|
487
|
-
async sendImage(jid, imgBuf, caption) {
|
|
488
|
-
if (!this.sock || !this.connected || !imgBuf) return false;
|
|
489
|
-
try {
|
|
490
|
-
const ret = await this.sock.sendMessage(jid, {
|
|
491
|
-
image: imgBuf,
|
|
492
|
-
caption: String(caption || '').slice(0, 800),
|
|
493
|
-
});
|
|
494
|
-
this._trackOutgoing(jid, '[görsel]', ret);
|
|
495
|
-
return true;
|
|
496
|
-
} catch (e) {
|
|
497
|
-
this.emit({ type: 'send-error', jid, preview: '[görsel]', error: String((e && e.message) || e) });
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/* Belge gönder (pdf/doc/xxx buffer) — ajanın send_file aracı için */
|
|
503
|
-
async sendFile(jid, buf, fileName, caption, mimetype) {
|
|
504
|
-
if (!this.sock || !this.connected || !buf) return false;
|
|
505
|
-
try {
|
|
506
|
-
const ret = await this.sock.sendMessage(jid, {
|
|
507
|
-
document: buf,
|
|
508
|
-
fileName: String(fileName || 'dosya'),
|
|
509
|
-
mimetype: mimetype || 'application/octet-stream',
|
|
510
|
-
caption: String(caption || '').slice(0, 800),
|
|
511
|
-
});
|
|
512
|
-
this._trackOutgoing(jid, '[dosya] ' + fileName, ret);
|
|
513
|
-
return true;
|
|
514
|
-
} catch (e) {
|
|
515
|
-
this.emit({ type: 'send-error', jid, preview: '[dosya]', error: String((e && e.message) || e) });
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/* Karşı chatta durum bildirimi: on=true → "yazıyor…", on=false → durdu */
|
|
521
|
-
async setComposing(jid, on) {
|
|
522
|
-
if (!this.sock || !this.connected) return;
|
|
523
|
-
try {
|
|
524
|
-
await this.sock.sendPresenceUpdate(on ? 'composing' : 'paused', jid);
|
|
525
|
-
} catch {}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async stop() {
|
|
529
|
-
this.stopping = true;
|
|
530
|
-
clearTimeout(this.reconnectTimer);
|
|
531
|
-
const s = this.sock;
|
|
532
|
-
this.sock = null;
|
|
533
|
-
this.connected = false;
|
|
534
|
-
this.user = null;
|
|
535
|
-
if (s) {
|
|
536
|
-
try {
|
|
537
|
-
await s.logout();
|
|
538
|
-
} catch {
|
|
539
|
-
try {
|
|
540
|
-
s.end();
|
|
541
|
-
} catch {}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
this._emitStatus({ status: 'disconnected' });
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async resetAuth() {
|
|
548
|
-
await this.stop();
|
|
549
|
-
try {
|
|
550
|
-
fs.rmSync(this.authDir, { recursive: true, force: true });
|
|
551
|
-
} catch {}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
module.exports = { WhatsAppBridge, statusLabel };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* WhatsApp bridge — Baileys tabanlı. QR ile eşleme, auth %APPDATA%\beast\wa-auth,
|
|
4
|
+
gelen özel mesajlar onIncoming(jid, text)'e düşer, send(jid,text) cevap döner. */
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const QRCode = require('qrcode');
|
|
9
|
+
|
|
10
|
+
let baileys = null;
|
|
11
|
+
try {
|
|
12
|
+
baileys = require('@whiskeysockets/baileys');
|
|
13
|
+
} catch (e) {
|
|
14
|
+
baileys = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* WAMessageStatus: 0=ERROR 1=PENDING 2=SERVER_ACK(gönderildi)
|
|
18
|
+
3=DELIVERY_ACK(teslim) 4=READ(okundu) 5=PLAYED(ses çalındı) */
|
|
19
|
+
const STATUS_LABELS = {
|
|
20
|
+
0: 'hata',
|
|
21
|
+
1: 'bekliyor',
|
|
22
|
+
2: 'gönderildi ✓',
|
|
23
|
+
3: 'teslim ✓✓',
|
|
24
|
+
4: 'okundu ✓✓',
|
|
25
|
+
5: 'çalındı ✓✓',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function statusLabel(status) {
|
|
29
|
+
return STATUS_LABELS[status] || `bilinmeyen(${status})`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Baileys kendi debug çıktılarını bastırmak için main enjekte etmezse no-op */
|
|
33
|
+
function waLogSafe(line) {
|
|
34
|
+
try { console.log(`[WA] ${line}`); } catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TRACK_CAP = 500;
|
|
38
|
+
|
|
39
|
+
/* lastKnownPresence etiketleri */
|
|
40
|
+
const PRESENCE_LABELS = {
|
|
41
|
+
available: 'çevrimiçi',
|
|
42
|
+
composing: 'yazıyor',
|
|
43
|
+
recording: 'ses kaydediyor',
|
|
44
|
+
paused: '',
|
|
45
|
+
unavailable: 'çevrimdışı',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
class WhatsAppBridge {
|
|
49
|
+
constructor({ authDir, emit, onIncoming, onReaction }) {
|
|
50
|
+
this.authDir = authDir;
|
|
51
|
+
this.emit = emit || (() => {});
|
|
52
|
+
this.onIncoming = onIncoming || null;
|
|
53
|
+
this.onReaction = onReaction || null;
|
|
54
|
+
this.sock = null;
|
|
55
|
+
this.connected = false;
|
|
56
|
+
this.user = null;
|
|
57
|
+
this.stopping = false;
|
|
58
|
+
this.reconnectTimer = null;
|
|
59
|
+
this._tracked = new Map(); // msgId -> { jid, preview, ts, status, receiptDetail }
|
|
60
|
+
this._watchJids = new Set(); // presence aboneliği tutulacak sohbetler
|
|
61
|
+
this._seenIds = new Set(); // işlenen mesaj id'leri — offline replay + notify çift işlemeyi önler
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* main tarafı waChats anahtarlarını bildirir; bağlantı varsa anında abone olunur */
|
|
65
|
+
setWatchJids(jids) {
|
|
66
|
+
this._watchJids = new Set(jids || []);
|
|
67
|
+
if (this.sock && this.connected) {
|
|
68
|
+
this._subscribePresence().catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _subscribePresence() {
|
|
73
|
+
if (!this.sock || typeof this.sock.presenceSubscribe !== 'function') return;
|
|
74
|
+
for (const jid of this._watchJids) {
|
|
75
|
+
try {
|
|
76
|
+
await this.sock.presenceSubscribe(jid);
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Baileys medya akışını buffer'a dök */
|
|
82
|
+
async _mediaBuffer(desc, type) {
|
|
83
|
+
if (!baileys || typeof baileys.downloadContentFromMessage !== 'function') {
|
|
84
|
+
throw new Error('medya indirme desteklenmiyor');
|
|
85
|
+
}
|
|
86
|
+
const stream = await baileys.downloadContentFromMessage(desc, type);
|
|
87
|
+
const chunks = [];
|
|
88
|
+
for await (const chunk of stream) chunks.push(chunk);
|
|
89
|
+
return Buffer.concat(chunks);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get available() {
|
|
93
|
+
return !!baileys;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
snapshot() {
|
|
97
|
+
return {
|
|
98
|
+
status: this.connected ? 'connected' : this.sock ? 'connecting' : 'disconnected',
|
|
99
|
+
user: this.user,
|
|
100
|
+
available: this.available,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_emitStatus(s) {
|
|
105
|
+
this.emit({ type: 'status', ...s });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async start() {
|
|
109
|
+
if (!baileys) throw new Error('Baileys kurulu değil: npm i @whiskeysockets/baileys qrcode');
|
|
110
|
+
if (this.sock) return;
|
|
111
|
+
this.stopping = false;
|
|
112
|
+
|
|
113
|
+
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = baileys;
|
|
114
|
+
fs.mkdirSync(this.authDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
|
|
117
|
+
let version;
|
|
118
|
+
try {
|
|
119
|
+
({ version } = await fetchLatestBaileysVersion());
|
|
120
|
+
} catch {
|
|
121
|
+
version = undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.sock = makeWASocket({
|
|
125
|
+
version,
|
|
126
|
+
auth: state,
|
|
127
|
+
printQRInTerminal: false,
|
|
128
|
+
browser: ['Beast Agent', 'Chrome', '1.0.0'],
|
|
129
|
+
syncFullHistory: false,
|
|
130
|
+
/* false OLMALI: bot "çevrimiçi" işaretlenirse WhatsApp sunucusu kesintide
|
|
131
|
+
gelen mesajları bu cihaz için kuyruklamayı aksatıyor. false ile kesintideki
|
|
132
|
+
mesajlar sunucuda bekler, bağlantı dönünce 'append' upsert'iyle iletilir. */
|
|
133
|
+
markOnlineOnConnect: false,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
137
|
+
|
|
138
|
+
this.sock.ev.on('connection.update', async (u) => {
|
|
139
|
+
try {
|
|
140
|
+
if (u.qr) {
|
|
141
|
+
const dataUrl = await QRCode.toDataURL(u.qr, { margin: 1, width: 240 });
|
|
142
|
+
this.connected = false;
|
|
143
|
+
this._emitStatus({ status: 'qr', qr: dataUrl });
|
|
144
|
+
}
|
|
145
|
+
if (u.connection === 'open') {
|
|
146
|
+
this.connected = true;
|
|
147
|
+
const su = this.sock.user || {};
|
|
148
|
+
this.user = [su.name || su.verifiedName || '', su.id ? String(su.id).split(':')[0] : '']
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join(' · ');
|
|
151
|
+
/* mention/reply kontrolü için ham id (örn 1234:56@s.whatsapp.net) */
|
|
152
|
+
this._userIdRaw = String(su.id || '');
|
|
153
|
+
this._emitStatus({ status: 'connected', user: this.user });
|
|
154
|
+
this.sock.sendPresenceUpdate('available').catch(() => {});
|
|
155
|
+
this._subscribePresence().catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
if (u.connection === 'close') {
|
|
158
|
+
const code = u.lastDisconnect && u.lastDisconnect.error &&
|
|
159
|
+
u.lastDisconnect.error.output && u.lastDisconnect.error.output.statusCode;
|
|
160
|
+
const loggedOut = code === DisconnectReason.loggedOut;
|
|
161
|
+
this.sock = null;
|
|
162
|
+
if (this.stopping) {
|
|
163
|
+
this.connected = false;
|
|
164
|
+
this._emitStatus({ status: 'disconnected' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (loggedOut) {
|
|
168
|
+
try {
|
|
169
|
+
fs.rmSync(this.authDir, { recursive: true, force: true });
|
|
170
|
+
} catch {}
|
|
171
|
+
this.connected = false;
|
|
172
|
+
this.user = null;
|
|
173
|
+
this._emitStatus({ status: 'logged-out' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this._emitStatus({ status: 'reconnecting' });
|
|
177
|
+
clearTimeout(this.reconnectTimer);
|
|
178
|
+
this.reconnectTimer = setTimeout(() => this.start().catch(() => {}), 3000);
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
this._emitStatus({ status: 'error', error: String((e && e.message) || e) });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.sock.ev.on('messages.upsert', (m) => this._handleMessages(m));
|
|
186
|
+
|
|
187
|
+
/* Teslim/okundu takibi — sadece kendi gönderdiklerimiz izlenir */
|
|
188
|
+
this.sock.ev.on('messages.update', (ups) => this._handleStatusUpdates(ups));
|
|
189
|
+
this.sock.ev.on('message-receipt.update', (rs) => this._handleReceipts(rs));
|
|
190
|
+
|
|
191
|
+
/* Karşı tarafın çevrimiçi/yazıyor durumu — izlenen sohbetler için */
|
|
192
|
+
this.sock.ev.on('presence.update', (u) => this._handlePresence(u));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_handlePresence(u) {
|
|
196
|
+
try {
|
|
197
|
+
if (!u || !u.id) return;
|
|
198
|
+
const presences = u.presences || {};
|
|
199
|
+
for (const [pid, p] of Object.entries(presences)) {
|
|
200
|
+
const st = p && p.lastKnownPresence;
|
|
201
|
+
if (!st || !(st in PRESENCE_LABELS)) continue;
|
|
202
|
+
this.emit({
|
|
203
|
+
type: 'presence',
|
|
204
|
+
jid: u.id,
|
|
205
|
+
participant: pid,
|
|
206
|
+
presence: st,
|
|
207
|
+
label: PRESENCE_LABELS[st],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_trackOutgoing(jid, preview, ret) {
|
|
214
|
+
try {
|
|
215
|
+
const id = ret && ret.key && ret.key.id;
|
|
216
|
+
if (!id) return;
|
|
217
|
+
if (this._tracked.size >= TRACK_CAP) {
|
|
218
|
+
const first = this._tracked.keys().next().value;
|
|
219
|
+
this._tracked.delete(first);
|
|
220
|
+
}
|
|
221
|
+
this._tracked.set(id, { jid: String(jid || ''), preview: String(preview || '').slice(0, 40), ts: Date.now(), status: 1, receiptDetail: '' });
|
|
222
|
+
this.emit({ type: 'send', id, jid, preview: this._tracked.get(id).preview });
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_handleStatusUpdates(ups) {
|
|
227
|
+
for (const up of ups || []) {
|
|
228
|
+
try {
|
|
229
|
+
/* bazı Baileys sürümleri tepkiyi update içinde taşır */
|
|
230
|
+
const rmUp = (up && up.update && (up.update.reactionMessage ||
|
|
231
|
+
(up.update.message && up.update.message.reactionMessage))) || null;
|
|
232
|
+
if (rmUp) {
|
|
233
|
+
this._handleReaction({ key: up.key }, rmUp);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const st = up && up.update && up.update.status;
|
|
237
|
+
const id = up && up.key && up.key.id;
|
|
238
|
+
if (typeof st !== 'number' || !id) continue;
|
|
239
|
+
const t = this._tracked.get(id);
|
|
240
|
+
if (!t || st === t.status) continue; // yalnızca bizim mesajlarımız + değişim varsa
|
|
241
|
+
t.status = st;
|
|
242
|
+
this.emit({ type: 'tick', id, jid: t.jid, status: st, label: statusLabel(st), preview: t.preview });
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_handleReceipts(rs) {
|
|
248
|
+
for (const r of rs || []) {
|
|
249
|
+
try {
|
|
250
|
+
const rc = r && r.receipt;
|
|
251
|
+
const id = r && r.key && r.key.id;
|
|
252
|
+
const t = id && this._tracked.get(id);
|
|
253
|
+
if (!t || !rc) continue;
|
|
254
|
+
const bits = [];
|
|
255
|
+
if (rc.deliveryTimestamp) bits.push(`teslim=${new Date(rc.deliveryTimestamp * 1000).toISOString()}`);
|
|
256
|
+
else if (rc.deliveredAt) bits.push(`teslim=${new Date(rc.deliveredAt).toISOString()}`);
|
|
257
|
+
if (rc.readTimestamp) bits.push(`okundu=${new Date(rc.readTimestamp * 1000).toISOString()}`);
|
|
258
|
+
else if (rc.readAt) bits.push(`okundu=${new Date(rc.readAt).toISOString()}`);
|
|
259
|
+
if (rc.playedTimestamp) bits.push(`çalındı=${new Date(rc.playedTimestamp * 1000).toISOString()}`);
|
|
260
|
+
else if (rc.playedAt) bits.push(`çalındı=${new Date(rc.playedAt).toISOString()}`);
|
|
261
|
+
const detail = bits.join(' ');
|
|
262
|
+
if (!detail || detail === t.receiptDetail) continue;
|
|
263
|
+
t.receiptDetail = detail;
|
|
264
|
+
this.emit({ type: 'receipt', id, jid: t.jid, preview: t.preview, detail });
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_handleMessages(m) {
|
|
270
|
+
/* 'notify' = canlı mesaj akışı
|
|
271
|
+
'append' = bağlantı kesintisinde KAÇIRILMIŞ mesajlar — bağlantı dönünce
|
|
272
|
+
sunucu bunları oynatır; FALLOUT sorunu tam olarak burasıydı:
|
|
273
|
+
yalnızca 'notify' işlendiği için kesintideki mesajlar sessizce
|
|
274
|
+
düşüyor ve cevapsız kalıyordu. 'append' içindeki eski history
|
|
275
|
+
kayıtlarını elemek için son 15 dk tazelik filtresi kullanılır. */
|
|
276
|
+
if (m.type !== 'notify' && m.type !== 'append') return;
|
|
277
|
+
const nowMs = Date.now();
|
|
278
|
+
for (const msg of m.messages || []) {
|
|
279
|
+
/* aynı mesaj hem 'append' hem 'notify' ile gelebilir — ID dedup */
|
|
280
|
+
const mid = msg.key && msg.key.id;
|
|
281
|
+
if (mid) {
|
|
282
|
+
if (this._seenIds.has(mid)) continue;
|
|
283
|
+
this._seenIds.add(mid);
|
|
284
|
+
if (this._seenIds.size > 600) {
|
|
285
|
+
for (const k of [...this._seenIds].slice(0, 300)) this._seenIds.delete(k);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/* tepkiler ayrı kanal: onay kapısı bunları dinler */
|
|
289
|
+
const rm = msg.message && msg.message.reactionMessage;
|
|
290
|
+
if (rm) {
|
|
291
|
+
this._handleReaction(msg, rm);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (m.type === 'append') {
|
|
295
|
+
const ts = Number(msg.messageTimestamp) || 0;
|
|
296
|
+
const tsMs = ts > 1e12 ? ts : ts * 1000; // saniye/ms normalizasyonu
|
|
297
|
+
if (!tsMs || nowMs - tsMs > 15 * 60 * 1000) continue; // eski history — işleme
|
|
298
|
+
try {
|
|
299
|
+
waLogSafe(`offline/append mesaj işleniyor (kesintiden kalan, ${new Date(tsMs).toISOString()})`);
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
this._processIncoming(msg).catch(() => {});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_handleReaction(msg, rm) {
|
|
307
|
+
try {
|
|
308
|
+
if (!rm || !rm.key || !rm.key.id) return;
|
|
309
|
+
if (!this.onReaction) {
|
|
310
|
+
this.emit({ type: 'reaction-unhandled', targetId: String(rm.key.id), reason: 'onReaction kanca yok' });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
/* Baileys bazı sürümlerde tepkiyi kendi mesajı gibi işaretleyebiliyor;
|
|
314
|
+
bizim onay kartımıza gelen tepkilerden kendi attıklarımız zaten yoktur */
|
|
315
|
+
const jid = String(rm.key.remoteJid || msg.key.remoteJid || '');
|
|
316
|
+
if (!jid) return;
|
|
317
|
+
let senderNum = '';
|
|
318
|
+
const raw = msg.key.participant || msg.key.remoteJidAlt || jid;
|
|
319
|
+
const num = String(raw).split('@')[0].split(':')[0];
|
|
320
|
+
if (/^\d+$/.test(num)) senderNum = num;
|
|
321
|
+
const emoji = String(rm.text || '');
|
|
322
|
+
waLogSafe(`reaction event ← target=${String(rm.key.id)} emoji=${JSON.stringify(emoji)} chat=${jid} sender=+${senderNum}`);
|
|
323
|
+
this.emit({
|
|
324
|
+
type: 'reaction',
|
|
325
|
+
jid,
|
|
326
|
+
targetId: String(rm.key.id),
|
|
327
|
+
emoji,
|
|
328
|
+
sender: senderNum,
|
|
329
|
+
});
|
|
330
|
+
this.onReaction(jid, String(rm.key.id), emoji, senderNum);
|
|
331
|
+
} catch {}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Tek mesajı işle: metin + medya (resim/ses/belge) çıkarımı.
|
|
335
|
+
Grup sohbetleri (@g.us) de kabul edilir; payload.isGroup ile işaretlenir,
|
|
336
|
+
bot @mention edilmişse payload.mentioned=true gelir. */
|
|
337
|
+
async _processIncoming(msg) {
|
|
338
|
+
try {
|
|
339
|
+
const jid = msg.key && msg.key.remoteJid;
|
|
340
|
+
if (!jid) return;
|
|
341
|
+
if (jid === 'status@broadcast' || jid.endsWith('@newsletter')) return;
|
|
342
|
+
/* botun kendi kimliği: telefon base'i + LID base'i (LID dönemi) */
|
|
343
|
+
const meBase = String(this._userIdRaw || '').split('@')[0].split(':')[0];
|
|
344
|
+
const lidBase = String((this.sock && this.sock.user && this.sock.user.lid) || '').split('@')[0].split(':')[0];
|
|
345
|
+
/* KENDİ KENDİNE SOHBET (Saved Messages): kullanıcı kendi numarasını izinliye
|
|
346
|
+
ekleyip kendi sohbetinden bota yazabilsin. Bu sohbetteki HER mesaj fromMe
|
|
347
|
+
gelir; botun KENDİ gönderdiği mesajlar (_tracked) filtrelenir → döngü yok.
|
|
348
|
+
Diğer sohbetlerdeki elle yazılan fromMe mesajları bot etkilenmesin diye yutulur. */
|
|
349
|
+
const selfChat = !!(meBase && (jid === meBase + '@s.whatsapp.net' || (lidBase && jid === lidBase + '@lid')));
|
|
350
|
+
if (msg.key.fromMe) {
|
|
351
|
+
if (this._tracked.has(String(msg.key.id))) return; // botun kendi cevabı — echo
|
|
352
|
+
if (!selfChat) return;
|
|
353
|
+
}
|
|
354
|
+
const isGroup = jid.endsWith('@g.us');
|
|
355
|
+
const participantJid = isGroup && msg.key.participant ? String(msg.key.participant) : '';
|
|
356
|
+
const mm = msg.message || {};
|
|
357
|
+
const text =
|
|
358
|
+
mm.conversation ||
|
|
359
|
+
(mm.extendedTextMessage && mm.extendedTextMessage.text) ||
|
|
360
|
+
(mm.imageMessage && mm.imageMessage.caption) ||
|
|
361
|
+
(mm.documentMessage && mm.documentMessage.caption) ||
|
|
362
|
+
'';
|
|
363
|
+
const t = String(text || '').trim();
|
|
364
|
+
|
|
365
|
+
// gruplarda bot'un kendisi @mention edilmiş mi
|
|
366
|
+
/* LİD DÖNEMİ UYARISI: mentionedJid artık çoğu zaman botun @lid numarasıyla
|
|
367
|
+
gelir (telefon JID'i DEĞİL) — bu yüzden yalnız meBase ile karşılaştırma
|
|
368
|
+
mention'ı ıskalıyordu. Şimdi: botun telefon base'i + LID base'i ikisiyle
|
|
369
|
+
de eşitlik aranır; contextInfo görsel/video/belge caption'larından da alınır. */
|
|
370
|
+
let mentioned = false;
|
|
371
|
+
if (isGroup) {
|
|
372
|
+
const ci =
|
|
373
|
+
(mm.extendedTextMessage && mm.extendedTextMessage.contextInfo) ||
|
|
374
|
+
(mm.imageMessage && mm.imageMessage.contextInfo) ||
|
|
375
|
+
(mm.videoMessage && mm.videoMessage.contextInfo) ||
|
|
376
|
+
(mm.documentMessage && mm.documentMessage.contextInfo) ||
|
|
377
|
+
null;
|
|
378
|
+
const men = (ci && ci.mentionedJid) || [];
|
|
379
|
+
const baseOf = (j) => String(j || '').split('?')[0].split(':')[0].split('@')[0];
|
|
380
|
+
for (const m of men) {
|
|
381
|
+
const base = baseOf(m);
|
|
382
|
+
if (base && ((meBase && base === meBase) || (lidBase && base === lidBase))) {
|
|
383
|
+
mentioned = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!mentioned && ci && ci.participant) {
|
|
388
|
+
const pBase = baseOf(ci.participant);
|
|
389
|
+
if ((meBase && pBase === meBase) || (lidBase && pBase === lidBase)) {
|
|
390
|
+
mentioned = true; // mesajımıza reply verilmiş
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/* metin yedeği: @<bot numarası> elle yazılmışsa da mention sayılır */
|
|
394
|
+
if (!mentioned && meBase && t.includes('@' + meBase)) mentioned = true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// medya çıkarımı (varsa)
|
|
398
|
+
let media = null;
|
|
399
|
+
const MAX_MEDIA = 20 * 1024 * 1024;
|
|
400
|
+
try {
|
|
401
|
+
if (mm.imageMessage) {
|
|
402
|
+
const buf = await this._mediaBuffer(mm.imageMessage, 'image');
|
|
403
|
+
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
404
|
+
media = { kind: 'image', buf, mimetype: mm.imageMessage.mimetype || 'image/jpeg', name: 'gorsel.jpg' };
|
|
405
|
+
}
|
|
406
|
+
} else if (mm.audioMessage) {
|
|
407
|
+
const buf = await this._mediaBuffer(mm.audioMessage, 'audio');
|
|
408
|
+
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
409
|
+
media = { kind: 'audio', buf, mimetype: mm.audioMessage.mimetype || 'audio/ogg; codecs=opus' };
|
|
410
|
+
}
|
|
411
|
+
} else if (mm.documentMessage) {
|
|
412
|
+
const buf = await this._mediaBuffer(mm.documentMessage, 'document');
|
|
413
|
+
if (buf && buf.length && buf.length <= MAX_MEDIA) {
|
|
414
|
+
media = { kind: 'document', buf, mimetype: mm.documentMessage.mimetype || 'application/octet-stream', name: mm.documentMessage.fileName || 'dosya' };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {}
|
|
418
|
+
|
|
419
|
+
if (!t && !media) return;
|
|
420
|
+
// Yeni WA LID sistemi: gerçek numara remoteJidAlt'ta geliyor
|
|
421
|
+
const alt =
|
|
422
|
+
msg.key.remoteJidAlt ||
|
|
423
|
+
(msg.message && msg.message.extendedTextMessage && msg.message.extendedTextMessage.remoteJidAlt) ||
|
|
424
|
+
'';
|
|
425
|
+
let senderNum = '';
|
|
426
|
+
const pnSrc = String(alt || '').startsWith('alt:') ? String(alt).slice(4) : String(alt || '');
|
|
427
|
+
if (/^\d+@s\.whatsapp\.net$/.test(pnSrc)) {
|
|
428
|
+
senderNum = pnSrc.split('@')[0];
|
|
429
|
+
} else if (/^\d+@(s\.whatsapp\.net)?$/.test(jid)) {
|
|
430
|
+
senderNum = jid.split('@')[0].split(':')[0];
|
|
431
|
+
}
|
|
432
|
+
if (!senderNum) {
|
|
433
|
+
senderNum = (participantJid || jid).split('@')[0].split(':')[0];
|
|
434
|
+
if (!/^\d+$/.test(senderNum)) senderNum = '';
|
|
435
|
+
}
|
|
436
|
+
/* kendi kendine sohbette gönderen = botun telefon numarası — izin listesi
|
|
437
|
+
telefon numarasıyla eşleşsin (LID self-chat'te jid @lid olur) */
|
|
438
|
+
if (selfChat && meBase) senderNum = meBase;
|
|
439
|
+
this.onIncoming(jid, {
|
|
440
|
+
text: t,
|
|
441
|
+
media,
|
|
442
|
+
isGroup,
|
|
443
|
+
participant: participantJid,
|
|
444
|
+
/* LID çağında gerçek telefon: participantAlt; varsa WA kullanıcı adı */
|
|
445
|
+
participantPn: isGroup ? String(msg.key.participantAlt || '') : '',
|
|
446
|
+
participantUsername: isGroup ? String(msg.key.participantUsername || '') : '',
|
|
447
|
+
mentioned,
|
|
448
|
+
}, senderNum);
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* Cevap: başarı → { id }, başarısızlık → false (hata emit edilir, yutulmaz) */
|
|
453
|
+
async send(jid, text) {
|
|
454
|
+
if (!this.sock || !this.connected) return false;
|
|
455
|
+
const txt = String(text).slice(0, 3500);
|
|
456
|
+
let ret;
|
|
457
|
+
try {
|
|
458
|
+
ret = await this.sock.sendMessage(jid, { text: txt });
|
|
459
|
+
} catch (e) {
|
|
460
|
+
this.emit({
|
|
461
|
+
type: 'send-error',
|
|
462
|
+
jid,
|
|
463
|
+
preview: txt.slice(0, 40),
|
|
464
|
+
error: String((e && e.message) || e),
|
|
465
|
+
});
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
this._trackOutgoing(jid, txt, ret);
|
|
469
|
+
const id = ret && ret.key && ret.key.id ? String(ret.key.id) : '';
|
|
470
|
+
return id ? { id } : true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer) */
|
|
474
|
+
async sendAudio(jid, audioBuf) {
|
|
475
|
+
if (!this.sock || !this.connected || !audioBuf) return false;
|
|
476
|
+
try {
|
|
477
|
+
const ret = await this.sock.sendMessage(jid, { audio: audioBuf, ptt: true, mimetype: 'audio/mpeg' });
|
|
478
|
+
this._trackOutgoing(jid, '[sesli yanıt]', ret);
|
|
479
|
+
return true;
|
|
480
|
+
} catch (e) {
|
|
481
|
+
this.emit({ type: 'send-error', jid, preview: '[ses]', error: String((e && e.message) || e) });
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* Görsel gönder (jpeg/png buffer) — /screenshot vb. için */
|
|
487
|
+
async sendImage(jid, imgBuf, caption) {
|
|
488
|
+
if (!this.sock || !this.connected || !imgBuf) return false;
|
|
489
|
+
try {
|
|
490
|
+
const ret = await this.sock.sendMessage(jid, {
|
|
491
|
+
image: imgBuf,
|
|
492
|
+
caption: String(caption || '').slice(0, 800),
|
|
493
|
+
});
|
|
494
|
+
this._trackOutgoing(jid, '[görsel]', ret);
|
|
495
|
+
return true;
|
|
496
|
+
} catch (e) {
|
|
497
|
+
this.emit({ type: 'send-error', jid, preview: '[görsel]', error: String((e && e.message) || e) });
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* Belge gönder (pdf/doc/xxx buffer) — ajanın send_file aracı için */
|
|
503
|
+
async sendFile(jid, buf, fileName, caption, mimetype) {
|
|
504
|
+
if (!this.sock || !this.connected || !buf) return false;
|
|
505
|
+
try {
|
|
506
|
+
const ret = await this.sock.sendMessage(jid, {
|
|
507
|
+
document: buf,
|
|
508
|
+
fileName: String(fileName || 'dosya'),
|
|
509
|
+
mimetype: mimetype || 'application/octet-stream',
|
|
510
|
+
caption: String(caption || '').slice(0, 800),
|
|
511
|
+
});
|
|
512
|
+
this._trackOutgoing(jid, '[dosya] ' + fileName, ret);
|
|
513
|
+
return true;
|
|
514
|
+
} catch (e) {
|
|
515
|
+
this.emit({ type: 'send-error', jid, preview: '[dosya]', error: String((e && e.message) || e) });
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Karşı chatta durum bildirimi: on=true → "yazıyor…", on=false → durdu */
|
|
521
|
+
async setComposing(jid, on) {
|
|
522
|
+
if (!this.sock || !this.connected) return;
|
|
523
|
+
try {
|
|
524
|
+
await this.sock.sendPresenceUpdate(on ? 'composing' : 'paused', jid);
|
|
525
|
+
} catch {}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async stop() {
|
|
529
|
+
this.stopping = true;
|
|
530
|
+
clearTimeout(this.reconnectTimer);
|
|
531
|
+
const s = this.sock;
|
|
532
|
+
this.sock = null;
|
|
533
|
+
this.connected = false;
|
|
534
|
+
this.user = null;
|
|
535
|
+
if (s) {
|
|
536
|
+
try {
|
|
537
|
+
await s.logout();
|
|
538
|
+
} catch {
|
|
539
|
+
try {
|
|
540
|
+
s.end();
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
this._emitStatus({ status: 'disconnected' });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async resetAuth() {
|
|
548
|
+
await this.stop();
|
|
549
|
+
try {
|
|
550
|
+
fs.rmSync(this.authDir, { recursive: true, force: true });
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
module.exports = { WhatsAppBridge, statusLabel };
|