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.
- package/LICENSE +21 -21
- package/README.md +130 -128
- 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 +85 -11
- 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 -0
- 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 -651
- 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 +385 -6
- package/src/preload.js +13 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +38 -0
- package/src/renderer/index.html +448 -407
- package/src/renderer/renderer.js +897 -42
- package/src/renderer/style.css +275 -11
package/src/agent/discord.js
CHANGED
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* DISCORD KÖPRÜSÜ — Telegram köprüsünün Discord hali.
|
|
4
|
-
Bağımlılık: yalnızca `ws` (paket zaten kurulu — baileys ile geliyor).
|
|
5
|
-
- Gateway WebSocket: Identify → READY → MESSAGE_CREATE dispatch
|
|
6
|
-
- REST (api/v10): mesaj gönderme (2000 karakter sınırında bölerek)
|
|
7
|
-
- Sunucu (guild) mesajlarında yalnız @mention'a cevap verilir (spam koruması);
|
|
8
|
-
DM'de her izinli kullanıcıya cevap verilir.
|
|
9
|
-
- Bağlantı koparsa backoff ile yeniden bağlanır (yeniden Identify;
|
|
10
|
-
çevrimdışıyken gelen mesajlar backfill edilmez — Telegram long-poll'un aksine).
|
|
11
|
-
ÖNEMLİ: Discord Developer Portal'da bot için "MESSAGE CONTENT INTENT"
|
|
12
|
-
açılmalıdır — yoksa mesaj içerikleri boş gelir. */
|
|
13
|
-
|
|
14
|
-
const WebSocket = require('ws');
|
|
15
|
-
const https = require('https');
|
|
16
|
-
|
|
17
|
-
const API_BASE = 'https://discord.com/api/v10';
|
|
18
|
-
const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
19
|
-
const SEND_CHUNK = 1900; // Discord mesaj sınırı 2000 — güvenli pay
|
|
20
|
-
/* GUILDS(1) | GUILD_MESSAGES(512) | DIRECT_MESSAGES(4096) | MESSAGE_CONTENT(32768) */
|
|
21
|
-
const INTENTS = 1 | 512 | 4096 | 32768;
|
|
22
|
-
|
|
23
|
-
/* AĞ ARA YAZILIMI (antivirüs SSL taraması / proxy / ağ filtresi) Discord
|
|
24
|
-
trafiğine araya girip SELF-SIGNED sertifika sunarsa Node varsayılan olarak
|
|
25
|
-
bağlantıyı reddeder ("self signed certificate") ve bot hiç bağlanamaz.
|
|
26
|
-
Çözüm iki katman:
|
|
27
|
-
1) REST istekleri ÖNCE Electron/Chromium ağ yığınından (net.fetch) geçer —
|
|
28
|
-
sistem CA + sistem proxy otomatik tanınır (tarayıcının çalıştığı yol).
|
|
29
|
-
2) Zorlanırsa Node https'e düşülür; self-signed tespit edilirse bu köprü
|
|
30
|
-
için TLS doğrulaması esnetilir — YALNIZ Discord bağlantıları etkilenir. */
|
|
31
|
-
let tlsRelaxed = false;
|
|
32
|
-
const CERT_ERR_RE = /self[ -]?signed|unable to verify the first certificate|depth zero|err_tls|certificate has expired|cert_has_expired|unable_to_get_issuer|self signed certificate/i;
|
|
33
|
-
|
|
34
|
-
function isCertError(e) {
|
|
35
|
-
return CERT_ERR_RE.test(String((e && (e.message || e.code)) || e || ''));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Electron main process'te Chromium ağ yığını (sistem CA + proxy); düz Node
|
|
39
|
-
(testler) için require('electron') path string döndürür → net tanımsız kalır */
|
|
40
|
-
let _electronNet = null;
|
|
41
|
-
try {
|
|
42
|
-
const el = require('electron');
|
|
43
|
-
if (el && el.net && typeof el.net.fetch === 'function') _electronNet = el.net;
|
|
44
|
-
} catch {}
|
|
45
|
-
|
|
46
|
-
function bodySnippet(data) {
|
|
47
|
-
const s = String(data || '').replace(/\s+/g, ' ').trim();
|
|
48
|
-
return s ? ` — gövde: ${s.slice(0, 140)}` : '';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* Ağ engel sayfası tespiti: Discord API her zaman JSON döner — text/html veya
|
|
52
|
-
erişim-engelle sayfası gelirse ağ seviyesinde blok var demektir (Türkiye'de
|
|
53
|
-
Discord erişimi zaman zaman engellenir). Kullanıcıya net VPN yönlendirmesi. */
|
|
54
|
-
const VPN_HINT = 'Discord bu ağdan ERİŞİME ENGELLİ görünüyor — VPN açıp tekrar dene (engel sayfası döndü)';
|
|
55
|
-
|
|
56
|
-
function looksBlocked(ctype, data) {
|
|
57
|
-
const ct = String(ctype || '');
|
|
58
|
-
const body = String(data || '').slice(0, 4000);
|
|
59
|
-
return (
|
|
60
|
-
ct.includes('text/html') ||
|
|
61
|
-
/erisime[_ ]?engelle|erişime engelle|blocked|block_page/i.test(body)
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
class DiscordBridge {
|
|
66
|
-
constructor({ token, emit, onIncoming }) {
|
|
67
|
-
this.token = String(token || '').trim();
|
|
68
|
-
this.emit = emit || (() => {});
|
|
69
|
-
this.onIncoming = onIncoming || null;
|
|
70
|
-
this.connected = false;
|
|
71
|
-
this.stopping = false;
|
|
72
|
-
this.status = 'disconnected';
|
|
73
|
-
this.user = null; // { id, username, ... }
|
|
74
|
-
this._ws = null;
|
|
75
|
-
this._hbTimer = null;
|
|
76
|
-
this._seq = null;
|
|
77
|
-
this._backoff = 2000;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_setStatus(status, user) {
|
|
81
|
-
this.status = status;
|
|
82
|
-
this.connected = status === 'connected';
|
|
83
|
-
this.emit({ type: 'status', status, user: user || null });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/* REST çağrısı — JSON (token: "Bot <token>").
|
|
87
|
-
Yol 1: Chromium (net.fetch — sistem CA/proxy). Yol 2: Node https (+esnek TLS). */
|
|
88
|
-
api(method, path, body, attempt = 0) {
|
|
89
|
-
return this._api(method, path, body).catch((e) => {
|
|
90
|
-
if (isCertError(e) && !tlsRelaxed) {
|
|
91
|
-
tlsRelaxed = true;
|
|
92
|
-
this.emit({
|
|
93
|
-
type: 'warn',
|
|
94
|
-
text: 'ağ ara yazılımı (antivirüs/proxy) self-signed sertifikası tespit edildi — Discord bağlantısı esnek TLS ile devam ediyor',
|
|
95
|
-
});
|
|
96
|
-
if (attempt < 1) return this.api(method, path, body, attempt + 1);
|
|
97
|
-
}
|
|
98
|
-
throw e;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async _api(method, path, body) {
|
|
103
|
-
const payload = body ? JSON.stringify(body) : null;
|
|
104
|
-
const headers = { 'Content-Type': 'application/json', Authorization: 'Bot ' + this.token };
|
|
105
|
-
const url = API_BASE + path;
|
|
106
|
-
|
|
107
|
-
/* YOL 1 — Chromium ağ yığını (varsa): sistem sertifikaları + sistem proxy */
|
|
108
|
-
if (_electronNet) {
|
|
109
|
-
try {
|
|
110
|
-
const res = await _electronNet.fetch(url, {
|
|
111
|
-
method,
|
|
112
|
-
headers,
|
|
113
|
-
...(payload ? { body: payload } : {}),
|
|
114
|
-
});
|
|
115
|
-
const data = await res.text();
|
|
116
|
-
let j = null;
|
|
117
|
-
try { j = data ? JSON.parse(data) : null; } catch {}
|
|
118
|
-
if (res.ok && j !== null) return j;
|
|
119
|
-
if (j === null && looksBlocked(res.headers.get('content-type'), data)) {
|
|
120
|
-
throw new Error(VPN_HINT + bodySnippet(data));
|
|
121
|
-
}
|
|
122
|
-
throw new Error(
|
|
123
|
-
`discord ${path}: HTTP ${res.status}${j && j.message ? ' ' + j.message : ''}${bodySnippet(data)}`
|
|
124
|
-
);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
/* kendi formatladığımız API hatasıysa yukarı taşı; transport hatasıysa
|
|
127
|
-
Node https yoluna düş (orada esnek TLS şansı var) */
|
|
128
|
-
if (/^discord |Discord bu ağ/.test(String((e && e.message) || ''))) throw e;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/* YOL 2 — Node https (+ gerektiğinde esnek TLS) */
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
const req = https.request(
|
|
135
|
-
url,
|
|
136
|
-
{
|
|
137
|
-
method,
|
|
138
|
-
headers: {
|
|
139
|
-
...headers,
|
|
140
|
-
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
141
|
-
},
|
|
142
|
-
timeout: 15000,
|
|
143
|
-
rejectUnauthorized: !tlsRelaxed,
|
|
144
|
-
},
|
|
145
|
-
(res) => {
|
|
146
|
-
let data = '';
|
|
147
|
-
res.setEncoding('utf8');
|
|
148
|
-
res.on('data', (c) => (data += c));
|
|
149
|
-
res.on('end', () => {
|
|
150
|
-
const ctype = String(res.headers['content-type'] || '');
|
|
151
|
-
let j = null;
|
|
152
|
-
try { j = data ? JSON.parse(data) : null; } catch {}
|
|
153
|
-
if (res.statusCode >= 200 && res.statusCode < 300 && j !== null) return resolve(j);
|
|
154
|
-
if (looksBlocked(ctype, data)) {
|
|
155
|
-
return reject(new Error(VPN_HINT + bodySnippet(data)));
|
|
156
|
-
}
|
|
157
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
158
|
-
return reject(new Error(`discord ${path}: bozuk yanıt (HTTP ${res.statusCode}${ctype ? ', ' + ctype : ''})${bodySnippet(data)}`));
|
|
159
|
-
}
|
|
160
|
-
reject(new Error(`discord ${path}: HTTP ${res.statusCode} ${(j && j.message) || ''}`.trim() + bodySnippet(data)));
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
);
|
|
164
|
-
req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
|
|
165
|
-
req.on('error', reject);
|
|
166
|
-
if (payload) req.write(payload);
|
|
167
|
-
req.end();
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async start() {
|
|
172
|
-
if (!this.token) {
|
|
173
|
-
this._setStatus('error');
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
this.stopping = false;
|
|
177
|
-
this._setStatus('connecting');
|
|
178
|
-
try {
|
|
179
|
-
const me = await this.api('GET', '/users/@me');
|
|
180
|
-
this.user = me; // { id, username, ... }
|
|
181
|
-
this._setStatus('connected', '@' + (me.username || 'bot'));
|
|
182
|
-
} catch (e) {
|
|
183
|
-
this._setStatus('error');
|
|
184
|
-
throw e;
|
|
185
|
-
}
|
|
186
|
-
this._connect();
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
_connect() {
|
|
191
|
-
if (this.stopping) return;
|
|
192
|
-
clearInterval(this._hbTimer);
|
|
193
|
-
this._hbTimer = null;
|
|
194
|
-
let identified = false;
|
|
195
|
-
const ws = new WebSocket(GATEWAY_URL, { rejectUnauthorized: !tlsRelaxed });
|
|
196
|
-
this._ws = ws;
|
|
197
|
-
|
|
198
|
-
ws.on('message', (raw) => {
|
|
199
|
-
let p = null;
|
|
200
|
-
try { p = JSON.parse(String(raw)); } catch { return; }
|
|
201
|
-
const op = p.op;
|
|
202
|
-
const d = p.d;
|
|
203
|
-
const t = p.t;
|
|
204
|
-
if (typeof p.s === 'number') this._seq = p.s;
|
|
205
|
-
|
|
206
|
-
if (op === 10) {
|
|
207
|
-
/* Hello: heartbeat aralığı gelıyor → hafif erken at (güvenli pay) */
|
|
208
|
-
const iv = (d && d.heartbeat_interval) || 41250;
|
|
209
|
-
clearInterval(this._hbTimer);
|
|
210
|
-
this._hbTimer = setInterval(() => {
|
|
211
|
-
try {
|
|
212
|
-
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
213
|
-
} catch {}
|
|
214
|
-
}, Math.max(5000, Math.floor(iv * 0.8)));
|
|
215
|
-
if (!identified) {
|
|
216
|
-
identified = true;
|
|
217
|
-
try {
|
|
218
|
-
ws.send(
|
|
219
|
-
JSON.stringify({
|
|
220
|
-
op: 2,
|
|
221
|
-
d: {
|
|
222
|
-
token: this.token,
|
|
223
|
-
intents: INTENTS,
|
|
224
|
-
properties: { os: 'windows', browser: 'beast-agent', device: 'beast-agent' },
|
|
225
|
-
},
|
|
226
|
-
})
|
|
227
|
-
);
|
|
228
|
-
} catch {}
|
|
229
|
-
}
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (op === 1) {
|
|
233
|
-
/* sunucu heartbeat istedi — hemen at */
|
|
234
|
-
try {
|
|
235
|
-
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
236
|
-
} catch {}
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (op === 9 || op === 7) {
|
|
240
|
-
/* invalid session / reconnect isteği — kapat, close handler yeniden bağlanır */
|
|
241
|
-
try { ws.close(); } catch {}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (op === 0 && t === 'READY') {
|
|
245
|
-
this._backoff = 2000;
|
|
246
|
-
this._setStatus('connected', '@' + ((d && d.user && d.user.username) || 'bot'));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (op === 0 && t === 'MESSAGE_CREATE' && this.onIncoming) {
|
|
250
|
-
try {
|
|
251
|
-
const m = d || {};
|
|
252
|
-
if (!m.author || m.author.bot || m.webhook_id) return;
|
|
253
|
-
let text = String(m.content || '').trim();
|
|
254
|
-
if (!text) return;
|
|
255
|
-
const botId = this.user && this.user.id;
|
|
256
|
-
const mentioned = !!(botId && text.includes('<@' + botId + '>'));
|
|
257
|
-
if (botId) text = text.split('<@' + botId + '>').join(' ').replace(/\s+/g, ' ').trim();
|
|
258
|
-
const payload = {
|
|
259
|
-
text: text.slice(0, 6000),
|
|
260
|
-
senderId: String((m.author && m.author.id) || ''),
|
|
261
|
-
username: String((m.author && m.author.username) || ''),
|
|
262
|
-
senderName: String((m.author && (m.author.global_name || m.author.username)) || ''),
|
|
263
|
-
isGroup: !!m.guild_id,
|
|
264
|
-
mentioned,
|
|
265
|
-
channelId: String(m.channel_id || ''),
|
|
266
|
-
};
|
|
267
|
-
if (!payload.channelId) return;
|
|
268
|
-
/* Sunucu mesajlarında yalnız @mention (spam koruması); DM'de hepsi */
|
|
269
|
-
if (payload.isGroup && !mentioned) return;
|
|
270
|
-
this.onIncoming(payload.channelId, payload);
|
|
271
|
-
} catch {}
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
ws.on('close', () => {
|
|
277
|
-
clearInterval(this._hbTimer);
|
|
278
|
-
this._hbTimer = null;
|
|
279
|
-
if (this.stopping) return;
|
|
280
|
-
this._setStatus('connecting');
|
|
281
|
-
const wait = this._backoff;
|
|
282
|
-
this._backoff = Math.min(30000, this._backoff * 2);
|
|
283
|
-
setTimeout(() => this._connect(), wait);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
ws.on('error', (e) => {
|
|
287
|
-
/* gateway TLS'i de ara yazılımdan etkilenebilir — esnet ve close sonrası
|
|
288
|
-
yeniden bağlanma zaten relaxed ile kurar */
|
|
289
|
-
if (isCertError(e) && !tlsRelaxed) {
|
|
290
|
-
tlsRelaxed = true;
|
|
291
|
-
this.emit({
|
|
292
|
-
type: 'warn',
|
|
293
|
-
text: 'ağ ara yazılımı (antivirüs/proxy) self-signed sertifikası tespit edildi — Discord bağlantısı esnek TLS ile devam ediyor',
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async stop() {
|
|
300
|
-
this.stopping = true;
|
|
301
|
-
clearInterval(this._hbTimer);
|
|
302
|
-
this._hbTimer = null;
|
|
303
|
-
try {
|
|
304
|
-
if (this._ws) this._ws.close();
|
|
305
|
-
} catch {}
|
|
306
|
-
this._ws = null;
|
|
307
|
-
this.connected = false;
|
|
308
|
-
this.status = 'disconnected';
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
snapshot() {
|
|
312
|
-
return {
|
|
313
|
-
status: this.status,
|
|
314
|
-
user: this.user ? '@' + (this.user.username || 'bot') : null,
|
|
315
|
-
connected: this.connected,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/* Metin gönder — 2000 karakter sınırı için parçalara böl */
|
|
320
|
-
async send(channelId, text) {
|
|
321
|
-
const t = String(text || '');
|
|
322
|
-
if (!t.trim() || !channelId) return false;
|
|
323
|
-
const chunks = [];
|
|
324
|
-
for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
|
|
325
|
-
for (const part of chunks) {
|
|
326
|
-
await this.api('POST', `/channels/${channelId}/messages`, { content: part });
|
|
327
|
-
}
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
module.exports = { DiscordBridge };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* DISCORD KÖPRÜSÜ — Telegram köprüsünün Discord hali.
|
|
4
|
+
Bağımlılık: yalnızca `ws` (paket zaten kurulu — baileys ile geliyor).
|
|
5
|
+
- Gateway WebSocket: Identify → READY → MESSAGE_CREATE dispatch
|
|
6
|
+
- REST (api/v10): mesaj gönderme (2000 karakter sınırında bölerek)
|
|
7
|
+
- Sunucu (guild) mesajlarında yalnız @mention'a cevap verilir (spam koruması);
|
|
8
|
+
DM'de her izinli kullanıcıya cevap verilir.
|
|
9
|
+
- Bağlantı koparsa backoff ile yeniden bağlanır (yeniden Identify;
|
|
10
|
+
çevrimdışıyken gelen mesajlar backfill edilmez — Telegram long-poll'un aksine).
|
|
11
|
+
ÖNEMLİ: Discord Developer Portal'da bot için "MESSAGE CONTENT INTENT"
|
|
12
|
+
açılmalıdır — yoksa mesaj içerikleri boş gelir. */
|
|
13
|
+
|
|
14
|
+
const WebSocket = require('ws');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
|
|
17
|
+
const API_BASE = 'https://discord.com/api/v10';
|
|
18
|
+
const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
19
|
+
const SEND_CHUNK = 1900; // Discord mesaj sınırı 2000 — güvenli pay
|
|
20
|
+
/* GUILDS(1) | GUILD_MESSAGES(512) | DIRECT_MESSAGES(4096) | MESSAGE_CONTENT(32768) */
|
|
21
|
+
const INTENTS = 1 | 512 | 4096 | 32768;
|
|
22
|
+
|
|
23
|
+
/* AĞ ARA YAZILIMI (antivirüs SSL taraması / proxy / ağ filtresi) Discord
|
|
24
|
+
trafiğine araya girip SELF-SIGNED sertifika sunarsa Node varsayılan olarak
|
|
25
|
+
bağlantıyı reddeder ("self signed certificate") ve bot hiç bağlanamaz.
|
|
26
|
+
Çözüm iki katman:
|
|
27
|
+
1) REST istekleri ÖNCE Electron/Chromium ağ yığınından (net.fetch) geçer —
|
|
28
|
+
sistem CA + sistem proxy otomatik tanınır (tarayıcının çalıştığı yol).
|
|
29
|
+
2) Zorlanırsa Node https'e düşülür; self-signed tespit edilirse bu köprü
|
|
30
|
+
için TLS doğrulaması esnetilir — YALNIZ Discord bağlantıları etkilenir. */
|
|
31
|
+
let tlsRelaxed = false;
|
|
32
|
+
const CERT_ERR_RE = /self[ -]?signed|unable to verify the first certificate|depth zero|err_tls|certificate has expired|cert_has_expired|unable_to_get_issuer|self signed certificate/i;
|
|
33
|
+
|
|
34
|
+
function isCertError(e) {
|
|
35
|
+
return CERT_ERR_RE.test(String((e && (e.message || e.code)) || e || ''));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Electron main process'te Chromium ağ yığını (sistem CA + proxy); düz Node
|
|
39
|
+
(testler) için require('electron') path string döndürür → net tanımsız kalır */
|
|
40
|
+
let _electronNet = null;
|
|
41
|
+
try {
|
|
42
|
+
const el = require('electron');
|
|
43
|
+
if (el && el.net && typeof el.net.fetch === 'function') _electronNet = el.net;
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
function bodySnippet(data) {
|
|
47
|
+
const s = String(data || '').replace(/\s+/g, ' ').trim();
|
|
48
|
+
return s ? ` — gövde: ${s.slice(0, 140)}` : '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Ağ engel sayfası tespiti: Discord API her zaman JSON döner — text/html veya
|
|
52
|
+
erişim-engelle sayfası gelirse ağ seviyesinde blok var demektir (Türkiye'de
|
|
53
|
+
Discord erişimi zaman zaman engellenir). Kullanıcıya net VPN yönlendirmesi. */
|
|
54
|
+
const VPN_HINT = 'Discord bu ağdan ERİŞİME ENGELLİ görünüyor — VPN açıp tekrar dene (engel sayfası döndü)';
|
|
55
|
+
|
|
56
|
+
function looksBlocked(ctype, data) {
|
|
57
|
+
const ct = String(ctype || '');
|
|
58
|
+
const body = String(data || '').slice(0, 4000);
|
|
59
|
+
return (
|
|
60
|
+
ct.includes('text/html') ||
|
|
61
|
+
/erisime[_ ]?engelle|erişime engelle|blocked|block_page/i.test(body)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class DiscordBridge {
|
|
66
|
+
constructor({ token, emit, onIncoming }) {
|
|
67
|
+
this.token = String(token || '').trim();
|
|
68
|
+
this.emit = emit || (() => {});
|
|
69
|
+
this.onIncoming = onIncoming || null;
|
|
70
|
+
this.connected = false;
|
|
71
|
+
this.stopping = false;
|
|
72
|
+
this.status = 'disconnected';
|
|
73
|
+
this.user = null; // { id, username, ... }
|
|
74
|
+
this._ws = null;
|
|
75
|
+
this._hbTimer = null;
|
|
76
|
+
this._seq = null;
|
|
77
|
+
this._backoff = 2000;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_setStatus(status, user) {
|
|
81
|
+
this.status = status;
|
|
82
|
+
this.connected = status === 'connected';
|
|
83
|
+
this.emit({ type: 'status', status, user: user || null });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* REST çağrısı — JSON (token: "Bot <token>").
|
|
87
|
+
Yol 1: Chromium (net.fetch — sistem CA/proxy). Yol 2: Node https (+esnek TLS). */
|
|
88
|
+
api(method, path, body, attempt = 0) {
|
|
89
|
+
return this._api(method, path, body).catch((e) => {
|
|
90
|
+
if (isCertError(e) && !tlsRelaxed) {
|
|
91
|
+
tlsRelaxed = true;
|
|
92
|
+
this.emit({
|
|
93
|
+
type: 'warn',
|
|
94
|
+
text: 'ağ ara yazılımı (antivirüs/proxy) self-signed sertifikası tespit edildi — Discord bağlantısı esnek TLS ile devam ediyor',
|
|
95
|
+
});
|
|
96
|
+
if (attempt < 1) return this.api(method, path, body, attempt + 1);
|
|
97
|
+
}
|
|
98
|
+
throw e;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _api(method, path, body) {
|
|
103
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
104
|
+
const headers = { 'Content-Type': 'application/json', Authorization: 'Bot ' + this.token };
|
|
105
|
+
const url = API_BASE + path;
|
|
106
|
+
|
|
107
|
+
/* YOL 1 — Chromium ağ yığını (varsa): sistem sertifikaları + sistem proxy */
|
|
108
|
+
if (_electronNet) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await _electronNet.fetch(url, {
|
|
111
|
+
method,
|
|
112
|
+
headers,
|
|
113
|
+
...(payload ? { body: payload } : {}),
|
|
114
|
+
});
|
|
115
|
+
const data = await res.text();
|
|
116
|
+
let j = null;
|
|
117
|
+
try { j = data ? JSON.parse(data) : null; } catch {}
|
|
118
|
+
if (res.ok && j !== null) return j;
|
|
119
|
+
if (j === null && looksBlocked(res.headers.get('content-type'), data)) {
|
|
120
|
+
throw new Error(VPN_HINT + bodySnippet(data));
|
|
121
|
+
}
|
|
122
|
+
throw new Error(
|
|
123
|
+
`discord ${path}: HTTP ${res.status}${j && j.message ? ' ' + j.message : ''}${bodySnippet(data)}`
|
|
124
|
+
);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
/* kendi formatladığımız API hatasıysa yukarı taşı; transport hatasıysa
|
|
127
|
+
Node https yoluna düş (orada esnek TLS şansı var) */
|
|
128
|
+
if (/^discord |Discord bu ağ/.test(String((e && e.message) || ''))) throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* YOL 2 — Node https (+ gerektiğinde esnek TLS) */
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const req = https.request(
|
|
135
|
+
url,
|
|
136
|
+
{
|
|
137
|
+
method,
|
|
138
|
+
headers: {
|
|
139
|
+
...headers,
|
|
140
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
141
|
+
},
|
|
142
|
+
timeout: 15000,
|
|
143
|
+
rejectUnauthorized: !tlsRelaxed,
|
|
144
|
+
},
|
|
145
|
+
(res) => {
|
|
146
|
+
let data = '';
|
|
147
|
+
res.setEncoding('utf8');
|
|
148
|
+
res.on('data', (c) => (data += c));
|
|
149
|
+
res.on('end', () => {
|
|
150
|
+
const ctype = String(res.headers['content-type'] || '');
|
|
151
|
+
let j = null;
|
|
152
|
+
try { j = data ? JSON.parse(data) : null; } catch {}
|
|
153
|
+
if (res.statusCode >= 200 && res.statusCode < 300 && j !== null) return resolve(j);
|
|
154
|
+
if (looksBlocked(ctype, data)) {
|
|
155
|
+
return reject(new Error(VPN_HINT + bodySnippet(data)));
|
|
156
|
+
}
|
|
157
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
158
|
+
return reject(new Error(`discord ${path}: bozuk yanıt (HTTP ${res.statusCode}${ctype ? ', ' + ctype : ''})${bodySnippet(data)}`));
|
|
159
|
+
}
|
|
160
|
+
reject(new Error(`discord ${path}: HTTP ${res.statusCode} ${(j && j.message) || ''}`.trim() + bodySnippet(data)));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
if (payload) req.write(payload);
|
|
167
|
+
req.end();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async start() {
|
|
172
|
+
if (!this.token) {
|
|
173
|
+
this._setStatus('error');
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
this.stopping = false;
|
|
177
|
+
this._setStatus('connecting');
|
|
178
|
+
try {
|
|
179
|
+
const me = await this.api('GET', '/users/@me');
|
|
180
|
+
this.user = me; // { id, username, ... }
|
|
181
|
+
this._setStatus('connected', '@' + (me.username || 'bot'));
|
|
182
|
+
} catch (e) {
|
|
183
|
+
this._setStatus('error');
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
this._connect();
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_connect() {
|
|
191
|
+
if (this.stopping) return;
|
|
192
|
+
clearInterval(this._hbTimer);
|
|
193
|
+
this._hbTimer = null;
|
|
194
|
+
let identified = false;
|
|
195
|
+
const ws = new WebSocket(GATEWAY_URL, { rejectUnauthorized: !tlsRelaxed });
|
|
196
|
+
this._ws = ws;
|
|
197
|
+
|
|
198
|
+
ws.on('message', (raw) => {
|
|
199
|
+
let p = null;
|
|
200
|
+
try { p = JSON.parse(String(raw)); } catch { return; }
|
|
201
|
+
const op = p.op;
|
|
202
|
+
const d = p.d;
|
|
203
|
+
const t = p.t;
|
|
204
|
+
if (typeof p.s === 'number') this._seq = p.s;
|
|
205
|
+
|
|
206
|
+
if (op === 10) {
|
|
207
|
+
/* Hello: heartbeat aralığı gelıyor → hafif erken at (güvenli pay) */
|
|
208
|
+
const iv = (d && d.heartbeat_interval) || 41250;
|
|
209
|
+
clearInterval(this._hbTimer);
|
|
210
|
+
this._hbTimer = setInterval(() => {
|
|
211
|
+
try {
|
|
212
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
213
|
+
} catch {}
|
|
214
|
+
}, Math.max(5000, Math.floor(iv * 0.8)));
|
|
215
|
+
if (!identified) {
|
|
216
|
+
identified = true;
|
|
217
|
+
try {
|
|
218
|
+
ws.send(
|
|
219
|
+
JSON.stringify({
|
|
220
|
+
op: 2,
|
|
221
|
+
d: {
|
|
222
|
+
token: this.token,
|
|
223
|
+
intents: INTENTS,
|
|
224
|
+
properties: { os: 'windows', browser: 'beast-agent', device: 'beast-agent' },
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (op === 1) {
|
|
233
|
+
/* sunucu heartbeat istedi — hemen at */
|
|
234
|
+
try {
|
|
235
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
236
|
+
} catch {}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (op === 9 || op === 7) {
|
|
240
|
+
/* invalid session / reconnect isteği — kapat, close handler yeniden bağlanır */
|
|
241
|
+
try { ws.close(); } catch {}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (op === 0 && t === 'READY') {
|
|
245
|
+
this._backoff = 2000;
|
|
246
|
+
this._setStatus('connected', '@' + ((d && d.user && d.user.username) || 'bot'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (op === 0 && t === 'MESSAGE_CREATE' && this.onIncoming) {
|
|
250
|
+
try {
|
|
251
|
+
const m = d || {};
|
|
252
|
+
if (!m.author || m.author.bot || m.webhook_id) return;
|
|
253
|
+
let text = String(m.content || '').trim();
|
|
254
|
+
if (!text) return;
|
|
255
|
+
const botId = this.user && this.user.id;
|
|
256
|
+
const mentioned = !!(botId && text.includes('<@' + botId + '>'));
|
|
257
|
+
if (botId) text = text.split('<@' + botId + '>').join(' ').replace(/\s+/g, ' ').trim();
|
|
258
|
+
const payload = {
|
|
259
|
+
text: text.slice(0, 6000),
|
|
260
|
+
senderId: String((m.author && m.author.id) || ''),
|
|
261
|
+
username: String((m.author && m.author.username) || ''),
|
|
262
|
+
senderName: String((m.author && (m.author.global_name || m.author.username)) || ''),
|
|
263
|
+
isGroup: !!m.guild_id,
|
|
264
|
+
mentioned,
|
|
265
|
+
channelId: String(m.channel_id || ''),
|
|
266
|
+
};
|
|
267
|
+
if (!payload.channelId) return;
|
|
268
|
+
/* Sunucu mesajlarında yalnız @mention (spam koruması); DM'de hepsi */
|
|
269
|
+
if (payload.isGroup && !mentioned) return;
|
|
270
|
+
this.onIncoming(payload.channelId, payload);
|
|
271
|
+
} catch {}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
ws.on('close', () => {
|
|
277
|
+
clearInterval(this._hbTimer);
|
|
278
|
+
this._hbTimer = null;
|
|
279
|
+
if (this.stopping) return;
|
|
280
|
+
this._setStatus('connecting');
|
|
281
|
+
const wait = this._backoff;
|
|
282
|
+
this._backoff = Math.min(30000, this._backoff * 2);
|
|
283
|
+
setTimeout(() => this._connect(), wait);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
ws.on('error', (e) => {
|
|
287
|
+
/* gateway TLS'i de ara yazılımdan etkilenebilir — esnet ve close sonrası
|
|
288
|
+
yeniden bağlanma zaten relaxed ile kurar */
|
|
289
|
+
if (isCertError(e) && !tlsRelaxed) {
|
|
290
|
+
tlsRelaxed = true;
|
|
291
|
+
this.emit({
|
|
292
|
+
type: 'warn',
|
|
293
|
+
text: 'ağ ara yazılımı (antivirüs/proxy) self-signed sertifikası tespit edildi — Discord bağlantısı esnek TLS ile devam ediyor',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async stop() {
|
|
300
|
+
this.stopping = true;
|
|
301
|
+
clearInterval(this._hbTimer);
|
|
302
|
+
this._hbTimer = null;
|
|
303
|
+
try {
|
|
304
|
+
if (this._ws) this._ws.close();
|
|
305
|
+
} catch {}
|
|
306
|
+
this._ws = null;
|
|
307
|
+
this.connected = false;
|
|
308
|
+
this.status = 'disconnected';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
snapshot() {
|
|
312
|
+
return {
|
|
313
|
+
status: this.status,
|
|
314
|
+
user: this.user ? '@' + (this.user.username || 'bot') : null,
|
|
315
|
+
connected: this.connected,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Metin gönder — 2000 karakter sınırı için parçalara böl */
|
|
320
|
+
async send(channelId, text) {
|
|
321
|
+
const t = String(text || '');
|
|
322
|
+
if (!t.trim() || !channelId) return false;
|
|
323
|
+
const chunks = [];
|
|
324
|
+
for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
|
|
325
|
+
for (const part of chunks) {
|
|
326
|
+
await this.api('POST', `/channels/${channelId}/messages`, { content: part });
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = { DiscordBridge };
|