natureco-cli 5.4.21 → 5.5.1
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/package.json +10 -2
- package/src/commands/channel-helper.js +51 -0
- package/src/commands/discord.js +116 -114
- package/src/commands/gateway-server.js +2107 -1977
- package/src/commands/imessage.js +2 -0
- package/src/commands/setup.js +22 -0
- package/src/commands/signal.js +2 -0
- package/src/commands/slack.js +121 -119
- package/src/commands/whatsapp.js +318 -316
- package/src/utils/api.js +1158 -1054
|
@@ -1,1977 +1,2107 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const { spawn, execSync } = require('child_process');
|
|
6
|
-
const pino = require('pino');
|
|
7
|
-
const { loadBaileys } = require('../utils/baileys');
|
|
8
|
-
const { ApiError } = require('../utils/errors');
|
|
9
|
-
|
|
10
|
-
const PID_FILE = path.join(os.homedir(), '.natureco', 'gateway.pid');
|
|
11
|
-
const LOG_FILE = path.join(os.homedir(), '.natureco', 'gateway.log');
|
|
12
|
-
|
|
13
|
-
// Silent logger for Baileys
|
|
14
|
-
const silentLogger = {
|
|
15
|
-
level: 'silent',
|
|
16
|
-
trace: () => {},
|
|
17
|
-
debug: () => {},
|
|
18
|
-
info: () => {},
|
|
19
|
-
warn: () => {},
|
|
20
|
-
error: () => {},
|
|
21
|
-
fatal: () => {},
|
|
22
|
-
child: () => silentLogger
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Create Baileys logger — real pino in worker mode, silent otherwise
|
|
26
|
-
let _baileysLogger = null;
|
|
27
|
-
function getBaileysLogger() {
|
|
28
|
-
if (!_baileysLogger) {
|
|
29
|
-
const isWorker = process.argv.includes('--gateway-worker');
|
|
30
|
-
_baileysLogger = isWorker
|
|
31
|
-
? pino({ level: 'info', transport: { target: 'pino/file', options: { destination: LOG_FILE } } })
|
|
32
|
-
: silentLogger;
|
|
33
|
-
}
|
|
34
|
-
return _baileysLogger;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Log helper - only writes to console in worker, file writing handled by stdio redirect
|
|
38
|
-
function log(module, message, color = 'white') {
|
|
39
|
-
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
40
|
-
const logLine = `[${timestamp}] [${module}] ${message}`;
|
|
41
|
-
|
|
42
|
-
// Console output with color (will be redirected to file by parent process)
|
|
43
|
-
const colorFn = chalk[color] || chalk.white;
|
|
44
|
-
console.log(colorFn(logLine));
|
|
45
|
-
|
|
46
|
-
// Note: File writing removed - parent process redirects stdout/stderr to log file
|
|
47
|
-
// This prevents duplicate log entries
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Number matching helper - compare last 10 digits only
|
|
51
|
-
function numberMatches(incoming, allowed) {
|
|
52
|
-
const clean = (n) => n.replace(/\D/g, '').slice(-10);
|
|
53
|
-
return clean(incoming) === clean(allowed);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function gatewayServer(action) {
|
|
57
|
-
// Check if running as background worker
|
|
58
|
-
if (process.argv.includes('--gateway-worker')) {
|
|
59
|
-
process.stdin.resume();
|
|
60
|
-
process.title = 'natureco-gateway';
|
|
61
|
-
log('gateway', 'Worker process started, PID: ' + process.pid);
|
|
62
|
-
return runGatewayWorker();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!action || action === 'start') {
|
|
66
|
-
return startGateway();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (action === 'stop') {
|
|
70
|
-
return stopGateway();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (action === 'status') {
|
|
74
|
-
return statusGateway();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (action === 'logs') {
|
|
78
|
-
return showLogs();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
console.log(chalk.red('\n❌ Unknown action\n'));
|
|
82
|
-
console.log(chalk.gray('Available actions: start, stop, status, logs\n'));
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function startGateway() {
|
|
87
|
-
// Check if already running
|
|
88
|
-
if (fs.existsSync(PID_FILE)) {
|
|
89
|
-
const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
90
|
-
try {
|
|
91
|
-
process.kill(pid, 0);
|
|
92
|
-
console.log(chalk.yellow('\n⚠️ Gateway already running\n'));
|
|
93
|
-
console.log(chalk.gray(`PID: ${pid}`));
|
|
94
|
-
console.log(chalk.gray('Stop with: natureco gateway stop\n'));
|
|
95
|
-
return;
|
|
96
|
-
} catch {
|
|
97
|
-
// Process not running, remove stale PID file
|
|
98
|
-
fs.unlinkSync(PID_FILE);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
console.log(chalk.green('\n🚀 Starting NatureCo Gateway...\n'));
|
|
103
|
-
|
|
104
|
-
// Ensure log directory exists
|
|
105
|
-
const dir = path.dirname(LOG_FILE);
|
|
106
|
-
if (!fs.existsSync(dir)) {
|
|
107
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Open log file for stdio redirection
|
|
111
|
-
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
112
|
-
|
|
113
|
-
// Start as detached background process
|
|
114
|
-
const child = spawn(process.execPath, [require.resolve('./gateway-server'), '--gateway-worker'], {
|
|
115
|
-
detached: true,
|
|
116
|
-
stdio: ['ignore', logFd, logFd],
|
|
117
|
-
windowsHide: true,
|
|
118
|
-
cwd: os.homedir(),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
child.unref();
|
|
122
|
-
|
|
123
|
-
// Save PID
|
|
124
|
-
if (!fs.existsSync(dir)) {
|
|
125
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
-
}
|
|
127
|
-
fs.writeFileSync(PID_FILE, String(child.pid), 'utf-8');
|
|
128
|
-
|
|
129
|
-
console.log(chalk.green('✅ Gateway started in background'));
|
|
130
|
-
console.log(chalk.cyan('PID:'), chalk.white(child.pid));
|
|
131
|
-
console.log(chalk.cyan('Logs:'), chalk.white(LOG_FILE));
|
|
132
|
-
console.log(chalk.gray('\nView logs: natureco gateway logs'));
|
|
133
|
-
console.log(chalk.gray('Check status: natureco gateway status'));
|
|
134
|
-
console.log(chalk.gray('Stop gateway: natureco gateway stop\n'));
|
|
135
|
-
|
|
136
|
-
process.exit(0);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function runGatewayWorker() {
|
|
140
|
-
// This runs in the background
|
|
141
|
-
const pkg = require('../../package.json');
|
|
142
|
-
log('gateway', `Starting NatureCo Gateway v${pkg.version}...`, 'green');
|
|
143
|
-
|
|
144
|
-
// Load config
|
|
145
|
-
const { getConfig } = require('../utils/config');
|
|
146
|
-
const config = getConfig();
|
|
147
|
-
|
|
148
|
-
if (!config || !config.providerUrl) {
|
|
149
|
-
log('gateway', 'Setup yapılmamış. Run "natureco setup" first.', 'red');
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Store provider instances globally for HTTP endpoint
|
|
154
|
-
global.whatsappSock = null;
|
|
155
|
-
global.telegramBot = null;
|
|
156
|
-
global.signalProvider = null;
|
|
157
|
-
|
|
158
|
-
// Start WhatsApp if configured
|
|
159
|
-
if (config.whatsappConnected && config.whatsappBotId) {
|
|
160
|
-
const sessionDir = path.join(os.homedir(), '.natureco', 'whatsapp-sessions', config.whatsappBotId);
|
|
161
|
-
|
|
162
|
-
if (fs.existsSync(sessionDir)) {
|
|
163
|
-
const phone = config.whatsappPhone?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
164
|
-
log('whatsapp', `starting provider (+${phone})`, 'cyan');
|
|
165
|
-
startWhatsAppProvider(sessionDir, config);
|
|
166
|
-
} else {
|
|
167
|
-
log('whatsapp', 'session not found, skipping', 'yellow');
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
log('whatsapp', 'not configured, skipping', 'gray');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Start Telegram if configured
|
|
174
|
-
if (config.telegramToken && config.telegramBotId) {
|
|
175
|
-
log('telegram', 'starting bot...', 'cyan');
|
|
176
|
-
startTelegramProvider(config);
|
|
177
|
-
} else {
|
|
178
|
-
log('telegram', 'not configured, skipping', 'gray');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Start Signal if configured
|
|
182
|
-
if (config.signalBotId && config.signalHttpUrl) {
|
|
183
|
-
log('signal', `starting provider (${config.signalAccount || config.signalHttpUrl})`, 'cyan');
|
|
184
|
-
startSignalProvider(config);
|
|
185
|
-
} else if (config.signalBotId) {
|
|
186
|
-
log('signal', 'not configured (no HTTP URL), skipping', 'gray');
|
|
187
|
-
} else {
|
|
188
|
-
log('signal', 'not configured, skipping', 'gray');
|
|
189
|
-
}
|
|
190
|
-
// Start IRC if configured
|
|
191
|
-
if (config.ircBotId && config.ircHost && config.ircNick) {
|
|
192
|
-
log('irc', `connecting to ${config.ircNick} @ ${config.ircHost}:${config.ircPort}`, 'cyan');
|
|
193
|
-
startIrcProvider(config);
|
|
194
|
-
} else if (config.ircBotId) {
|
|
195
|
-
log('irc', 'not configured (missing host or nick), skipping', 'gray');
|
|
196
|
-
} else {
|
|
197
|
-
log('irc', 'not configured, skipping', 'gray');
|
|
198
|
-
}
|
|
199
|
-
// Start Mattermost if configured
|
|
200
|
-
if (config.mattermostBotId && config.mattermostBaseUrl && config.mattermostToken) {
|
|
201
|
-
log('mattermost', `connecting to ${config.mattermostBaseUrl}`, 'cyan');
|
|
202
|
-
startMattermostProvider(config);
|
|
203
|
-
} else if (config.mattermostBotId) {
|
|
204
|
-
log('mattermost', 'not configured (missing URL or token), skipping', 'gray');
|
|
205
|
-
} else {
|
|
206
|
-
log('mattermost', 'not configured, skipping', 'gray');
|
|
207
|
-
}
|
|
208
|
-
// Start iMessage if configured
|
|
209
|
-
if (config.imessageBotId) {
|
|
210
|
-
if (process.platform === 'darwin') {
|
|
211
|
-
log('imessage', 'starting provider (macOS)', 'cyan');
|
|
212
|
-
startImessageProvider(config);
|
|
213
|
-
} else {
|
|
214
|
-
log('imessage', 'macOS only, skipping', 'yellow');
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
log('imessage', 'not configured, skipping', 'gray');
|
|
218
|
-
}
|
|
219
|
-
// Start SMS provider if configured
|
|
220
|
-
if (config.smsBotId && config.smsAccountSid && config.smsAuthToken) {
|
|
221
|
-
log('sms', `starting provider (${config.smsFromNumber || 'Messaging Service'})`, 'cyan');
|
|
222
|
-
startSmsProvider(config);
|
|
223
|
-
} else if (config.smsBotId) {
|
|
224
|
-
log('sms', 'not configured (missing Account SID), skipping', 'gray');
|
|
225
|
-
} else {
|
|
226
|
-
log('sms', 'not configured, skipping', 'gray');
|
|
227
|
-
}
|
|
228
|
-
if (config.webhooks?.length) log('webhooks', `${config.webhooks.length} route(s) configured`, 'gray');
|
|
229
|
-
|
|
230
|
-
// Start HTTP server for message sending
|
|
231
|
-
startHttpServer();
|
|
232
|
-
|
|
233
|
-
// Load and start cron jobs
|
|
234
|
-
startCronJobs(config);
|
|
235
|
-
|
|
236
|
-
// Health check every 60 seconds
|
|
237
|
-
setInterval(async () => {
|
|
238
|
-
const alive = fs.existsSync(PID_FILE) && (() => { try { process.kill(process.pid, 0); return true; } catch { return false; } })();
|
|
239
|
-
const wsOk = global.gatewayWs && global.gatewayWs.readyState === 1;
|
|
240
|
-
if (alive && wsOk) {
|
|
241
|
-
log('gateway', 'health check: OK', 'gray');
|
|
242
|
-
} else {
|
|
243
|
-
log('gateway', `health check: WARN (alive=${alive}, ws=${wsOk})`, 'yellow');
|
|
244
|
-
}
|
|
245
|
-
}, 60000);
|
|
246
|
-
|
|
247
|
-
log('gateway', 'Gateway running in background', 'green');
|
|
248
|
-
|
|
249
|
-
// Handle shutdown
|
|
250
|
-
const shutdown = () => {
|
|
251
|
-
log('gateway', 'Shutting down...', 'yellow');
|
|
252
|
-
if (global.signalProvider?.ws) {
|
|
253
|
-
try { global.signalProvider.ws.close(); } catch {}
|
|
254
|
-
}
|
|
255
|
-
if (signalPollingInterval) {
|
|
256
|
-
clearInterval(signalPollingInterval);
|
|
257
|
-
signalPollingInterval = null;
|
|
258
|
-
}
|
|
259
|
-
stopIrcProvider();
|
|
260
|
-
stopMattermostProvider();
|
|
261
|
-
stopImessageProvider();
|
|
262
|
-
stopSmsProvider();
|
|
263
|
-
if (fs.existsSync(PID_FILE)) {
|
|
264
|
-
fs.unlinkSync(PID_FILE);
|
|
265
|
-
}
|
|
266
|
-
process.exit(0);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
process.on('SIGINT', shutdown);
|
|
270
|
-
process.on('SIGTERM', shutdown);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async function startWhatsAppProvider(sessionDir, config) {
|
|
274
|
-
try {
|
|
275
|
-
const { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, Browsers } = loadBaileys();
|
|
276
|
-
|
|
277
|
-
const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
|
|
278
|
-
const { version } = await fetchLatestBaileysVersion();
|
|
279
|
-
|
|
280
|
-
// Track last bot reply to prevent infinite loop
|
|
281
|
-
let lastBotReply = '';
|
|
282
|
-
|
|
283
|
-
const sock = makeWASocket({
|
|
284
|
-
version,
|
|
285
|
-
auth: state,
|
|
286
|
-
printQRInTerminal: false,
|
|
287
|
-
logger: getBaileysLogger(),
|
|
288
|
-
browser: Browsers.ubuntu('Chrome'),
|
|
289
|
-
connectTimeoutMs: 60000,
|
|
290
|
-
defaultQueryTimeoutMs: 60000,
|
|
291
|
-
keepAliveIntervalMs: 10000,
|
|
292
|
-
retryRequestDelayMs: 2000,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Connection update handler
|
|
296
|
-
sock.ev.on('connection.update', async (update) => {
|
|
297
|
-
const { connection, lastDisconnect } = update;
|
|
298
|
-
|
|
299
|
-
if (connection === 'close') {
|
|
300
|
-
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
301
|
-
|
|
302
|
-
// Remove all event listeners before reconnecting
|
|
303
|
-
sock.ev.removeAllListeners('connection.update');
|
|
304
|
-
sock.ev.removeAllListeners('messages.upsert');
|
|
305
|
-
sock.ev.removeAllListeners('creds.update');
|
|
306
|
-
|
|
307
|
-
if (statusCode === 515 || statusCode === 408) {
|
|
308
|
-
log('whatsapp', 'connection lost, reconnecting in 10s...', 'yellow');
|
|
309
|
-
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
310
|
-
return;
|
|
311
|
-
} else if (statusCode === 401) {
|
|
312
|
-
log('whatsapp', 'session expired, please reconnect', 'red');
|
|
313
|
-
} else {
|
|
314
|
-
log('whatsapp', `disconnected (code: ${statusCode}), reconnecting in 10s...`, 'yellow');
|
|
315
|
-
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
316
|
-
}
|
|
317
|
-
} else if (connection === 'open') {
|
|
318
|
-
const phone = sock.user?.id?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
319
|
-
log('whatsapp', `Connected successfully`, 'green');
|
|
320
|
-
log('whatsapp', `Listening for inbound messages.`, 'cyan');
|
|
321
|
-
log('whatsapp', `Phone: +${phone}`, 'gray');
|
|
322
|
-
|
|
323
|
-
// Store socket globally for HTTP endpoint
|
|
324
|
-
global.whatsappSock = sock;
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
sock.ev.on('messages.upsert', async ({ messages }) => {
|
|
329
|
-
log('whatsapp', `messages.upsert event triggered, ${messages.length} message(s)`, 'gray');
|
|
330
|
-
|
|
331
|
-
for (const msg of messages) {
|
|
332
|
-
// Log raw message info
|
|
333
|
-
log('whatsapp', `Raw message - fromMe: ${msg.key.fromMe}, remoteJid: ${msg.key.remoteJid}`, 'gray');
|
|
334
|
-
|
|
335
|
-
const remoteJid = msg.key.remoteJid || '';
|
|
336
|
-
const isLID = remoteJid.includes('@lid');
|
|
337
|
-
|
|
338
|
-
// Skip bot's own replies (fromMe=true and not LID, or matches last bot reply)
|
|
339
|
-
const messageText = msg.message?.conversation ||
|
|
340
|
-
msg.message?.extendedTextMessage?.text ||
|
|
341
|
-
msg.message?.imageMessage?.caption ||
|
|
342
|
-
msg.message?.videoMessage?.caption ||
|
|
343
|
-
msg.message?.buttonsResponseMessage?.selectedDisplayText ||
|
|
344
|
-
msg.message?.listResponseMessage?.title ||
|
|
345
|
-
msg.message?.ephemeralMessage?.message?.conversation ||
|
|
346
|
-
msg.message?.viewOnceMessage?.message?.conversation ||
|
|
347
|
-
'';
|
|
348
|
-
|
|
349
|
-
// Skip if this is the bot's own reply
|
|
350
|
-
if (msg.key.fromMe && !isLID) {
|
|
351
|
-
log('whatsapp', 'Skipping bot own reply (fromMe=true, not LID)', 'gray');
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Skip if message matches last bot reply (prevent loop)
|
|
356
|
-
if (messageText === lastBotReply && lastBotReply !== '') {
|
|
357
|
-
log('whatsapp', 'Skipping duplicate bot reply (matches last sent)', 'gray');
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Handle LID format (new WhatsApp format)
|
|
362
|
-
if (isLID && !msg.key.fromMe) {
|
|
363
|
-
log('whatsapp', 'LID format detected, fromMe=false, blocked', 'yellow');
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const sender = remoteJid.split('@')[0].split(':')[0];
|
|
368
|
-
const allowedNumbers = config.whatsappAllowedNumbers || [];
|
|
369
|
-
|
|
370
|
-
// Log incoming number
|
|
371
|
-
log('whatsapp', `Incoming from: +${sender}, allowed: ${JSON.stringify(allowedNumbers)}`, 'gray');
|
|
372
|
-
|
|
373
|
-
// Access control - skip if fromMe + LID (own conversation)
|
|
374
|
-
if (!(msg.key.fromMe && isLID) && allowedNumbers.length > 0 && !allowedNumbers.some(n => numberMatches(n, sender))) {
|
|
375
|
-
log('whatsapp', `blocked message from +${sender} (not in allowed list)`, 'yellow');
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (!messageText) {
|
|
380
|
-
log('whatsapp', `Message without text content. Keys: ${Object.keys(msg.message || {}).join(', ')}`, 'gray');
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const ownNumber = sock.user?.id?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
385
|
-
log('whatsapp', `Inbound message +${sender} -> +${ownNumber} (${messageText.length} chars)`, 'cyan');
|
|
386
|
-
|
|
387
|
-
try {
|
|
388
|
-
const { sendMessage } = require('../utils/api');
|
|
389
|
-
const { getMemoryPrompt, extractMemoryFromMessage, addMemoryEntry } = require('../utils/memory');
|
|
390
|
-
const { getConfig } = require('../utils/config');
|
|
391
|
-
const cfg = getConfig();
|
|
392
|
-
|
|
393
|
-
log('whatsapp', 'Sending to AI provider...', 'cyan');
|
|
394
|
-
|
|
395
|
-
// Use WhatsApp-specific conversation ID for persistent history
|
|
396
|
-
const conversationId = `whatsapp_${sender.replace(/\D/g, '')}`;
|
|
397
|
-
|
|
398
|
-
// Use same botId as terminal for shared memory
|
|
399
|
-
const botId = 'universal-provider';
|
|
400
|
-
const memoryPrompt = getMemoryPrompt(botId);
|
|
401
|
-
|
|
402
|
-
// WhatsApp system prompt with memory
|
|
403
|
-
let systemPrompt = `You are a helpful WhatsApp assistant. Keep responses concise and friendly. Use emojis when appropriate. If users ask for file operations or system commands, politely explain that those features are available in the terminal version.`;
|
|
404
|
-
|
|
405
|
-
if (memoryPrompt) {
|
|
406
|
-
systemPrompt += '\n\n' + memoryPrompt;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const response = await sendMessage(cfg.providerApiKey || cfg.apiKey || '', botId, messageText, conversationId, systemPrompt);
|
|
410
|
-
const reply = response?.reply || response?.message || '';
|
|
411
|
-
|
|
412
|
-
if (reply) {
|
|
413
|
-
log('whatsapp', 'Sending reply...', 'cyan');
|
|
414
|
-
|
|
415
|
-
await sock.sendMessage(msg.key.remoteJid, { text: reply });
|
|
416
|
-
|
|
417
|
-
// Store last bot reply to prevent loop
|
|
418
|
-
lastBotReply = reply;
|
|
419
|
-
|
|
420
|
-
log('whatsapp', `Reply sent (${reply.length} chars)`, 'green');
|
|
421
|
-
|
|
422
|
-
// Extract and save memory from user message
|
|
423
|
-
const memoryEntries = extractMemoryFromMessage(messageText);
|
|
424
|
-
for (const entry of memoryEntries) {
|
|
425
|
-
addMemoryEntry(botId, entry.key, entry.value);
|
|
426
|
-
log('whatsapp', `Memory saved: ${entry.key} = ${entry.value}`, 'gray');
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
log('whatsapp', 'No reply from provider', 'yellow');
|
|
430
|
-
}
|
|
431
|
-
} catch (err) {
|
|
432
|
-
log('whatsapp', `Provider error: ${err.message}`, 'red');
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
sock.ev.on('creds.update', saveCreds);
|
|
438
|
-
|
|
439
|
-
} catch (err) {
|
|
440
|
-
log('whatsapp', `Failed to start: ${err.message}`, 'red');
|
|
441
|
-
log('whatsapp', 'Retrying in 10s...', 'yellow');
|
|
442
|
-
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let
|
|
450
|
-
try {
|
|
451
|
-
|
|
452
|
-
} catch (
|
|
453
|
-
log('
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
signal:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if (
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (!
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
);
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
if (
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { spawn, execSync } = require('child_process');
|
|
6
|
+
const pino = require('pino');
|
|
7
|
+
const { loadBaileys } = require('../utils/baileys');
|
|
8
|
+
const { ApiError } = require('../utils/errors');
|
|
9
|
+
|
|
10
|
+
const PID_FILE = path.join(os.homedir(), '.natureco', 'gateway.pid');
|
|
11
|
+
const LOG_FILE = path.join(os.homedir(), '.natureco', 'gateway.log');
|
|
12
|
+
|
|
13
|
+
// Silent logger for Baileys
|
|
14
|
+
const silentLogger = {
|
|
15
|
+
level: 'silent',
|
|
16
|
+
trace: () => {},
|
|
17
|
+
debug: () => {},
|
|
18
|
+
info: () => {},
|
|
19
|
+
warn: () => {},
|
|
20
|
+
error: () => {},
|
|
21
|
+
fatal: () => {},
|
|
22
|
+
child: () => silentLogger
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Create Baileys logger — real pino in worker mode, silent otherwise
|
|
26
|
+
let _baileysLogger = null;
|
|
27
|
+
function getBaileysLogger() {
|
|
28
|
+
if (!_baileysLogger) {
|
|
29
|
+
const isWorker = process.argv.includes('--gateway-worker');
|
|
30
|
+
_baileysLogger = isWorker
|
|
31
|
+
? pino({ level: 'info', transport: { target: 'pino/file', options: { destination: LOG_FILE } } })
|
|
32
|
+
: silentLogger;
|
|
33
|
+
}
|
|
34
|
+
return _baileysLogger;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Log helper - only writes to console in worker, file writing handled by stdio redirect
|
|
38
|
+
function log(module, message, color = 'white') {
|
|
39
|
+
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
40
|
+
const logLine = `[${timestamp}] [${module}] ${message}`;
|
|
41
|
+
|
|
42
|
+
// Console output with color (will be redirected to file by parent process)
|
|
43
|
+
const colorFn = chalk[color] || chalk.white;
|
|
44
|
+
console.log(colorFn(logLine));
|
|
45
|
+
|
|
46
|
+
// Note: File writing removed - parent process redirects stdout/stderr to log file
|
|
47
|
+
// This prevents duplicate log entries
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Number matching helper - compare last 10 digits only
|
|
51
|
+
function numberMatches(incoming, allowed) {
|
|
52
|
+
const clean = (n) => n.replace(/\D/g, '').slice(-10);
|
|
53
|
+
return clean(incoming) === clean(allowed);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function gatewayServer(action) {
|
|
57
|
+
// Check if running as background worker
|
|
58
|
+
if (process.argv.includes('--gateway-worker')) {
|
|
59
|
+
process.stdin.resume();
|
|
60
|
+
process.title = 'natureco-gateway';
|
|
61
|
+
log('gateway', 'Worker process started, PID: ' + process.pid);
|
|
62
|
+
return runGatewayWorker();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!action || action === 'start') {
|
|
66
|
+
return startGateway();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (action === 'stop') {
|
|
70
|
+
return stopGateway();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (action === 'status') {
|
|
74
|
+
return statusGateway();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (action === 'logs') {
|
|
78
|
+
return showLogs();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(chalk.red('\n❌ Unknown action\n'));
|
|
82
|
+
console.log(chalk.gray('Available actions: start, stop, status, logs\n'));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function startGateway() {
|
|
87
|
+
// Check if already running
|
|
88
|
+
if (fs.existsSync(PID_FILE)) {
|
|
89
|
+
const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, 0);
|
|
92
|
+
console.log(chalk.yellow('\n⚠️ Gateway already running\n'));
|
|
93
|
+
console.log(chalk.gray(`PID: ${pid}`));
|
|
94
|
+
console.log(chalk.gray('Stop with: natureco gateway stop\n'));
|
|
95
|
+
return;
|
|
96
|
+
} catch {
|
|
97
|
+
// Process not running, remove stale PID file
|
|
98
|
+
fs.unlinkSync(PID_FILE);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(chalk.green('\n🚀 Starting NatureCo Gateway...\n'));
|
|
103
|
+
|
|
104
|
+
// Ensure log directory exists
|
|
105
|
+
const dir = path.dirname(LOG_FILE);
|
|
106
|
+
if (!fs.existsSync(dir)) {
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Open log file for stdio redirection
|
|
111
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
112
|
+
|
|
113
|
+
// Start as detached background process
|
|
114
|
+
const child = spawn(process.execPath, [require.resolve('./gateway-server'), '--gateway-worker'], {
|
|
115
|
+
detached: true,
|
|
116
|
+
stdio: ['ignore', logFd, logFd],
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
cwd: os.homedir(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
child.unref();
|
|
122
|
+
|
|
123
|
+
// Save PID
|
|
124
|
+
if (!fs.existsSync(dir)) {
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
fs.writeFileSync(PID_FILE, String(child.pid), 'utf-8');
|
|
128
|
+
|
|
129
|
+
console.log(chalk.green('✅ Gateway started in background'));
|
|
130
|
+
console.log(chalk.cyan('PID:'), chalk.white(child.pid));
|
|
131
|
+
console.log(chalk.cyan('Logs:'), chalk.white(LOG_FILE));
|
|
132
|
+
console.log(chalk.gray('\nView logs: natureco gateway logs'));
|
|
133
|
+
console.log(chalk.gray('Check status: natureco gateway status'));
|
|
134
|
+
console.log(chalk.gray('Stop gateway: natureco gateway stop\n'));
|
|
135
|
+
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runGatewayWorker() {
|
|
140
|
+
// This runs in the background
|
|
141
|
+
const pkg = require('../../package.json');
|
|
142
|
+
log('gateway', `Starting NatureCo Gateway v${pkg.version}...`, 'green');
|
|
143
|
+
|
|
144
|
+
// Load config
|
|
145
|
+
const { getConfig } = require('../utils/config');
|
|
146
|
+
const config = getConfig();
|
|
147
|
+
|
|
148
|
+
if (!config || !config.providerUrl) {
|
|
149
|
+
log('gateway', 'Setup yapılmamış. Run "natureco setup" first.', 'red');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Store provider instances globally for HTTP endpoint
|
|
154
|
+
global.whatsappSock = null;
|
|
155
|
+
global.telegramBot = null;
|
|
156
|
+
global.signalProvider = null;
|
|
157
|
+
|
|
158
|
+
// Start WhatsApp if configured
|
|
159
|
+
if (config.whatsappConnected && config.whatsappBotId) {
|
|
160
|
+
const sessionDir = path.join(os.homedir(), '.natureco', 'whatsapp-sessions', config.whatsappBotId);
|
|
161
|
+
|
|
162
|
+
if (fs.existsSync(sessionDir)) {
|
|
163
|
+
const phone = config.whatsappPhone?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
164
|
+
log('whatsapp', `starting provider (+${phone})`, 'cyan');
|
|
165
|
+
startWhatsAppProvider(sessionDir, config);
|
|
166
|
+
} else {
|
|
167
|
+
log('whatsapp', 'session not found, skipping', 'yellow');
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
log('whatsapp', 'not configured, skipping', 'gray');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Start Telegram if configured
|
|
174
|
+
if (config.telegramToken && config.telegramBotId) {
|
|
175
|
+
log('telegram', 'starting bot...', 'cyan');
|
|
176
|
+
startTelegramProvider(config);
|
|
177
|
+
} else {
|
|
178
|
+
log('telegram', 'not configured, skipping', 'gray');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Start Signal if configured
|
|
182
|
+
if (config.signalBotId && config.signalHttpUrl) {
|
|
183
|
+
log('signal', `starting provider (${config.signalAccount || config.signalHttpUrl})`, 'cyan');
|
|
184
|
+
startSignalProvider(config);
|
|
185
|
+
} else if (config.signalBotId) {
|
|
186
|
+
log('signal', 'not configured (no HTTP URL), skipping', 'gray');
|
|
187
|
+
} else {
|
|
188
|
+
log('signal', 'not configured, skipping', 'gray');
|
|
189
|
+
}
|
|
190
|
+
// Start IRC if configured
|
|
191
|
+
if (config.ircBotId && config.ircHost && config.ircNick) {
|
|
192
|
+
log('irc', `connecting to ${config.ircNick} @ ${config.ircHost}:${config.ircPort}`, 'cyan');
|
|
193
|
+
startIrcProvider(config);
|
|
194
|
+
} else if (config.ircBotId) {
|
|
195
|
+
log('irc', 'not configured (missing host or nick), skipping', 'gray');
|
|
196
|
+
} else {
|
|
197
|
+
log('irc', 'not configured, skipping', 'gray');
|
|
198
|
+
}
|
|
199
|
+
// Start Mattermost if configured
|
|
200
|
+
if (config.mattermostBotId && config.mattermostBaseUrl && config.mattermostToken) {
|
|
201
|
+
log('mattermost', `connecting to ${config.mattermostBaseUrl}`, 'cyan');
|
|
202
|
+
startMattermostProvider(config);
|
|
203
|
+
} else if (config.mattermostBotId) {
|
|
204
|
+
log('mattermost', 'not configured (missing URL or token), skipping', 'gray');
|
|
205
|
+
} else {
|
|
206
|
+
log('mattermost', 'not configured, skipping', 'gray');
|
|
207
|
+
}
|
|
208
|
+
// Start iMessage if configured
|
|
209
|
+
if (config.imessageBotId) {
|
|
210
|
+
if (process.platform === 'darwin') {
|
|
211
|
+
log('imessage', 'starting provider (macOS)', 'cyan');
|
|
212
|
+
startImessageProvider(config);
|
|
213
|
+
} else {
|
|
214
|
+
log('imessage', 'macOS only, skipping', 'yellow');
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
log('imessage', 'not configured, skipping', 'gray');
|
|
218
|
+
}
|
|
219
|
+
// Start SMS provider if configured
|
|
220
|
+
if (config.smsBotId && config.smsAccountSid && config.smsAuthToken) {
|
|
221
|
+
log('sms', `starting provider (${config.smsFromNumber || 'Messaging Service'})`, 'cyan');
|
|
222
|
+
startSmsProvider(config);
|
|
223
|
+
} else if (config.smsBotId) {
|
|
224
|
+
log('sms', 'not configured (missing Account SID), skipping', 'gray');
|
|
225
|
+
} else {
|
|
226
|
+
log('sms', 'not configured, skipping', 'gray');
|
|
227
|
+
}
|
|
228
|
+
if (config.webhooks?.length) log('webhooks', `${config.webhooks.length} route(s) configured`, 'gray');
|
|
229
|
+
|
|
230
|
+
// Start HTTP server for message sending
|
|
231
|
+
startHttpServer();
|
|
232
|
+
|
|
233
|
+
// Load and start cron jobs
|
|
234
|
+
startCronJobs(config);
|
|
235
|
+
|
|
236
|
+
// Health check every 60 seconds
|
|
237
|
+
setInterval(async () => {
|
|
238
|
+
const alive = fs.existsSync(PID_FILE) && (() => { try { process.kill(process.pid, 0); return true; } catch { return false; } })();
|
|
239
|
+
const wsOk = global.gatewayWs && global.gatewayWs.readyState === 1;
|
|
240
|
+
if (alive && wsOk) {
|
|
241
|
+
log('gateway', 'health check: OK', 'gray');
|
|
242
|
+
} else {
|
|
243
|
+
log('gateway', `health check: WARN (alive=${alive}, ws=${wsOk})`, 'yellow');
|
|
244
|
+
}
|
|
245
|
+
}, 60000);
|
|
246
|
+
|
|
247
|
+
log('gateway', 'Gateway running in background', 'green');
|
|
248
|
+
|
|
249
|
+
// Handle shutdown
|
|
250
|
+
const shutdown = () => {
|
|
251
|
+
log('gateway', 'Shutting down...', 'yellow');
|
|
252
|
+
if (global.signalProvider?.ws) {
|
|
253
|
+
try { global.signalProvider.ws.close(); } catch {}
|
|
254
|
+
}
|
|
255
|
+
if (signalPollingInterval) {
|
|
256
|
+
clearInterval(signalPollingInterval);
|
|
257
|
+
signalPollingInterval = null;
|
|
258
|
+
}
|
|
259
|
+
stopIrcProvider();
|
|
260
|
+
stopMattermostProvider();
|
|
261
|
+
stopImessageProvider();
|
|
262
|
+
stopSmsProvider();
|
|
263
|
+
if (fs.existsSync(PID_FILE)) {
|
|
264
|
+
fs.unlinkSync(PID_FILE);
|
|
265
|
+
}
|
|
266
|
+
process.exit(0);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
process.on('SIGINT', shutdown);
|
|
270
|
+
process.on('SIGTERM', shutdown);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function startWhatsAppProvider(sessionDir, config) {
|
|
274
|
+
try {
|
|
275
|
+
const { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, Browsers } = loadBaileys();
|
|
276
|
+
|
|
277
|
+
const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
|
|
278
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
279
|
+
|
|
280
|
+
// Track last bot reply to prevent infinite loop
|
|
281
|
+
let lastBotReply = '';
|
|
282
|
+
|
|
283
|
+
const sock = makeWASocket({
|
|
284
|
+
version,
|
|
285
|
+
auth: state,
|
|
286
|
+
printQRInTerminal: false,
|
|
287
|
+
logger: getBaileysLogger(),
|
|
288
|
+
browser: Browsers.ubuntu('Chrome'),
|
|
289
|
+
connectTimeoutMs: 60000,
|
|
290
|
+
defaultQueryTimeoutMs: 60000,
|
|
291
|
+
keepAliveIntervalMs: 10000,
|
|
292
|
+
retryRequestDelayMs: 2000,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Connection update handler
|
|
296
|
+
sock.ev.on('connection.update', async (update) => {
|
|
297
|
+
const { connection, lastDisconnect } = update;
|
|
298
|
+
|
|
299
|
+
if (connection === 'close') {
|
|
300
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
301
|
+
|
|
302
|
+
// Remove all event listeners before reconnecting
|
|
303
|
+
sock.ev.removeAllListeners('connection.update');
|
|
304
|
+
sock.ev.removeAllListeners('messages.upsert');
|
|
305
|
+
sock.ev.removeAllListeners('creds.update');
|
|
306
|
+
|
|
307
|
+
if (statusCode === 515 || statusCode === 408) {
|
|
308
|
+
log('whatsapp', 'connection lost, reconnecting in 10s...', 'yellow');
|
|
309
|
+
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
310
|
+
return;
|
|
311
|
+
} else if (statusCode === 401) {
|
|
312
|
+
log('whatsapp', 'session expired, please reconnect', 'red');
|
|
313
|
+
} else {
|
|
314
|
+
log('whatsapp', `disconnected (code: ${statusCode}), reconnecting in 10s...`, 'yellow');
|
|
315
|
+
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
316
|
+
}
|
|
317
|
+
} else if (connection === 'open') {
|
|
318
|
+
const phone = sock.user?.id?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
319
|
+
log('whatsapp', `Connected successfully`, 'green');
|
|
320
|
+
log('whatsapp', `Listening for inbound messages.`, 'cyan');
|
|
321
|
+
log('whatsapp', `Phone: +${phone}`, 'gray');
|
|
322
|
+
|
|
323
|
+
// Store socket globally for HTTP endpoint
|
|
324
|
+
global.whatsappSock = sock;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
sock.ev.on('messages.upsert', async ({ messages }) => {
|
|
329
|
+
log('whatsapp', `messages.upsert event triggered, ${messages.length} message(s)`, 'gray');
|
|
330
|
+
|
|
331
|
+
for (const msg of messages) {
|
|
332
|
+
// Log raw message info
|
|
333
|
+
log('whatsapp', `Raw message - fromMe: ${msg.key.fromMe}, remoteJid: ${msg.key.remoteJid}`, 'gray');
|
|
334
|
+
|
|
335
|
+
const remoteJid = msg.key.remoteJid || '';
|
|
336
|
+
const isLID = remoteJid.includes('@lid');
|
|
337
|
+
|
|
338
|
+
// Skip bot's own replies (fromMe=true and not LID, or matches last bot reply)
|
|
339
|
+
const messageText = msg.message?.conversation ||
|
|
340
|
+
msg.message?.extendedTextMessage?.text ||
|
|
341
|
+
msg.message?.imageMessage?.caption ||
|
|
342
|
+
msg.message?.videoMessage?.caption ||
|
|
343
|
+
msg.message?.buttonsResponseMessage?.selectedDisplayText ||
|
|
344
|
+
msg.message?.listResponseMessage?.title ||
|
|
345
|
+
msg.message?.ephemeralMessage?.message?.conversation ||
|
|
346
|
+
msg.message?.viewOnceMessage?.message?.conversation ||
|
|
347
|
+
'';
|
|
348
|
+
|
|
349
|
+
// Skip if this is the bot's own reply
|
|
350
|
+
if (msg.key.fromMe && !isLID) {
|
|
351
|
+
log('whatsapp', 'Skipping bot own reply (fromMe=true, not LID)', 'gray');
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Skip if message matches last bot reply (prevent loop)
|
|
356
|
+
if (messageText === lastBotReply && lastBotReply !== '') {
|
|
357
|
+
log('whatsapp', 'Skipping duplicate bot reply (matches last sent)', 'gray');
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Handle LID format (new WhatsApp format)
|
|
362
|
+
if (isLID && !msg.key.fromMe) {
|
|
363
|
+
log('whatsapp', 'LID format detected, fromMe=false, blocked', 'yellow');
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const sender = remoteJid.split('@')[0].split(':')[0];
|
|
368
|
+
const allowedNumbers = config.whatsappAllowedNumbers || [];
|
|
369
|
+
|
|
370
|
+
// Log incoming number
|
|
371
|
+
log('whatsapp', `Incoming from: +${sender}, allowed: ${JSON.stringify(allowedNumbers)}`, 'gray');
|
|
372
|
+
|
|
373
|
+
// Access control - skip if fromMe + LID (own conversation)
|
|
374
|
+
if (!(msg.key.fromMe && isLID) && allowedNumbers.length > 0 && !allowedNumbers.some(n => numberMatches(n, sender))) {
|
|
375
|
+
log('whatsapp', `blocked message from +${sender} (not in allowed list)`, 'yellow');
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!messageText) {
|
|
380
|
+
log('whatsapp', `Message without text content. Keys: ${Object.keys(msg.message || {}).join(', ')}`, 'gray');
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const ownNumber = sock.user?.id?.split(':')[0].replace('@s.whatsapp.net', '') || 'unknown';
|
|
385
|
+
log('whatsapp', `Inbound message +${sender} -> +${ownNumber} (${messageText.length} chars)`, 'cyan');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const { sendMessage } = require('../utils/api');
|
|
389
|
+
const { getMemoryPrompt, extractMemoryFromMessage, addMemoryEntry } = require('../utils/memory');
|
|
390
|
+
const { getConfig } = require('../utils/config');
|
|
391
|
+
const cfg = getConfig();
|
|
392
|
+
|
|
393
|
+
log('whatsapp', 'Sending to AI provider...', 'cyan');
|
|
394
|
+
|
|
395
|
+
// Use WhatsApp-specific conversation ID for persistent history
|
|
396
|
+
const conversationId = `whatsapp_${sender.replace(/\D/g, '')}`;
|
|
397
|
+
|
|
398
|
+
// Use same botId as terminal for shared memory
|
|
399
|
+
const botId = 'universal-provider';
|
|
400
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
401
|
+
|
|
402
|
+
// WhatsApp system prompt with memory
|
|
403
|
+
let systemPrompt = `You are a helpful WhatsApp assistant. Keep responses concise and friendly. Use emojis when appropriate. If users ask for file operations or system commands, politely explain that those features are available in the terminal version.`;
|
|
404
|
+
|
|
405
|
+
if (memoryPrompt) {
|
|
406
|
+
systemPrompt += '\n\n' + memoryPrompt;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const response = await sendMessage(cfg.providerApiKey || cfg.apiKey || '', botId, messageText, conversationId, systemPrompt);
|
|
410
|
+
const reply = response?.reply || response?.message || '';
|
|
411
|
+
|
|
412
|
+
if (reply) {
|
|
413
|
+
log('whatsapp', 'Sending reply...', 'cyan');
|
|
414
|
+
|
|
415
|
+
await sock.sendMessage(msg.key.remoteJid, { text: reply });
|
|
416
|
+
|
|
417
|
+
// Store last bot reply to prevent loop
|
|
418
|
+
lastBotReply = reply;
|
|
419
|
+
|
|
420
|
+
log('whatsapp', `Reply sent (${reply.length} chars)`, 'green');
|
|
421
|
+
|
|
422
|
+
// Extract and save memory from user message
|
|
423
|
+
const memoryEntries = extractMemoryFromMessage(messageText);
|
|
424
|
+
for (const entry of memoryEntries) {
|
|
425
|
+
addMemoryEntry(botId, entry.key, entry.value);
|
|
426
|
+
log('whatsapp', `Memory saved: ${entry.key} = ${entry.value}`, 'gray');
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
log('whatsapp', 'No reply from provider', 'yellow');
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
log('whatsapp', `Provider error: ${err.message}`, 'red');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
sock.ev.on('creds.update', saveCreds);
|
|
438
|
+
|
|
439
|
+
} catch (err) {
|
|
440
|
+
log('whatsapp', `Failed to start: ${err.message}`, 'red');
|
|
441
|
+
log('whatsapp', 'Retrying in 10s...', 'yellow');
|
|
442
|
+
setTimeout(() => startWhatsAppProvider(sessionDir, config), 10000);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
async function startDiscordProvider(config) {
|
|
448
|
+
try {
|
|
449
|
+
let Discord;
|
|
450
|
+
try {
|
|
451
|
+
Discord = require('discord.js');
|
|
452
|
+
} catch (e) {
|
|
453
|
+
log('discord', 'discord.js not installed. Run: npm install discord.js', 'red');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const { Client, GatewayIntentBits, Events } = Discord;
|
|
458
|
+
const client = new Client({
|
|
459
|
+
intents: [
|
|
460
|
+
GatewayIntentBits.Guilds,
|
|
461
|
+
GatewayIntentBits.GuildMessages,
|
|
462
|
+
GatewayIntentBits.MessageContent,
|
|
463
|
+
GatewayIntentBits.DirectMessages,
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const botId = config.discordBotId || `discord_${Date.now()}`;
|
|
468
|
+
global.discordClient = client;
|
|
469
|
+
global.discordBotId = botId;
|
|
470
|
+
|
|
471
|
+
client.once(Events.ClientReady, (c) => {
|
|
472
|
+
log('discord', `bot online as ${c.user.tag}`, 'green');
|
|
473
|
+
config.discordBotId = botId;
|
|
474
|
+
saveConfig(config);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
client.on(Events.MessageCreate, async (message) => {
|
|
478
|
+
if (message.author.bot) return;
|
|
479
|
+
const chatId = message.channel.id;
|
|
480
|
+
const allowedChats = config.discordAllowedChats || [];
|
|
481
|
+
if (allowedChats.length > 0 && !allowedChats.includes(chatId)) return;
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const response = await callProviderForGateway(config, message.content, {
|
|
485
|
+
chatId, userId: message.author.id, channel: 'discord'
|
|
486
|
+
});
|
|
487
|
+
if (response) {
|
|
488
|
+
// Discord 2000 char limit
|
|
489
|
+
const chunks = response.match(/.{1,1900}/gs) || [response];
|
|
490
|
+
for (const chunk of chunks) {
|
|
491
|
+
await message.reply(chunk);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch (e) {
|
|
495
|
+
log('discord', `error: ${e.message}`, 'red');
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
await client.login(config.discordToken);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
log('discord', `failed: ${e.message}`, 'red');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function startSlackProvider(config) {
|
|
506
|
+
try {
|
|
507
|
+
const { WebClient } = require('@slack/web-api');
|
|
508
|
+
const client = new WebClient(config.slackToken);
|
|
509
|
+
|
|
510
|
+
const botId = config.slackBotId || `slack_${Date.now()}`;
|
|
511
|
+
global.slackClient = client;
|
|
512
|
+
global.slackBotId = botId;
|
|
513
|
+
|
|
514
|
+
const authResult = await client.auth.test();
|
|
515
|
+
log('slack', `bot online as ${authResult.user}`, 'green');
|
|
516
|
+
config.slackBotId = botId;
|
|
517
|
+
saveConfig(config);
|
|
518
|
+
|
|
519
|
+
// Slack RTM veya events API kullan
|
|
520
|
+
// Not: Tam real-time icin Bolt framework gerekli, su an basit polling
|
|
521
|
+
log('slack', 'polling mode active (use Bolt SDK for real-time)', 'gray');
|
|
522
|
+
} catch (e) {
|
|
523
|
+
log('slack', `failed: ${e.message}`, 'red');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function callProviderForGateway(config, message, context) {
|
|
528
|
+
// Gateway icin LLM API cagir
|
|
529
|
+
const apiMessages = [{ role: 'user', content: message }];
|
|
530
|
+
return new Promise((resolve, reject) => {
|
|
531
|
+
const providerUrl = config.providerUrl;
|
|
532
|
+
const providerApiKey = config.providerApiKey;
|
|
533
|
+
const model = config.providerModel;
|
|
534
|
+
|
|
535
|
+
const isMM = providerUrl && (providerUrl.includes('minimax') || providerUrl.includes('minimaxi'));
|
|
536
|
+
const endpoint = isMM
|
|
537
|
+
? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
|
|
538
|
+
: `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
539
|
+
|
|
540
|
+
const data = JSON.stringify({
|
|
541
|
+
model, messages: apiMessages, temperature: 0.7, max_tokens: 2048
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const url = new URL(endpoint);
|
|
545
|
+
const req = https.request({
|
|
546
|
+
hostname: url.hostname,
|
|
547
|
+
port: url.port || 443,
|
|
548
|
+
path: url.pathname,
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: {
|
|
551
|
+
'Authorization': `Bearer ${providerApiKey}`,
|
|
552
|
+
'Content-Type': 'application/json',
|
|
553
|
+
'Content-Length': Buffer.byteLength(data)
|
|
554
|
+
},
|
|
555
|
+
timeout: 30000
|
|
556
|
+
}, (res) => {
|
|
557
|
+
let body = '';
|
|
558
|
+
res.on('data', c => body += c);
|
|
559
|
+
res.on('end', () => {
|
|
560
|
+
try {
|
|
561
|
+
const parsed = JSON.parse(body);
|
|
562
|
+
const reply = parsed.choices?.[0]?.message?.content;
|
|
563
|
+
resolve(reply || 'Cevap uretilemedi');
|
|
564
|
+
} catch (e) {
|
|
565
|
+
reject(new Error('Parse hatasi: ' + e.message));
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
req.on('error', reject);
|
|
570
|
+
req.on('timeout', () => req.destroy());
|
|
571
|
+
req.write(data);
|
|
572
|
+
req.end();
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function startTelegramProvider(config) {
|
|
577
|
+
try {
|
|
578
|
+
// Lazy load node-telegram-bot-api
|
|
579
|
+
let TelegramBot;
|
|
580
|
+
try {
|
|
581
|
+
TelegramBot = require('node-telegram-bot-api');
|
|
582
|
+
} catch (err) {
|
|
583
|
+
log('telegram', 'node-telegram-bot-api not installed', 'red');
|
|
584
|
+
log('telegram', 'Install with: npm install -g node-telegram-bot-api', 'yellow');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const bot = new TelegramBot(config.telegramToken, { polling: true });
|
|
589
|
+
|
|
590
|
+
log('telegram', 'Bot started successfully', 'green');
|
|
591
|
+
log('telegram', 'Listening for messages...', 'cyan');
|
|
592
|
+
|
|
593
|
+
bot.on('message', async (msg) => {
|
|
594
|
+
const chatId = msg.chat.id;
|
|
595
|
+
const messageText = msg.text;
|
|
596
|
+
|
|
597
|
+
if (!messageText) {
|
|
598
|
+
log('telegram', `Message without text from chat ${chatId}`, 'gray');
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Access control - check allowed chats
|
|
603
|
+
const allowedChats = config.telegramAllowedChats || [];
|
|
604
|
+
if (allowedChats.length > 0 && !allowedChats.includes(String(chatId))) {
|
|
605
|
+
log('telegram', `Blocked message from chat ${chatId} (not in allowed list)`, 'yellow');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
log('telegram', `Inbound message from chat ${chatId} (${messageText.length} chars)`, 'cyan');
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const { sendMessage } = require('../utils/api');
|
|
613
|
+
const { getMemoryPrompt, extractMemoryFromMessage, addMemoryEntry } = require('../utils/memory');
|
|
614
|
+
const { getConfig } = require('../utils/config');
|
|
615
|
+
const cfg = getConfig();
|
|
616
|
+
|
|
617
|
+
log('telegram', 'Sending to AI provider...', 'cyan');
|
|
618
|
+
|
|
619
|
+
// Use Telegram-specific conversation ID for persistent history
|
|
620
|
+
const conversationId = `telegram_${chatId}`;
|
|
621
|
+
|
|
622
|
+
// Use same botId as terminal for shared memory
|
|
623
|
+
const botId = 'universal-provider';
|
|
624
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
625
|
+
|
|
626
|
+
// Telegram system prompt with memory
|
|
627
|
+
let systemPrompt = `You are a helpful Telegram assistant. Keep responses concise and friendly. Use emojis when appropriate. If users ask for file operations or system commands, politely explain that those features are available in the terminal version.`;
|
|
628
|
+
|
|
629
|
+
if (memoryPrompt) {
|
|
630
|
+
systemPrompt += '\n\n' + memoryPrompt;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const response = await sendMessage(cfg.providerApiKey || cfg.apiKey || '', botId, messageText, conversationId, systemPrompt);
|
|
634
|
+
const reply = response?.reply || response?.message || '';
|
|
635
|
+
|
|
636
|
+
if (reply) {
|
|
637
|
+
log('telegram', 'Sending reply...', 'cyan');
|
|
638
|
+
|
|
639
|
+
await bot.sendMessage(chatId, reply);
|
|
640
|
+
|
|
641
|
+
log('telegram', `Reply sent (${reply.length} chars)`, 'green');
|
|
642
|
+
|
|
643
|
+
// Extract and save memory from user message
|
|
644
|
+
const memoryEntries = extractMemoryFromMessage(messageText);
|
|
645
|
+
for (const entry of memoryEntries) {
|
|
646
|
+
addMemoryEntry(botId, entry.key, entry.value);
|
|
647
|
+
log('telegram', `Memory saved: ${entry.key} = ${entry.value}`, 'gray');
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
log('telegram', 'No reply from provider', 'yellow');
|
|
651
|
+
}
|
|
652
|
+
} catch (err) {
|
|
653
|
+
log('telegram', `Provider error: ${err.message}`, 'red');
|
|
654
|
+
|
|
655
|
+
// Send error message to user
|
|
656
|
+
try {
|
|
657
|
+
await bot.sendMessage(chatId, '❌ Bir hata oluştu. Lütfen tekrar deneyin.');
|
|
658
|
+
} catch (sendErr) {
|
|
659
|
+
log('telegram', `Failed to send error message: ${sendErr.message}`, 'red');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
bot.on('polling_error', (error) => {
|
|
665
|
+
log('telegram', `Polling error: ${error.message}`, 'red');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Store bot globally for HTTP endpoint
|
|
669
|
+
global.telegramBot = bot;
|
|
670
|
+
|
|
671
|
+
} catch (err) {
|
|
672
|
+
log('telegram', `Failed to start: ${err.message}`, 'red');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function startSignalProvider(config) {
|
|
677
|
+
const baseUrl = config.signalHttpUrl;
|
|
678
|
+
const account = config.signalAccount;
|
|
679
|
+
const mode = config.signalApiMode || 'auto';
|
|
680
|
+
|
|
681
|
+
// Detect API mode
|
|
682
|
+
let detectedMode = mode;
|
|
683
|
+
if (mode === 'auto') {
|
|
684
|
+
try {
|
|
685
|
+
const nativeRes = await fetch(`${baseUrl}/api/v1/check`, { signal: AbortSignal.timeout(5000) });
|
|
686
|
+
if (nativeRes.ok) {
|
|
687
|
+
detectedMode = 'native';
|
|
688
|
+
log('signal', 'detected native JSON-RPC API', 'gray');
|
|
689
|
+
}
|
|
690
|
+
} catch {}
|
|
691
|
+
if (detectedMode === 'auto') {
|
|
692
|
+
try {
|
|
693
|
+
const containerRes = await fetch(`${baseUrl}/v1/about`, { signal: AbortSignal.timeout(5000) });
|
|
694
|
+
if (containerRes.ok) {
|
|
695
|
+
detectedMode = 'container';
|
|
696
|
+
log('signal', 'detected container REST API', 'gray');
|
|
697
|
+
}
|
|
698
|
+
} catch {}
|
|
699
|
+
}
|
|
700
|
+
if (detectedMode === 'auto') {
|
|
701
|
+
log('signal', 'API not reachable, will retry', 'yellow');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (detectedMode === 'auto' || detectedMode === 'none') {
|
|
706
|
+
log('signal', 'API unreachable, will start daemon on demand', 'gray');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
global.signalProvider = { baseUrl, account, mode: detectedMode };
|
|
711
|
+
|
|
712
|
+
// Start event stream for incoming messages (native mode uses SSE)
|
|
713
|
+
if (detectedMode === 'native') {
|
|
714
|
+
startSignalEventStream(baseUrl, account, config);
|
|
715
|
+
} else if (detectedMode === 'container') {
|
|
716
|
+
startSignalWebSocket(baseUrl, account, config);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
log('signal', 'provider ready', 'green');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function startSignalEventStream(baseUrl, account, config) {
|
|
723
|
+
const eventsUrl = `${baseUrl}/api/v1/events?account=${encodeURIComponent(account)}`;
|
|
724
|
+
|
|
725
|
+
const connect = async () => {
|
|
726
|
+
try {
|
|
727
|
+
const response = await fetch(eventsUrl, {
|
|
728
|
+
signal: AbortSignal.timeout(300000),
|
|
729
|
+
headers: { 'Accept': 'text/event-stream' },
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
if (!response.ok) {
|
|
733
|
+
throw new Error(`SSE HTTP ${response.status}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
log('signal', 'SSE event stream connected', 'cyan');
|
|
737
|
+
const reader = response.body.getReader();
|
|
738
|
+
const decoder = new TextDecoder();
|
|
739
|
+
let buffer = '';
|
|
740
|
+
|
|
741
|
+
while (true) {
|
|
742
|
+
const { done, value } = await reader.read();
|
|
743
|
+
if (done) break;
|
|
744
|
+
|
|
745
|
+
buffer += decoder.decode(value, { stream: true });
|
|
746
|
+
const lines = buffer.split('\n');
|
|
747
|
+
buffer = lines.pop() || '';
|
|
748
|
+
|
|
749
|
+
let eventType = '';
|
|
750
|
+
let eventData = '';
|
|
751
|
+
|
|
752
|
+
for (const line of lines) {
|
|
753
|
+
if (line.startsWith('event: ')) {
|
|
754
|
+
eventType = line.slice(7).trim();
|
|
755
|
+
} else if (line.startsWith('data: ')) {
|
|
756
|
+
eventData = line.slice(6).trim();
|
|
757
|
+
} else if (line === '' && eventData) {
|
|
758
|
+
processSignalEvent(eventType, eventData, config);
|
|
759
|
+
eventType = '';
|
|
760
|
+
eventData = '';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
if (err.name === 'AbortError') {
|
|
766
|
+
log('signal', 'SSE stream timeout, reconnecting...', 'yellow');
|
|
767
|
+
} else {
|
|
768
|
+
log('signal', `SSE error: ${err.message}, reconnecting in 10s...`, 'yellow');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
setTimeout(connect, 10000);
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
connect();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function startSignalWebSocket(baseUrl, account, config) {
|
|
779
|
+
const wsUrl = baseUrl.replace(/^http/, 'ws') + `/v1/receive/${encodeURIComponent(account)}`;
|
|
780
|
+
|
|
781
|
+
const connect = async () => {
|
|
782
|
+
try {
|
|
783
|
+
let WebSocket;
|
|
784
|
+
try {
|
|
785
|
+
WebSocket = require('ws');
|
|
786
|
+
} catch {
|
|
787
|
+
log('signal', 'ws module not available, falling back to polling', 'yellow');
|
|
788
|
+
startSignalPolling(baseUrl, account, config);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const ws = new WebSocket(wsUrl);
|
|
793
|
+
|
|
794
|
+
ws.on('open', () => {
|
|
795
|
+
log('signal', 'WebSocket event stream connected', 'cyan');
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
ws.on('message', (data) => {
|
|
799
|
+
try {
|
|
800
|
+
const envelope = JSON.parse(data.toString());
|
|
801
|
+
processSignalEnvelope(envelope, config);
|
|
802
|
+
} catch (err) {
|
|
803
|
+
log('signal', `Failed to parse WS message: ${err.message}`, 'red');
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
ws.on('close', (code) => {
|
|
808
|
+
log('signal', `WebSocket closed (code ${code}), reconnecting in 10s...`, 'yellow');
|
|
809
|
+
setTimeout(connect, 10000);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
ws.on('error', (err) => {
|
|
813
|
+
log('signal', `WebSocket error: ${err.message}`, 'red');
|
|
814
|
+
ws.close();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
global.signalProvider.ws = ws;
|
|
818
|
+
|
|
819
|
+
} catch (err) {
|
|
820
|
+
log('signal', `WebSocket failed: ${err.message}, retrying in 10s...`, 'yellow');
|
|
821
|
+
setTimeout(connect, 10000);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
connect();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Fallback polling for container mode when ws is not available
|
|
829
|
+
let signalPollingInterval = null;
|
|
830
|
+
function startSignalPolling(baseUrl, account, config) {
|
|
831
|
+
if (signalPollingInterval) return;
|
|
832
|
+
log('signal', 'starting polling fallback (30s interval)', 'gray');
|
|
833
|
+
signalPollingInterval = setInterval(async () => {
|
|
834
|
+
try {
|
|
835
|
+
const res = await fetch(`${baseUrl}/v1/receive/${encodeURIComponent(account)}`, {
|
|
836
|
+
signal: AbortSignal.timeout(15000),
|
|
837
|
+
});
|
|
838
|
+
if (res.ok) {
|
|
839
|
+
const envelopes = await res.json();
|
|
840
|
+
if (Array.isArray(envelopes)) {
|
|
841
|
+
for (const envelope of envelopes) {
|
|
842
|
+
processSignalEnvelope(envelope, config);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch {}
|
|
847
|
+
}, 30000);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function processSignalEvent(eventType, eventData, config) {
|
|
851
|
+
try {
|
|
852
|
+
const data = JSON.parse(eventData);
|
|
853
|
+
if (data.envelope) {
|
|
854
|
+
processSignalEnvelope(data.envelope, config);
|
|
855
|
+
}
|
|
856
|
+
} catch (err) {
|
|
857
|
+
log('signal', `Event parse error: ${err.message}`, 'red');
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function processSignalEnvelope(envelope, config) {
|
|
862
|
+
try {
|
|
863
|
+
const dataMessage = envelope.dataMessage || envelope.syncMessage?.sent?.message;
|
|
864
|
+
if (!dataMessage) return;
|
|
865
|
+
|
|
866
|
+
const sender = envelope.sourceUuid || envelope.sourceNumber || envelope.sourceName;
|
|
867
|
+
if (!sender) return;
|
|
868
|
+
|
|
869
|
+
// Skip self-messages
|
|
870
|
+
if (envelope.sourceNumber === config.signalAccount) return;
|
|
871
|
+
|
|
872
|
+
const messageText = dataMessage.message || '';
|
|
873
|
+
if (!messageText.trim()) return;
|
|
874
|
+
|
|
875
|
+
log('signal', `Inbound from ${sender}: "${messageText.slice(0, 80)}"`, 'cyan');
|
|
876
|
+
|
|
877
|
+
// Check DM policy
|
|
878
|
+
const dmPolicy = config.signalDmPolicy || 'pairing';
|
|
879
|
+
if (dmPolicy === 'disabled') return;
|
|
880
|
+
|
|
881
|
+
// Get AI response
|
|
882
|
+
try {
|
|
883
|
+
const { sendMessage } = require('../utils/api');
|
|
884
|
+
const { getMemoryPrompt, extractMemoryFromMessage, addMemoryEntry } = require('../utils/memory');
|
|
885
|
+
const { getConfig } = require('../utils/config');
|
|
886
|
+
const cfg = getConfig();
|
|
887
|
+
|
|
888
|
+
const conversationId = `signal_${sender}`;
|
|
889
|
+
const botId = 'universal-provider';
|
|
890
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
891
|
+
|
|
892
|
+
let systemPrompt = `You are a helpful Signal assistant. Keep responses concise.`;
|
|
893
|
+
if (memoryPrompt) {
|
|
894
|
+
systemPrompt += '\n\n' + memoryPrompt;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const response = await sendMessage(
|
|
898
|
+
cfg.providerApiKey || cfg.apiKey || '', botId, messageText,
|
|
899
|
+
conversationId, systemPrompt
|
|
900
|
+
);
|
|
901
|
+
const reply = response?.reply || response?.message || '';
|
|
902
|
+
|
|
903
|
+
if (reply) {
|
|
904
|
+
await sendSignalMessage(config, sender, reply);
|
|
905
|
+
log('signal', `Reply sent to ${sender} (${reply.length} chars)`, 'green');
|
|
906
|
+
|
|
907
|
+
const memoryEntries = extractMemoryFromMessage(messageText);
|
|
908
|
+
for (const entry of memoryEntries) {
|
|
909
|
+
addMemoryEntry(botId, entry.key, entry.value);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} catch (err) {
|
|
913
|
+
log('signal', `Response error: ${err.message}`, 'red');
|
|
914
|
+
}
|
|
915
|
+
} catch (err) {
|
|
916
|
+
log('signal', `Envelope error: ${err.message}`, 'red');
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function sendSignalMessage(config, recipient, message) {
|
|
921
|
+
const baseUrl = config.signalHttpUrl;
|
|
922
|
+
const account = config.signalAccount;
|
|
923
|
+
const mode = config.signalApiMode || 'auto';
|
|
924
|
+
|
|
925
|
+
if (mode === 'native' || mode === 'auto') {
|
|
926
|
+
// Try native JSON-RPC first
|
|
927
|
+
const rpcPayload = {
|
|
928
|
+
jsonrpc: '2.0',
|
|
929
|
+
method: 'send',
|
|
930
|
+
params: {
|
|
931
|
+
recipient: [recipient],
|
|
932
|
+
message,
|
|
933
|
+
account,
|
|
934
|
+
},
|
|
935
|
+
id: Date.now(),
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
const res = await fetch(`${baseUrl}/api/v1/rpc`, {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: { 'Content-Type': 'application/json' },
|
|
942
|
+
body: JSON.stringify(rpcPayload),
|
|
943
|
+
signal: AbortSignal.timeout(30000),
|
|
944
|
+
});
|
|
945
|
+
if (res.ok) return;
|
|
946
|
+
} catch {}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Fallback to container mode
|
|
950
|
+
const containerPayload = {
|
|
951
|
+
message,
|
|
952
|
+
number: account,
|
|
953
|
+
recipients: [recipient],
|
|
954
|
+
text_mode: 'normal',
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const res = await fetch(`${baseUrl}/v2/send`, {
|
|
958
|
+
method: 'POST',
|
|
959
|
+
headers: { 'Content-Type': 'application/json' },
|
|
960
|
+
body: JSON.stringify(containerPayload),
|
|
961
|
+
signal: AbortSignal.timeout(30000),
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
if (!res.ok) {
|
|
965
|
+
const text = await res.text();
|
|
966
|
+
throw new Error(`Signal send failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// IRC provider
|
|
971
|
+
let ircClient = null;
|
|
972
|
+
let ircReconnectTimer = null;
|
|
973
|
+
|
|
974
|
+
async function startIrcProvider(config) {
|
|
975
|
+
if (ircClient) {
|
|
976
|
+
log('irc', 'already connected', 'yellow');
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const net = require('net');
|
|
981
|
+
const tls = require('tls');
|
|
982
|
+
|
|
983
|
+
const host = config.ircHost;
|
|
984
|
+
const port = config.ircPort || 6697;
|
|
985
|
+
const useTls = config.ircTls !== false;
|
|
986
|
+
const nick = config.ircNick;
|
|
987
|
+
const username = config.ircUsername || nick;
|
|
988
|
+
const realname = config.ircRealname || 'NatureCo';
|
|
989
|
+
const password = config.ircPassword || '';
|
|
990
|
+
const channels = config.ircChannels || [];
|
|
991
|
+
const nickservEnabled = config.ircNickservEnabled !== false;
|
|
992
|
+
const nickservPassword = config.ircNickservPassword || '';
|
|
993
|
+
|
|
994
|
+
let buffer = '';
|
|
995
|
+
let currentNick = nick;
|
|
996
|
+
let registered = false;
|
|
997
|
+
let quitRequested = false;
|
|
998
|
+
|
|
999
|
+
const connect = () => {
|
|
1000
|
+
if (quitRequested) return;
|
|
1001
|
+
|
|
1002
|
+
registered = false;
|
|
1003
|
+
const socket = useTls
|
|
1004
|
+
? tls.connect({ host, port, servername: host })
|
|
1005
|
+
: net.createConnection({ host, port });
|
|
1006
|
+
|
|
1007
|
+
socket.setEncoding('utf8');
|
|
1008
|
+
|
|
1009
|
+
const sendRaw = (line) => {
|
|
1010
|
+
socket.write(line + '\r\n');
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
socket.once('connect', () => {
|
|
1014
|
+
log('irc', `connected to ${host}:${port}`, 'green');
|
|
1015
|
+
if (password) sendRaw(`PASS ${password}`);
|
|
1016
|
+
sendRaw(`NICK ${currentNick}`);
|
|
1017
|
+
sendRaw(`USER ${username} 0 * :${realname}`);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
socket.on('data', (chunk) => {
|
|
1021
|
+
buffer += chunk;
|
|
1022
|
+
const lines = buffer.split('\r\n');
|
|
1023
|
+
buffer = lines.pop() || '';
|
|
1024
|
+
|
|
1025
|
+
for (const line of lines) {
|
|
1026
|
+
if (!line.trim()) continue;
|
|
1027
|
+
|
|
1028
|
+
// PING handler
|
|
1029
|
+
if (line.startsWith('PING ')) {
|
|
1030
|
+
sendRaw('PONG ' + line.slice(5));
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Parse IRC message
|
|
1035
|
+
const parsed = parseIrcLine(line);
|
|
1036
|
+
if (!parsed) continue;
|
|
1037
|
+
|
|
1038
|
+
// Track nick changes
|
|
1039
|
+
if (parsed.command === 'NICK' && parsed.prefixNick === currentNick) {
|
|
1040
|
+
currentNick = parsed.trailing || parsed.params[0];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Welcome — server ready
|
|
1044
|
+
if (parsed.command === '001') {
|
|
1045
|
+
registered = true;
|
|
1046
|
+
log('irc', `registered as ${currentNick}`, 'cyan');
|
|
1047
|
+
|
|
1048
|
+
// NickServ auth
|
|
1049
|
+
if (nickservEnabled && nickservPassword) {
|
|
1050
|
+
sendRaw(`PRIVMSG NickServ :IDENTIFY ${nickservPassword}`);
|
|
1051
|
+
log('irc', 'NickServ IDENTIFY sent', 'gray');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Join channels
|
|
1055
|
+
for (const ch of channels) {
|
|
1056
|
+
sendRaw(`JOIN ${ch.startsWith('#') ? ch : '#' + ch}`);
|
|
1057
|
+
log('irc', `joining ${ch.startsWith('#') ? ch : '#' + ch}`, 'gray');
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
global.ircClient = { sendRaw, isReady: () => registered };
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Handle PRIVMSG (incoming messages)
|
|
1065
|
+
if (parsed.command === 'PRIVMSG' && registered) {
|
|
1066
|
+
const senderNick = parsed.prefixNick;
|
|
1067
|
+
if (senderNick === currentNick) continue; // skip self
|
|
1068
|
+
|
|
1069
|
+
const target = parsed.params[0];
|
|
1070
|
+
const text = parsed.trailing || '';
|
|
1071
|
+
|
|
1072
|
+
if (!text.trim()) continue;
|
|
1073
|
+
|
|
1074
|
+
const isChannel = target.startsWith('#');
|
|
1075
|
+
log('irc', `${isChannel ? target : senderNick}: ${text.slice(0, 100)}`, 'cyan');
|
|
1076
|
+
|
|
1077
|
+
// In DM or channel message with mention
|
|
1078
|
+
const isDirect = !isChannel;
|
|
1079
|
+
if (!isDirect && !text.toLowerCase().includes(currentNick.toLowerCase())) continue;
|
|
1080
|
+
|
|
1081
|
+
processIrcMessage(config, senderNick, target, text, socket);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
socket.on('close', () => {
|
|
1087
|
+
global.ircClient = null;
|
|
1088
|
+
if (!quitRequested) {
|
|
1089
|
+
log('irc', 'connection lost, reconnecting in 10s...', 'yellow');
|
|
1090
|
+
ircReconnectTimer = setTimeout(connect, 10000);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
socket.on('error', (err) => {
|
|
1095
|
+
log('irc', `socket error: ${err.message}`, 'red');
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
ircClient = socket;
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
connect();
|
|
1102
|
+
}
|
|
1103
|
+
function parseIrcLine(line) {
|
|
1104
|
+
const result = { raw: line, prefix: null, prefixNick: null, command: '', params: [], trailing: '' };
|
|
1105
|
+
let rest = line;
|
|
1106
|
+
|
|
1107
|
+
if (rest.startsWith(':')) {
|
|
1108
|
+
const spaceIdx = rest.indexOf(' ');
|
|
1109
|
+
if (spaceIdx === -1) return null;
|
|
1110
|
+
result.prefix = rest.slice(1, spaceIdx);
|
|
1111
|
+
const bangIdx = result.prefix.indexOf('!');
|
|
1112
|
+
result.prefixNick = bangIdx !== -1 ? result.prefix.slice(0, bangIdx) : result.prefix;
|
|
1113
|
+
rest = rest.slice(spaceIdx + 1);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const spaceIdx = rest.indexOf(' ');
|
|
1117
|
+
if (spaceIdx === -1) {
|
|
1118
|
+
result.command = rest;
|
|
1119
|
+
return result;
|
|
1120
|
+
}
|
|
1121
|
+
result.command = rest.slice(0, spaceIdx);
|
|
1122
|
+
rest = rest.slice(spaceIdx + 1);
|
|
1123
|
+
|
|
1124
|
+
// Trailing
|
|
1125
|
+
const trailIdx = rest.indexOf(' :');
|
|
1126
|
+
if (trailIdx !== -1) {
|
|
1127
|
+
result.trailing = rest.slice(trailIdx + 2);
|
|
1128
|
+
rest = rest.slice(0, trailIdx);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
result.params = rest.split(' ').filter(Boolean);
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function processIrcMessage(config, sender, target, text, socket) {
|
|
1136
|
+
try {
|
|
1137
|
+
const { sendMessage } = require('../utils/api');
|
|
1138
|
+
const { getMemoryPrompt } = require('../utils/memory');
|
|
1139
|
+
const { getConfig } = require('../utils/config');
|
|
1140
|
+
const cfg = getConfig();
|
|
1141
|
+
|
|
1142
|
+
const conversationId = `irc_${sender}_${target}`;
|
|
1143
|
+
const botId = 'universal-provider';
|
|
1144
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
1145
|
+
|
|
1146
|
+
let systemPrompt = `You are a helpful IRC assistant. Keep responses concise. Use IRC-friendly formatting.`;
|
|
1147
|
+
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1148
|
+
|
|
1149
|
+
const response = await sendMessage(
|
|
1150
|
+
cfg.providerApiKey || cfg.apiKey || '', botId, text,
|
|
1151
|
+
conversationId, systemPrompt
|
|
1152
|
+
);
|
|
1153
|
+
const reply = response?.reply || response?.message || '';
|
|
1154
|
+
|
|
1155
|
+
if (reply) {
|
|
1156
|
+
const ircTarget = target.startsWith('#') ? target : sender;
|
|
1157
|
+
sendIrcMessage(socket, ircTarget, reply);
|
|
1158
|
+
log('irc', `Reply sent to ${ircTarget} (${reply.length} chars)`, 'green');
|
|
1159
|
+
}
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
log('irc', `Response error: ${err.message}`, 'red');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function sendIrcMessage(socket, target, text) {
|
|
1166
|
+
if (!socket) return;
|
|
1167
|
+
const maxLen = 400;
|
|
1168
|
+
const lines = text.split('\n');
|
|
1169
|
+
for (const line of lines) {
|
|
1170
|
+
const cleanLine = line.replace(/\r/g, '').trim();
|
|
1171
|
+
if (!cleanLine) continue;
|
|
1172
|
+
if (cleanLine.length <= maxLen) {
|
|
1173
|
+
socket.write(`PRIVMSG ${target} :${cleanLine}\r\n`);
|
|
1174
|
+
} else {
|
|
1175
|
+
const words = cleanLine.split(' ');
|
|
1176
|
+
let chunk = '';
|
|
1177
|
+
for (const word of words) {
|
|
1178
|
+
if (chunk.length + word.length + 1 > maxLen) {
|
|
1179
|
+
socket.write(`PRIVMSG ${target} :${chunk}\r\n`);
|
|
1180
|
+
chunk = word;
|
|
1181
|
+
} else {
|
|
1182
|
+
chunk = chunk ? chunk + ' ' + word : word;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (chunk) socket.write(`PRIVMSG ${target} :${chunk}\r\n`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function stopIrcProvider() {
|
|
1191
|
+
if (ircReconnectTimer) {
|
|
1192
|
+
clearTimeout(ircReconnectTimer);
|
|
1193
|
+
ircReconnectTimer = null;
|
|
1194
|
+
}
|
|
1195
|
+
if (ircClient) {
|
|
1196
|
+
try {
|
|
1197
|
+
ircClient.write('QUIT :Gateway shutting down\r\n');
|
|
1198
|
+
ircClient.end();
|
|
1199
|
+
} catch {}
|
|
1200
|
+
ircClient = null;
|
|
1201
|
+
}
|
|
1202
|
+
global.ircClient = null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Mattermost provider
|
|
1206
|
+
let mattermostWs = null;
|
|
1207
|
+
let mattermostReconnectTimer = null;
|
|
1208
|
+
|
|
1209
|
+
async function startMattermostProvider(config) {
|
|
1210
|
+
const baseUrl = config.mattermostBaseUrl.replace(/\/+$/, '');
|
|
1211
|
+
const token = config.mattermostToken;
|
|
1212
|
+
|
|
1213
|
+
// Fetch bot user info
|
|
1214
|
+
try {
|
|
1215
|
+
const meRes = await fetch(`${baseUrl}/api/v4/users/me`, {
|
|
1216
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
1217
|
+
signal: AbortSignal.timeout(10000),
|
|
1218
|
+
});
|
|
1219
|
+
if (!meRes.ok) {
|
|
1220
|
+
log('mattermost', `API auth failed (HTTP ${meRes.status})`, 'red');
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const me = await meRes.json();
|
|
1224
|
+
log('mattermost', `authenticated as @${me.username} (${me.id})`, 'green');
|
|
1225
|
+
|
|
1226
|
+
global.mattermostProvider = { baseUrl, token, userId: me.id, username: me.username };
|
|
1227
|
+
|
|
1228
|
+
// Connect WebSocket for real-time events
|
|
1229
|
+
connectMattermostWebSocket(baseUrl, token, config);
|
|
1230
|
+
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
log('mattermost', `connection failed: ${err.message}`, 'red');
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function connectMattermostWebSocket(baseUrl, token, config) {
|
|
1237
|
+
const wsUrl = baseUrl.replace(/^http/, 'ws') + '/api/v4/websocket';
|
|
1238
|
+
|
|
1239
|
+
const connect = () => {
|
|
1240
|
+
try {
|
|
1241
|
+
let WebSocket;
|
|
1242
|
+
try {
|
|
1243
|
+
WebSocket = require('ws');
|
|
1244
|
+
} catch {
|
|
1245
|
+
log('mattermost', 'ws module not available', 'yellow');
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const ws = new WebSocket(wsUrl);
|
|
1250
|
+
|
|
1251
|
+
ws.on('open', () => {
|
|
1252
|
+
log('mattermost', 'WebSocket connected', 'cyan');
|
|
1253
|
+
|
|
1254
|
+
// Authenticate
|
|
1255
|
+
ws.send(JSON.stringify({
|
|
1256
|
+
seq: 1,
|
|
1257
|
+
action: 'authentication_challenge',
|
|
1258
|
+
data: { token },
|
|
1259
|
+
}));
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
ws.on('message', (data) => {
|
|
1263
|
+
try {
|
|
1264
|
+
const msg = JSON.parse(data.toString());
|
|
1265
|
+
|
|
1266
|
+
if (msg.event === 'posted') {
|
|
1267
|
+
handleMattermostPost(msg.data, msg.broadcast, config);
|
|
1268
|
+
} else if (msg.event === 'hello') {
|
|
1269
|
+
log('mattermost', 'WebSocket authenticated', 'green');
|
|
1270
|
+
} else if (msg.event === 'user_added' || msg.event === 'user_removed') {
|
|
1271
|
+
// no-op
|
|
1272
|
+
}
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
log('mattermost', `message parse error: ${err.message}`, 'red');
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
ws.on('close', (code) => {
|
|
1279
|
+
log('mattermost', `WebSocket closed (code ${code})`, 'yellow');
|
|
1280
|
+
mattermostWs = null;
|
|
1281
|
+
mattermostReconnectTimer = setTimeout(connect, 10000);
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
ws.on('error', (err) => {
|
|
1285
|
+
log('mattermost', `WebSocket error: ${err.message}`, 'red');
|
|
1286
|
+
ws.close();
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
mattermostWs = ws;
|
|
1290
|
+
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
log('mattermost', `WebSocket connect failed: ${err.message}`, 'red');
|
|
1293
|
+
mattermostReconnectTimer = setTimeout(connect, 30000);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
connect();
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function handleMattermostPost(data, broadcast, config) {
|
|
1301
|
+
try {
|
|
1302
|
+
if (!data?.post) return;
|
|
1303
|
+
const post = typeof data.post === 'string' ? JSON.parse(data.post) : data.post;
|
|
1304
|
+
|
|
1305
|
+
// Skip bot's own messages
|
|
1306
|
+
if (post.user_id === global.mattermostProvider?.userId) return;
|
|
1307
|
+
|
|
1308
|
+
const channelId = broadcast?.channel_id || post.channel_id;
|
|
1309
|
+
if (!channelId) return;
|
|
1310
|
+
|
|
1311
|
+
const messageText = post.message || '';
|
|
1312
|
+
if (!messageText.trim()) return;
|
|
1313
|
+
|
|
1314
|
+
const senderId = post.user_id;
|
|
1315
|
+
log('mattermost', `Inbound from user ${senderId}: "${messageText.slice(0, 80)}"`, 'cyan');
|
|
1316
|
+
|
|
1317
|
+
// Skip system messages
|
|
1318
|
+
if (post.props?.from_webhook) return;
|
|
1319
|
+
|
|
1320
|
+
// Get AI response
|
|
1321
|
+
const { sendMessage } = require('../utils/api');
|
|
1322
|
+
const { getMemoryPrompt } = require('../utils/memory');
|
|
1323
|
+
const { getConfig } = require('../utils/config');
|
|
1324
|
+
const cfg = getConfig();
|
|
1325
|
+
|
|
1326
|
+
const conversationId = `mattermost_${channelId}`;
|
|
1327
|
+
const botId = 'universal-provider';
|
|
1328
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
1329
|
+
|
|
1330
|
+
let systemPrompt = `You are a helpful Mattermost assistant. Keep responses concise.`;
|
|
1331
|
+
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1332
|
+
|
|
1333
|
+
const response = await sendMessage(
|
|
1334
|
+
cfg.providerApiKey || cfg.apiKey || '', botId, messageText,
|
|
1335
|
+
conversationId, systemPrompt
|
|
1336
|
+
);
|
|
1337
|
+
const reply = response?.reply || response?.message || '';
|
|
1338
|
+
|
|
1339
|
+
if (reply) {
|
|
1340
|
+
await sendMattermostMessage(config, channelId, reply);
|
|
1341
|
+
log('mattermost', `Reply sent to channel ${channelId} (${reply.length} chars)`, 'green');
|
|
1342
|
+
}
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
log('mattermost', `Post error: ${err.message}`, 'red');
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function sendMattermostMessage(config, channelId, message) {
|
|
1349
|
+
const baseUrl = config.mattermostBaseUrl.replace(/\/+$/, '');
|
|
1350
|
+
const token = config.mattermostToken;
|
|
1351
|
+
|
|
1352
|
+
const res = await fetch(`${baseUrl}/api/v4/posts`, {
|
|
1353
|
+
method: 'POST',
|
|
1354
|
+
headers: {
|
|
1355
|
+
'Content-Type': 'application/json',
|
|
1356
|
+
'Authorization': `Bearer ${token}`,
|
|
1357
|
+
},
|
|
1358
|
+
body: JSON.stringify({
|
|
1359
|
+
channel_id: channelId,
|
|
1360
|
+
message,
|
|
1361
|
+
}),
|
|
1362
|
+
signal: AbortSignal.timeout(15000),
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
if (!res.ok) {
|
|
1366
|
+
const text = await res.text();
|
|
1367
|
+
throw new Error(`Mattermost send failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function stopMattermostProvider() {
|
|
1372
|
+
if (mattermostReconnectTimer) {
|
|
1373
|
+
clearTimeout(mattermostReconnectTimer);
|
|
1374
|
+
mattermostReconnectTimer = null;
|
|
1375
|
+
}
|
|
1376
|
+
if (mattermostWs) {
|
|
1377
|
+
try { mattermostWs.close(); } catch {}
|
|
1378
|
+
mattermostWs = null;
|
|
1379
|
+
}
|
|
1380
|
+
global.mattermostProvider = null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// iMessage provider
|
|
1384
|
+
let imessagePollInterval = null;
|
|
1385
|
+
let imessageLastMessageId = null;
|
|
1386
|
+
|
|
1387
|
+
async function startImessageProvider(config) {
|
|
1388
|
+
const imsgPath = config.imessageCliPath || findImsgBin();
|
|
1389
|
+
if (!imsgPath || !fs.existsSync(imsgPath)) {
|
|
1390
|
+
log('imessage', 'imsg binary not found, install with: brew install mbilker/imsg/imsg', 'red');
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
log('imessage', `using imsg: ${imsgPath}`, 'gray');
|
|
1395
|
+
global.imessageProvider = { imsgPath };
|
|
1396
|
+
|
|
1397
|
+
// Start polling for new messages
|
|
1398
|
+
imessagePollInterval = setInterval(async () => {
|
|
1399
|
+
try {
|
|
1400
|
+
const result = execSync(`"${imsgPath}" messages --format json --limit 5 2>/dev/null`, {
|
|
1401
|
+
encoding: 'utf-8', timeout: 10000, stdio: 'pipe',
|
|
1402
|
+
});
|
|
1403
|
+
const lines = result.trim().split('\n').filter(Boolean);
|
|
1404
|
+
for (const line of lines) {
|
|
1405
|
+
try {
|
|
1406
|
+
const msg = JSON.parse(line);
|
|
1407
|
+
await processImessageMessage(msg, config);
|
|
1408
|
+
} catch {}
|
|
1409
|
+
}
|
|
1410
|
+
} catch {}
|
|
1411
|
+
}, 15000);
|
|
1412
|
+
|
|
1413
|
+
log('imessage', 'polling started (15s interval)', 'green');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function findImsgBin() {
|
|
1417
|
+
const config = require('../utils/config').getConfig();
|
|
1418
|
+
if (config.imessageCliPath && fs.existsSync(config.imessageCliPath)) return config.imessageCliPath;
|
|
1419
|
+
try {
|
|
1420
|
+
const result = execSync('which imsg 2>/dev/null', { encoding: 'utf-8', timeout: 5000 });
|
|
1421
|
+
const p = result.trim();
|
|
1422
|
+
if (p && fs.existsSync(p)) return p;
|
|
1423
|
+
} catch {}
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async function processImessageMessage(msg, config) {
|
|
1428
|
+
try {
|
|
1429
|
+
// Deduplicate
|
|
1430
|
+
if (msg.id && msg.id === imessageLastMessageId) return;
|
|
1431
|
+
if (msg.id) imessageLastMessageId = msg.id;
|
|
1432
|
+
|
|
1433
|
+
// Skip outgoing messages (from us)
|
|
1434
|
+
if (msg.is_from_me || msg.from_me) return;
|
|
1435
|
+
|
|
1436
|
+
const sender = msg.sender || msg.from || msg.address || '';
|
|
1437
|
+
const text = msg.text || msg.message || '';
|
|
1438
|
+
|
|
1439
|
+
if (!text.trim() || !sender) return;
|
|
1440
|
+
|
|
1441
|
+
log('imessage', `Inbound from ${sender}: "${text.slice(0, 80)}"`, 'cyan');
|
|
1442
|
+
|
|
1443
|
+
const { sendMessage } = require('../utils/api');
|
|
1444
|
+
const { getMemoryPrompt } = require('../utils/memory');
|
|
1445
|
+
const { getConfig } = require('../utils/config');
|
|
1446
|
+
const cfg = getConfig();
|
|
1447
|
+
|
|
1448
|
+
const conversationId = `imessage_${sender}`;
|
|
1449
|
+
const botId = 'universal-provider';
|
|
1450
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
1451
|
+
|
|
1452
|
+
let systemPrompt = `You are a helpful iMessage assistant. Keep responses concise.`;
|
|
1453
|
+
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1454
|
+
|
|
1455
|
+
const response = await sendMessage(
|
|
1456
|
+
cfg.providerApiKey || cfg.apiKey || '', botId, text,
|
|
1457
|
+
conversationId, systemPrompt
|
|
1458
|
+
);
|
|
1459
|
+
const reply = response?.reply || response?.message || '';
|
|
1460
|
+
|
|
1461
|
+
if (reply) {
|
|
1462
|
+
execSync(`"${global.imessageProvider.imsgPath}" send --address "${sender}" --message "${reply.replace(/"/g, '\\"')}" 2>/dev/null`, {
|
|
1463
|
+
timeout: 15000, stdio: 'pipe',
|
|
1464
|
+
});
|
|
1465
|
+
log('imessage', `Reply sent to ${sender} (${reply.length} chars)`, 'green');
|
|
1466
|
+
}
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
log('imessage', `Message error: ${err.message}`, 'red');
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function stopImessageProvider() {
|
|
1473
|
+
if (imessagePollInterval) {
|
|
1474
|
+
clearInterval(imessagePollInterval);
|
|
1475
|
+
imessagePollInterval = null;
|
|
1476
|
+
}
|
|
1477
|
+
global.imessageProvider = null;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// SMS (Twilio) provider
|
|
1481
|
+
let smsReplayCache = new Map();
|
|
1482
|
+
const SMS_RATE_LIMIT_WINDOW = 60000;
|
|
1483
|
+
const SMS_RATE_LIMIT_MAX = 30;
|
|
1484
|
+
const smsRateLimitMap = new Map();
|
|
1485
|
+
|
|
1486
|
+
async function startSmsProvider(config) {
|
|
1487
|
+
log('sms', 'provider initialized for outbound sending', 'green');
|
|
1488
|
+
|
|
1489
|
+
if (config.smsEnableWebhook !== false) {
|
|
1490
|
+
log('sms', 'webhook endpoint will be registered on HTTP server', 'gray');
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
global.smsProvider = {
|
|
1494
|
+
accountSid: config.smsAccountSid,
|
|
1495
|
+
authToken: config.smsAuthToken,
|
|
1496
|
+
fromNumber: config.smsFromNumber,
|
|
1497
|
+
messagingServiceSid: config.smsMessagingServiceSid || null,
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async function sendSmsMessage(config, to, message) {
|
|
1502
|
+
const accountSid = config.smsAccountSid;
|
|
1503
|
+
const authToken = config.smsAuthToken;
|
|
1504
|
+
const from = config.smsFromNumber;
|
|
1505
|
+
const messagingServiceSid = config.smsMessagingServiceSid;
|
|
1506
|
+
|
|
1507
|
+
const base64Auth = Buffer.from(`${accountSid}:${authToken}`).toString('base64');
|
|
1508
|
+
|
|
1509
|
+
const body = new URLSearchParams();
|
|
1510
|
+
if (messagingServiceSid) {
|
|
1511
|
+
body.append('MessagingServiceSid', messagingServiceSid);
|
|
1512
|
+
} else {
|
|
1513
|
+
body.append('From', from);
|
|
1514
|
+
}
|
|
1515
|
+
body.append('To', to);
|
|
1516
|
+
body.append('Body', message);
|
|
1517
|
+
|
|
1518
|
+
const res = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`, {
|
|
1519
|
+
method: 'POST',
|
|
1520
|
+
headers: {
|
|
1521
|
+
'Authorization': `Basic ${base64Auth}`,
|
|
1522
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1523
|
+
},
|
|
1524
|
+
body: body.toString(),
|
|
1525
|
+
signal: AbortSignal.timeout(30000),
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
if (!res.ok) {
|
|
1529
|
+
const text = await res.text();
|
|
1530
|
+
let detail = text.slice(0, 300);
|
|
1531
|
+
try {
|
|
1532
|
+
const errJson = JSON.parse(text);
|
|
1533
|
+
detail = errJson.message || detail;
|
|
1534
|
+
} catch {}
|
|
1535
|
+
throw new Error(`Twilio send failed (HTTP ${res.status}): ${detail}`);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return res.json();
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function verifyTwilioSignature(authToken, url, params, signature) {
|
|
1542
|
+
const crypto = require('crypto');
|
|
1543
|
+
// Sort params by key
|
|
1544
|
+
const sorted = Object.keys(params).sort();
|
|
1545
|
+
const data = url + sorted.map(k => k + params[k]).join('');
|
|
1546
|
+
const expected = crypto.createHmac('sha1', authToken).update(data).digest('base64');
|
|
1547
|
+
try {
|
|
1548
|
+
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
1549
|
+
} catch {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async function handleSmsWebhook(config, body, req) {
|
|
1555
|
+
// Rate limiting
|
|
1556
|
+
const ip = req.socket?.remoteAddress || 'unknown';
|
|
1557
|
+
const now = Date.now();
|
|
1558
|
+
const windowKey = `${ip}:${Math.floor(now / SMS_RATE_LIMIT_WINDOW)}`;
|
|
1559
|
+
const count = (smsRateLimitMap.get(windowKey) || 0) + 1;
|
|
1560
|
+
smsRateLimitMap.set(windowKey, count);
|
|
1561
|
+
if (count > SMS_RATE_LIMIT_MAX) {
|
|
1562
|
+
return { status: 429, body: { error: 'Too many requests' } };
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Replay protection
|
|
1566
|
+
const messageSid = body.MessageSid || '';
|
|
1567
|
+
if (messageSid && smsReplayCache.has(messageSid)) {
|
|
1568
|
+
return { status: 200, body: { ok: true, cached: true } };
|
|
1569
|
+
}
|
|
1570
|
+
if (messageSid) {
|
|
1571
|
+
smsReplayCache.set(messageSid, true);
|
|
1572
|
+
setTimeout(() => smsReplayCache.delete(messageSid), 600000);
|
|
1573
|
+
if (smsReplayCache.size > 10000) {
|
|
1574
|
+
const keys = [...smsReplayCache.keys()].slice(0, 2000);
|
|
1575
|
+
keys.forEach(k => smsReplayCache.delete(k));
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Signature validation
|
|
1580
|
+
const signature = req.headers['x-twilio-signature'] || '';
|
|
1581
|
+
if (signature && config.smsAuthToken) {
|
|
1582
|
+
const fullUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;
|
|
1583
|
+
const params = {};
|
|
1584
|
+
for (const [k, v] of Object.entries(body)) {
|
|
1585
|
+
params[k] = String(v);
|
|
1586
|
+
}
|
|
1587
|
+
if (!verifyTwilioSignature(config.smsAuthToken, fullUrl, params, signature)) {
|
|
1588
|
+
log('sms', 'invalid Twilio signature, rejecting', 'red');
|
|
1589
|
+
return { status: 403, body: { error: 'Invalid signature' } };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const from = body.From || '';
|
|
1594
|
+
const to = body.To || '';
|
|
1595
|
+
const text = body.Body || '';
|
|
1596
|
+
|
|
1597
|
+
if (!from || !text.trim()) {
|
|
1598
|
+
return { status: 200, body: { ok: true, ignored: true } };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
log('sms', `Inbound from ${from}: "${text.slice(0, 80)}"`, 'cyan');
|
|
1602
|
+
|
|
1603
|
+
// Check DM policy
|
|
1604
|
+
const dmPolicy = config.smsDmPolicy || 'allowlist';
|
|
1605
|
+
if (dmPolicy === 'disabled') {
|
|
1606
|
+
return { status: 200, body: { ok: true, ignored: true } };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Get AI response
|
|
1610
|
+
try {
|
|
1611
|
+
const { sendMessage } = require('../utils/api');
|
|
1612
|
+
const { getMemoryPrompt } = require('../utils/memory');
|
|
1613
|
+
const { getConfig } = require('../utils/config');
|
|
1614
|
+
const cfg = getConfig();
|
|
1615
|
+
|
|
1616
|
+
const conversationId = `sms_${from}`;
|
|
1617
|
+
const botId = 'universal-provider';
|
|
1618
|
+
const memoryPrompt = getMemoryPrompt(botId);
|
|
1619
|
+
|
|
1620
|
+
let systemPrompt = `You are a helpful SMS assistant. Keep responses concise (SMS format).`;
|
|
1621
|
+
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1622
|
+
|
|
1623
|
+
const response = await sendMessage(
|
|
1624
|
+
cfg.providerApiKey || cfg.apiKey || '', botId, text,
|
|
1625
|
+
conversationId, systemPrompt
|
|
1626
|
+
);
|
|
1627
|
+
const reply = response?.reply || response?.message || '';
|
|
1628
|
+
|
|
1629
|
+
if (reply) {
|
|
1630
|
+
await sendSmsMessage(config, from, reply);
|
|
1631
|
+
log('sms', `Reply sent to ${from} (${reply.length} chars)`, 'green');
|
|
1632
|
+
}
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
log('sms', `Response error: ${err.message}`, 'red');
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return { status: 200, body: { ok: true } };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function stopSmsProvider() {
|
|
1641
|
+
smsReplayCache.clear();
|
|
1642
|
+
smsRateLimitMap.clear();
|
|
1643
|
+
global.smsProvider = null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function startHttpServer() {
|
|
1647
|
+
const http = require('http');
|
|
1648
|
+
|
|
1649
|
+
const server = http.createServer(async (req, res) => {
|
|
1650
|
+
// CORS headers
|
|
1651
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1652
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
1653
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1654
|
+
|
|
1655
|
+
if (req.method === 'OPTIONS') {
|
|
1656
|
+
res.writeHead(200);
|
|
1657
|
+
res.end();
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (req.method === 'POST' && req.url === '/webhooks/sms') {
|
|
1662
|
+
// Twilio SMS webhook handler
|
|
1663
|
+
const { getConfig } = require('../utils/config');
|
|
1664
|
+
const cfg = getConfig();
|
|
1665
|
+
|
|
1666
|
+
let body = '';
|
|
1667
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
1668
|
+
req.on('end', async () => {
|
|
1669
|
+
// Parse form-encoded body
|
|
1670
|
+
const params = new URLSearchParams(body);
|
|
1671
|
+
const formBody = {};
|
|
1672
|
+
for (const [k, v] of params) formBody[k] = v;
|
|
1673
|
+
|
|
1674
|
+
// Create a mock req-like object for signature validation
|
|
1675
|
+
const mockReq = {
|
|
1676
|
+
url: req.url,
|
|
1677
|
+
headers: req.headers,
|
|
1678
|
+
socket: req.socket,
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
const result = await handleSmsWebhook(cfg, formBody, mockReq);
|
|
1682
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
1683
|
+
res.end(JSON.stringify(result.body));
|
|
1684
|
+
});
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (req.method === 'POST' && req.url === '/send') {
|
|
1689
|
+
let body = '';
|
|
1690
|
+
|
|
1691
|
+
req.on('data', chunk => {
|
|
1692
|
+
body += chunk.toString();
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
req.on('end', async () => {
|
|
1696
|
+
try {
|
|
1697
|
+
const { channel, target, message } = JSON.parse(body);
|
|
1698
|
+
|
|
1699
|
+
if (!channel || !target || !message) {
|
|
1700
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1701
|
+
res.end(JSON.stringify({ error: 'Missing required fields: channel, target, message' }));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (channel === 'whatsapp') {
|
|
1706
|
+
if (!global.whatsappSock) {
|
|
1707
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1708
|
+
res.end(JSON.stringify({ error: 'WhatsApp not connected' }));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const normalizedTarget = target.replace(/[^\d]/g, '');
|
|
1713
|
+
const jid = `${normalizedTarget}@s.whatsapp.net`;
|
|
1714
|
+
|
|
1715
|
+
await global.whatsappSock.sendMessage(jid, { text: message });
|
|
1716
|
+
|
|
1717
|
+
log('http', `WhatsApp message sent to ${target}`, 'green');
|
|
1718
|
+
|
|
1719
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1720
|
+
res.end(JSON.stringify({ success: true, channel: 'whatsapp', target }));
|
|
1721
|
+
|
|
1722
|
+
} else if (channel === 'telegram') {
|
|
1723
|
+
if (!global.telegramBot) {
|
|
1724
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1725
|
+
res.end(JSON.stringify({ error: 'Telegram not connected' }));
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const chatId = target.trim();
|
|
1730
|
+
if (!/^-?\d+$/.test(chatId)) {
|
|
1731
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1732
|
+
res.end(JSON.stringify({ error: 'Invalid Telegram chat ID' }));
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
await global.telegramBot.sendMessage(chatId, message);
|
|
1737
|
+
|
|
1738
|
+
log('http', `Telegram message sent to ${target}`, 'green');
|
|
1739
|
+
|
|
1740
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1741
|
+
res.end(JSON.stringify({ success: true, channel: 'telegram', target }));
|
|
1742
|
+
|
|
1743
|
+
} else if (channel === 'signal') {
|
|
1744
|
+
const { getConfig } = require('../utils/config');
|
|
1745
|
+
const cfg = getConfig();
|
|
1746
|
+
if (!cfg.signalHttpUrl) {
|
|
1747
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1748
|
+
res.end(JSON.stringify({ error: 'Signal not connected' }));
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
await sendSignalMessage(cfg, target, message);
|
|
1752
|
+
log('http', `Signal message sent to ${target}`, 'green');
|
|
1753
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1754
|
+
res.end(JSON.stringify({ success: true, channel: 'signal', target }));
|
|
1755
|
+
} else if (channel === 'irc') {
|
|
1756
|
+
if (!global.ircClient?.isReady()) {
|
|
1757
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1758
|
+
res.end(JSON.stringify({ error: 'IRC not connected' }));
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
sendIrcMessage(ircClient, target, message);
|
|
1762
|
+
log('http', `IRC message sent to ${target}`, 'green');
|
|
1763
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1764
|
+
res.end(JSON.stringify({ success: true, channel: 'irc', target }));
|
|
1765
|
+
} else if (channel === 'mattermost') {
|
|
1766
|
+
if (!global.mattermostProvider) {
|
|
1767
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1768
|
+
res.end(JSON.stringify({ error: 'Mattermost not connected' }));
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
await sendMattermostMessage(config, target, message);
|
|
1772
|
+
log('http', `Mattermost message sent to channel ${target}`, 'green');
|
|
1773
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1774
|
+
res.end(JSON.stringify({ success: true, channel: 'mattermost', target }));
|
|
1775
|
+
} else if (channel === 'imessage') {
|
|
1776
|
+
if (!global.imessageProvider) {
|
|
1777
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1778
|
+
res.end(JSON.stringify({ error: 'iMessage not connected' }));
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
try {
|
|
1782
|
+
execSync(`"${global.imessageProvider.imsgPath}" send --address "${target.replace(/"/g, '\\"')}" --message "${message.replace(/"/g, '\\"')}" 2>/dev/null`, {
|
|
1783
|
+
timeout: 15000, stdio: 'pipe',
|
|
1784
|
+
});
|
|
1785
|
+
log('http', `iMessage sent to ${target}`, 'green');
|
|
1786
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1787
|
+
res.end(JSON.stringify({ success: true, channel: 'imessage', target }));
|
|
1788
|
+
} catch (sendErr) {
|
|
1789
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1790
|
+
res.end(JSON.stringify({ error: `iMessage send failed: ${sendErr.message}` }));
|
|
1791
|
+
}
|
|
1792
|
+
} else if (channel === 'sms') {
|
|
1793
|
+
if (!global.smsProvider) {
|
|
1794
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
1795
|
+
res.end(JSON.stringify({ error: 'SMS not connected' }));
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
await sendSmsMessage(config, target, message);
|
|
1799
|
+
log('http', `SMS sent to ${target}`, 'green');
|
|
1800
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1801
|
+
res.end(JSON.stringify({ success: true, channel: 'sms', target }));
|
|
1802
|
+
} else {
|
|
1803
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1804
|
+
res.end(JSON.stringify({ error: 'Invalid channel. Use "whatsapp" or "telegram"' }));
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
log('http', `Error: ${err.message}`, 'red');
|
|
1809
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1810
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
} else {
|
|
1815
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1816
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
server.listen(3847, '127.0.0.1', () => {
|
|
1821
|
+
log('http', 'HTTP server listening on http://127.0.0.1:3847', 'green');
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
server.on('error', (err) => {
|
|
1825
|
+
log('http', `Server error: ${err.message}`, 'red');
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function startCronJobs(config) {
|
|
1830
|
+
try {
|
|
1831
|
+
const nodeCron = require('node-cron');
|
|
1832
|
+
const CRONS_FILE = path.join(os.homedir(), '.natureco', 'crons.json');
|
|
1833
|
+
|
|
1834
|
+
if (!fs.existsSync(CRONS_FILE)) {
|
|
1835
|
+
log('cron', 'No cron jobs configured', 'gray');
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const crons = JSON.parse(fs.readFileSync(CRONS_FILE, 'utf-8'));
|
|
1840
|
+
const enabledCrons = crons.filter(c => c.enabled);
|
|
1841
|
+
|
|
1842
|
+
if (enabledCrons.length === 0) {
|
|
1843
|
+
log('cron', 'No enabled cron jobs', 'gray');
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
log('cron', `Loading ${enabledCrons.length} cron job(s)...`, 'cyan');
|
|
1848
|
+
|
|
1849
|
+
enabledCrons.forEach(cronJob => {
|
|
1850
|
+
try {
|
|
1851
|
+
nodeCron.schedule(cronJob.schedule, async () => {
|
|
1852
|
+
log('cron', `Triggered: ${cronJob.name}`, 'yellow');
|
|
1853
|
+
|
|
1854
|
+
try {
|
|
1855
|
+
// Get provider config
|
|
1856
|
+
const { getConfig } = require('../utils/config');
|
|
1857
|
+
const cfg = getConfig();
|
|
1858
|
+
|
|
1859
|
+
if (!cfg.providerUrl || !cfg.providerApiKey) {
|
|
1860
|
+
log('cron', 'Provider not configured', 'red');
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const isAnthropic = cfg.providerUrl.includes('anthropic.com');
|
|
1865
|
+
|
|
1866
|
+
log('cron', `Sending prompt to AI (no tools)...`, 'cyan');
|
|
1867
|
+
|
|
1868
|
+
// Make direct API call without tools
|
|
1869
|
+
let reply = '';
|
|
1870
|
+
|
|
1871
|
+
if (isAnthropic) {
|
|
1872
|
+
// Anthropic API
|
|
1873
|
+
const response = await fetch(`${cfg.providerUrl}/v1/messages`, {
|
|
1874
|
+
method: 'POST',
|
|
1875
|
+
headers: {
|
|
1876
|
+
'x-api-key': cfg.providerApiKey,
|
|
1877
|
+
'anthropic-version': '2023-06-01',
|
|
1878
|
+
'Content-Type': 'application/json',
|
|
1879
|
+
},
|
|
1880
|
+
body: JSON.stringify({
|
|
1881
|
+
model: cfg.providerModel || 'claude-3-5-sonnet-20241022',
|
|
1882
|
+
max_tokens: 1000,
|
|
1883
|
+
messages: [{ role: 'user', content: cronJob.prompt }]
|
|
1884
|
+
}),
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
if (!response.ok) {
|
|
1888
|
+
const errorText = await response.text();
|
|
1889
|
+
throw new ApiError(`Anthropic API error: ${response.status} - ${errorText}`, response.status);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const data = await response.json();
|
|
1893
|
+
reply = data.content.find(c => c.type === 'text')?.text || '';
|
|
1894
|
+
|
|
1895
|
+
} else {
|
|
1896
|
+
// OpenAI-compatible API
|
|
1897
|
+
const response = await fetch(`${cfg.providerUrl}/chat/completions`, {
|
|
1898
|
+
method: 'POST',
|
|
1899
|
+
headers: {
|
|
1900
|
+
'Authorization': `Bearer ${cfg.providerApiKey}`,
|
|
1901
|
+
'Content-Type': 'application/json',
|
|
1902
|
+
},
|
|
1903
|
+
body: JSON.stringify({
|
|
1904
|
+
model: cfg.providerModel || 'llama-3.3-70b-versatile',
|
|
1905
|
+
messages: [{ role: 'user', content: cronJob.prompt }],
|
|
1906
|
+
temperature: 0.7,
|
|
1907
|
+
max_tokens: 1000,
|
|
1908
|
+
}),
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
if (!response.ok) {
|
|
1912
|
+
const errorText = await response.text();
|
|
1913
|
+
throw new ApiError(`Provider API error: ${response.status} - ${errorText}`, response.status);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const data = await response.json();
|
|
1917
|
+
reply = data.choices[0].message.content;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (!reply) {
|
|
1921
|
+
log('cron', `No response from AI`, 'red');
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
log('cron', `AI response received (${reply.length} chars)`, 'green');
|
|
1926
|
+
|
|
1927
|
+
// Send to target channel
|
|
1928
|
+
if (cronJob.action === 'whatsapp') {
|
|
1929
|
+
if (!global.whatsappSock) {
|
|
1930
|
+
log('cron', `WhatsApp not connected, skipping`, 'red');
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
const normalizedTarget = cronJob.target.replace(/[^\d]/g, '');
|
|
1935
|
+
const jid = `${normalizedTarget}@s.whatsapp.net`;
|
|
1936
|
+
|
|
1937
|
+
await global.whatsappSock.sendMessage(jid, { text: reply });
|
|
1938
|
+
log('cron', `Sent to WhatsApp: ${cronJob.target}`, 'green');
|
|
1939
|
+
|
|
1940
|
+
} else if (cronJob.action === 'telegram') {
|
|
1941
|
+
if (!global.telegramBot) {
|
|
1942
|
+
log('cron', `Telegram not connected, skipping`, 'red');
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
await global.telegramBot.sendMessage(cronJob.target, reply);
|
|
1947
|
+
log('cron', `Sent to Telegram: ${cronJob.target}`, 'green');
|
|
1948
|
+
} else if (cronJob.action === 'signal') {
|
|
1949
|
+
const { getConfig } = require('../utils/config');
|
|
1950
|
+
const cfg = getConfig();
|
|
1951
|
+
if (!cfg.signalHttpUrl) {
|
|
1952
|
+
log('cron', 'Signal not connected, skipping', 'red');
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
await sendSignalMessage(cfg, cronJob.target, reply);
|
|
1956
|
+
log('cron', `Sent to Signal: ${cronJob.target}`, 'green');
|
|
1957
|
+
} else if (cronJob.action === 'irc') {
|
|
1958
|
+
if (!global.ircClient?.isReady()) {
|
|
1959
|
+
log('cron', 'IRC not connected, skipping', 'red');
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
sendIrcMessage(ircClient, cronJob.target, reply);
|
|
1963
|
+
log('cron', `Sent to IRC: ${cronJob.target}`, 'green');
|
|
1964
|
+
} else if (cronJob.action === 'mattermost') {
|
|
1965
|
+
if (!global.mattermostProvider) {
|
|
1966
|
+
log('cron', 'Mattermost not connected, skipping', 'red');
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
await sendMattermostMessage(config, cronJob.target, reply);
|
|
1970
|
+
log('cron', `Sent to Mattermost: ${cronJob.target}`, 'green');
|
|
1971
|
+
} else if (cronJob.action === 'imessage') {
|
|
1972
|
+
if (!global.imessageProvider) {
|
|
1973
|
+
log('cron', 'iMessage not connected, skipping', 'red');
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
execSync(`"${global.imessageProvider.imsgPath}" send --address "${cronJob.target.replace(/"/g, '\\"')}" --message "${reply.replace(/"/g, '\\"')}" 2>/dev/null`, {
|
|
1977
|
+
timeout: 15000, stdio: 'pipe',
|
|
1978
|
+
});
|
|
1979
|
+
log('cron', `Sent to iMessage: ${cronJob.target}`, 'green');
|
|
1980
|
+
} else if (cronJob.action === 'sms') {
|
|
1981
|
+
if (!global.smsProvider) {
|
|
1982
|
+
log('cron', 'SMS not connected, skipping', 'red');
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
await sendSmsMessage(config, cronJob.target, reply);
|
|
1986
|
+
log('cron', `Sent to SMS: ${cronJob.target}`, 'green');
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
log('cron', `Error executing ${cronJob.name}: ${err.message}`, 'red');
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
log('cron', `Scheduled: ${cronJob.name} (${cronJob.schedule})`, 'green');
|
|
1995
|
+
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
log('cron', `Failed to schedule ${cronJob.name}: ${err.message}`, 'red');
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
log('cron', `Failed to load cron jobs: ${err.message}`, 'red');
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function stopGateway() {
|
|
2007
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
2008
|
+
console.log(chalk.gray('\n⚠️ Gateway not running\n'));
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
2013
|
+
|
|
2014
|
+
try {
|
|
2015
|
+
// Try graceful shutdown first
|
|
2016
|
+
process.kill(pid, 'SIGTERM');
|
|
2017
|
+
|
|
2018
|
+
// Wait a bit and check if process is still running
|
|
2019
|
+
setTimeout(() => {
|
|
2020
|
+
try {
|
|
2021
|
+
process.kill(pid, 0);
|
|
2022
|
+
// Process still running, force kill
|
|
2023
|
+
if (process.platform === 'win32') {
|
|
2024
|
+
try {
|
|
2025
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
2026
|
+
console.log(chalk.green('\n✅ Gateway stopped (force killed)\n'));
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
console.log(chalk.red(`\n❌ Failed to stop gateway: ${err.message}\n`));
|
|
2029
|
+
}
|
|
2030
|
+
} else {
|
|
2031
|
+
process.kill(pid, 'SIGKILL');
|
|
2032
|
+
console.log(chalk.green('\n✅ Gateway stopped (force killed)\n'));
|
|
2033
|
+
}
|
|
2034
|
+
} catch {
|
|
2035
|
+
// Process already stopped
|
|
2036
|
+
console.log(chalk.green('\n✅ Gateway stopped\n'));
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Remove PID file
|
|
2040
|
+
if (fs.existsSync(PID_FILE)) {
|
|
2041
|
+
fs.unlinkSync(PID_FILE);
|
|
2042
|
+
}
|
|
2043
|
+
}, 1000);
|
|
2044
|
+
|
|
2045
|
+
} catch (err) {
|
|
2046
|
+
// Process not found, try force kill on Windows
|
|
2047
|
+
if (process.platform === 'win32') {
|
|
2048
|
+
try {
|
|
2049
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
2050
|
+
console.log(chalk.green('\n✅ Gateway stopped\n'));
|
|
2051
|
+
} catch {
|
|
2052
|
+
console.log(chalk.yellow('\n⚠️ Gateway process not found\n'));
|
|
2053
|
+
}
|
|
2054
|
+
} else {
|
|
2055
|
+
console.log(chalk.yellow('\n⚠️ Gateway process not found\n'));
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Remove stale PID file
|
|
2059
|
+
if (fs.existsSync(PID_FILE)) {
|
|
2060
|
+
fs.unlinkSync(PID_FILE);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
function statusGateway() {
|
|
2066
|
+
if (!fs.existsSync(PID_FILE)) {
|
|
2067
|
+
console.log(chalk.gray('\n⚠️ Gateway not running\n'));
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const pid = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
2072
|
+
|
|
2073
|
+
try {
|
|
2074
|
+
process.kill(pid, 0);
|
|
2075
|
+
console.log(chalk.green('\n✅ Gateway running\n'));
|
|
2076
|
+
console.log(chalk.cyan('PID:'), chalk.white(pid));
|
|
2077
|
+
console.log(chalk.cyan('Logs:'), chalk.white(LOG_FILE));
|
|
2078
|
+
|
|
2079
|
+
// Show log tail if exists
|
|
2080
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
2081
|
+
const logs = fs.readFileSync(LOG_FILE, 'utf-8').split('\n').filter(l => l.trim()).slice(-10).join('\n');
|
|
2082
|
+
console.log(chalk.gray('\nRecent logs (last 10):'));
|
|
2083
|
+
console.log(chalk.white(logs));
|
|
2084
|
+
}
|
|
2085
|
+
console.log('');
|
|
2086
|
+
} catch {
|
|
2087
|
+
console.log(chalk.gray('\n⚠️ Gateway not running (stale PID file)\n'));
|
|
2088
|
+
fs.unlinkSync(PID_FILE);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function showLogs() {
|
|
2093
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
2094
|
+
console.log(chalk.gray('\n⚠️ No logs found\n'));
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const logs = fs.readFileSync(LOG_FILE, 'utf-8');
|
|
2099
|
+
console.log(logs);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Direkt çalıştırılınca worker olarak başlat
|
|
2103
|
+
if (require.main === module || process.argv.includes('--gateway-worker')) {
|
|
2104
|
+
gatewayServer('--gateway-worker');
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
module.exports = gatewayServer;
|