beast-agent 1.8.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.
Files changed (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +130 -128
  3. package/bin/beast-agent.js +137 -137
  4. package/package.json +1 -1
  5. package/scripts/fix-electron.js +138 -138
  6. package/scripts/release.js +137 -131
  7. package/scripts/swap-electron.js +40 -40
  8. package/src/agent/agentdefs.js +122 -122
  9. package/src/agent/bots.js +580 -580
  10. package/src/agent/bus.js +389 -389
  11. package/src/agent/computeruse.js +200 -200
  12. package/src/agent/config.js +284 -284
  13. package/src/agent/discord.js +332 -332
  14. package/src/agent/engine.js +85 -11
  15. package/src/agent/kb.js +123 -123
  16. package/src/agent/llm.js +430 -430
  17. package/src/agent/logger.js +90 -90
  18. package/src/agent/mcp.js +427 -0
  19. package/src/agent/mem0.js +605 -605
  20. package/src/agent/memory.js +427 -427
  21. package/src/agent/mqueue.js +124 -124
  22. package/src/agent/pdf.js +20 -20
  23. package/src/agent/research.js +133 -133
  24. package/src/agent/scripts/news.py +113 -113
  25. package/src/agent/scripts/stealthsearch.py +30 -30
  26. package/src/agent/scripts/websearch.py +225 -225
  27. package/src/agent/searxng.js +325 -325
  28. package/src/agent/seeds/brainstorming/SKILL.md +90 -90
  29. package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
  30. package/src/agent/seeds/executing-plans/SKILL.md +60 -60
  31. package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
  32. package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
  33. package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
  34. package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
  35. package/src/agent/seeds/writing-plans/SKILL.md +162 -162
  36. package/src/agent/seeds/writing-skills/SKILL.md +229 -229
  37. package/src/agent/skills.js +652 -651
  38. package/src/agent/store.js +378 -378
  39. package/src/agent/telegram.js +155 -155
  40. package/src/agent/tokens.js +39 -39
  41. package/src/agent/usage.js +125 -125
  42. package/src/agent/watchers.js +312 -312
  43. package/src/agent/watext.js +80 -80
  44. package/src/agent/whatsapp.js +555 -555
  45. package/src/cron.js +255 -255
  46. package/src/main.js +385 -6
  47. package/src/preload.js +13 -0
  48. package/src/renderer/browserPreload.js +73 -73
  49. package/src/renderer/i18n.js +38 -0
  50. package/src/renderer/index.html +448 -407
  51. package/src/renderer/renderer.js +897 -42
  52. package/src/renderer/style.css +275 -11
@@ -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 };