import baileysPkg from '@whiskeysockets/baileys'; const { default: makeWASocket, DisconnectReason, fetchLatestBaileysVersion } = baileysPkg; import { Boom } from '@hapi/boom'; import qrcode from 'qrcode-terminal'; import express from 'express'; import pino from 'pino'; import pg from 'pg'; // Driver penghubung ke PostgreSQL Supabase import { KATA_PEMICU, ID_GRUP_ANNOUNCEMENT, GRUP_DI_IZINKAN, NOMOR_KOORDINATOR_JADWAL, DATABASE_URL } from './config/env.js'; import { periksaJadwalOtomatis, dataJadwalGlobal } from './services/googleSheets.js'; // MENYELARASKAN IMPORT: Menggunakan 3 Agen Baru Terpadu import { jalankanRouterAgent } from './agents/routerAgent.js'; import { jalankanJadwalAgent } from './agents/jadwalAgent.js'; import { jalankanlainnyaAgent } from './agents/lainnyaAgent.js'; // --- TAMENG PERTAMA: INSTANSIASI EXPRESS SERVER (SINGLE PORT RULE) --- const app = express(); const PORT = process.env.PORT || 7860; // Port standar yang diwajibkan Hugging Face Spaces app.listen(PORT, '0.0.0.0', () => { console.log(`Server Odysseus aktif dan mendengarkan di port ${PORT}`); }); // Jalur Web Utama (Untuk cek status via browser) app.get('/', (req, res) => { res.status(200).send('Bot Pelayanan DNA Kids Aktif dan Berjalan di Hugging Face šŸš€\n'); }); // Jalur Khusus PING (Untuk ditembak secara berkala oleh Cron-Job.org / Uptime) app.get('/ping', (req, res) => { const waktuWIB = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta" }); console.log(`ā° [PING RECEIVED] Sinyal penjaga masuk pada pukul: ${waktuWIB} WIB. Menjaga kontainer tetap menyala.`); res.status(200).send('Bot Terjaga!'); }); // Jalankan server di port tunggal app.listen(PORT, () => { console.log(`šŸŒ [SERVER HTTP] Server Express berhasil mengunci port ${PORT}. Siap menerima ketukan eksternal.`); }); // --- KONFIGURASI MEMORI & STATE BOT --- const cooldownChat = new Map(); const memoriSesiChat = new Map(); let urutanGagalKonek = 0; const MAKSIMAL_GAGAL = 5; // --- ADAPTER AUTHENTICATION POSTGRESQL (SUPABASE) --- // Membuat pool koneksi ke database cloud const pool = new pg.Pool({ connectionString: DATABASE_URL }); async function gunakanAutentikasiPostgres() { // Membuat tabel penyimpanan session otomatis di database jika belum tersedia await pool.query(` CREATE TABLE IF NOT EXISTS sesi_bot_dna ( id VARCHAR(255) PRIMARY KEY, data TEXT NOT NULL ); `); const authStateDB = { state: { creds: await ambilDataSesi('creds') || {}, keys: { get: async (type, ids) => { const data = {}; for (const id of ids) { data[id] = await ambilDataSesi(`${type}-${id}`); } return data; }, set: async (data) => { for (const type in data) { for (const id in data[type]) { const value = data[type][id]; const keyName = `${type}-${id}`; if (value) { await simpanDataSesi(keyName, value); } else { await hapusDataSesi(keyName); } } } } } }, saveCreds: async () => { await simpanDataSesi('creds', authStateDB.state.creds); } }; return authStateDB; } // Fungsi pembantu (Helper) transaksi data SQL async function ambilDataSesi(id) { const res = await pool.query('SELECT data FROM sesi_bot_dna WHERE id = $1', [id]); return res.rows.length ? JSON.parse(res.rows[0].data) : null; } async function simpanDataSesi(id, data) { const jsonString = JSON.stringify(data); await pool.query('INSERT INTO sesi_bot_dna (id, data) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET data = $2', [id, jsonString]); } async function hapusDataSesi(id) { await pool.query('DELETE FROM sesi_bot_dna WHERE id = $1', [id]); } // --- ENGINE UTAMA WHATSAPP --- async function hubungkanKeWhatsApp() { if (urutanGagalKonek >= MAKSIMAL_GAGAL) { console.error("āŒ Gagal menembus WebSocket WA secara beruntun. Menghentikan percobaan..."); process.exit(1); } try { console.log("ā³ Mengambil data sesi login aman dari Cloud Database..."); // Mengganti useMultiFileAuthState dengan adapter database PostgreSQL kita const { state, saveCreds } = await gunakanAutentikasiPostgres(); console.log("ā³ Mengambil data versi protokol WhatsApp Web terbaru..."); const { version, isLatest } = await fetchLatestBaileysVersion(); console.log(`ā„¹ļø Menggunakan WA Web v${version.join('.')}, Terkini: ${isLatest}`); const sock = makeWASocket({ version, auth: state, printQRInTerminal: false, logger: pino({ level: 'silent' }), browser: ['Windows', 'Chrome', '122.0.0.0'], connectTimeoutMs: 60000, defaultQueryTimeoutMs: 60000, keepAliveIntervalMs: 30000 }); // --- EVENT LISTENER KONEKSI --- sock.ev.on('connection.update', (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { console.log('\n====== SCAN QR CODE INI VIA WHATSAPP KAMU ======'); qrcode.generate(qr, { small: true }); } if (connection === 'close') { const errorDetail = lastDisconnect?.error; const statusCode = errorDetail?.output?.statusCode || (errorDetail instanceof Boom ? errorDetail.output.statusCode : undefined); const alasanEror = errorDetail?.message || errorDetail || "Masalah Jaringan/Handshake Jarak Jauh"; console.log(`āš ļø Koneksi Terputus! Status: ${statusCode} | Alasan: ${alasanEror}`); const harusKonekUlang = statusCode !== DisconnectReason.loggedOut; if (harusKonekUlang) { urutanGagalKonek++; console.log(`šŸ”„ Mencoba koneksi ulang (${urutanGagalKonek}/${MAKSIMAL_GAGAL}) dalam 10 detik...`); setTimeout(hubungkanKeWhatsApp, 10000); } } else if (connection === 'open') { console.log('\nāœ… Bot Otomisasi SM Versi Modular Odysseus Sukses Terhubung via Supabase!\n'); urutanGagalKonek = 0; periksaJadwalOtomatis(); } }); sock.ev.on('creds.update', saveCreds); // --- EVENT LISTENER PESAN MASUK --- sock.ev.on('messages.upsert', async ({ messages }) => { const m = messages[0]; if (!m.message) return; const idPengirim = m.key.remoteJid; const teksMasingPesan = m.message?.conversation || m.message?.extendedTextMessage?.text || ""; console.log(`šŸ“© [LOG MASUK] Dari: ${idPengirim} | Isi Pesan: "${teksMasingPesan}"`); if (idPengirim === NOMOR_KOORDINATOR_JADWAL && teksMasingPesan.toLowerCase() === '!update') { await sock.sendMessage(idPengirim, { text: 'ā³ Sedang meremot pembaruan Google Sheets...' }); await periksaJadwalOtomatis(); await sock.sendMessage(idPengirim, { text: 'āœ… Sinkronisasi jadwal berhasil diperbarui!' }); return; } if (idPengirim === 'status@broadcast') return; const arrayPemicu = Array.isArray(KATA_PEMICU) ? KATA_PEMICU : [KATA_PEMICU]; const pemicuCocok = arrayPemicu.find(p => teksMasingPesan.toLowerCase().startsWith(p.toLowerCase())); if (m.key.fromMe && !pemicuCocok) return; if (idPengirim === ID_GRUP_ANNOUNCEMENT) return; if (teksMasingPesan.trim() === "" || !pemicuCocok) return; const apakahDariGrup = idPengirim.endsWith('@g.us'); if (apakahDariGrup && !GRUP_DI_IZINKAN.includes(idPengirim)) return; const pertanyaanBersih = teksMasingPesan.slice(pemicuCocok.length).trim(); if (!pertanyaanBersih) return; const waktuSekarang = Date.now(); const waktuTerakhirChat = cooldownChat.get(idPengirim) || 0; if (waktuSekarang - waktuTerakhirChat < 10000) { const sisaWaktu = Math.ceil((10000 - (waktuSekarang - waktuTerakhirChat)) / 1000); await sock.sendMessage(idPengirim, { text: `āš ļø *Sistem Cooldown!* Coba lagi dalam ${sisaWaktu} detik.` }); return; } cooldownChat.set(idPengirim, waktuSekarang); if (!memoriSesiChat.has(idPengirim)) memoriSesiChat.set(idPengirim, []); let riwayat = memoriSesiChat.get(idPengirim); try { await sock.sendPresenceUpdate('composing', idPengirim); const kategori = await jalankanRouterAgent(pertanyaanBersih); let jawabanAkhir = ""; if (kategori === "jadwal") { jawabanAkhir = await jalankanJadwalAgent(pertanyaanBersih, dataJadwalGlobal, riwayat); } else { jawabanAkhir = await jalankanlainnyaAgent(pertanyaanBersih, riwayat); } riwayat.push({ role: "user", content: pertanyaanBersih }); riwayat.push({ role: "assistant", content: jawabanAkhir }); if (riwayat.length > 6) riwayat.splice(0, 2); await sock.sendMessage(idPengirim, { text: jawabanAkhir }); } catch (errProses) { console.error("āŒ Terjadi eror saat memproses pesan:", errProses); } }); } catch (error) { console.error("āŒ Terjadi eror saat menginisialisasi koneksi:", error); } } hubungkanKeWhatsApp(); process.on('uncaughtException', (err) => { console.error('Terperangkap Fatal Error Sistem:', err); });