const { CompatBot } = require('./telegramCompat'); const { getSetting, getNowLocal } = require('../config/settingsManager'); const { logger } = require('../config/logger'); const customerSvc = require('./customerService'); const billingSvc = require('./billingService'); const mikrotikSvc = require('./mikrotikService'); const customerDevice = require('./customerDeviceService'); const whatsappService = require('./whatsappService'); const db = require('../config/database'); const monitoringSvc = require('./monitoringService'); const oltSvc = require('./oltService'); const ticketSvc = require('./ticketService'); let bot = null; function initTelegram() { const enabled = getSetting('telegram_enabled', false); const token = getSetting('telegram_bot_token', ''); if (!enabled || !token) { if (bot) { bot.stopPolling(); bot = null; logger.info('Telegram Bot: Dihentikan (Nonaktif)'); } return; } // Jika token berubah, kita harus stop bot lama dan buat baru if (bot && bot.token !== token) { bot.stopPolling(); bot = null; logger.info('Telegram Bot: Token berubah, me-restart bot...'); } if (bot) { logger.info('Telegram Bot: Sudah berjalan, melewati inisialisasi.'); return; } bot = new CompatBot(token); // โ”€โ”€ Wrapper aman: bila Telegram gagal parse Markdown (mis. underscore pada // username pelanggan), kirim ulang otomatis TANPA parse_mode. (function () { const _send = bot.sendMessage.bind(bot); bot.sendMessage = async function (chatId, text, opts) { try { return await _send(chatId, text, opts); } catch (e) { const m = String((e && e.message) || ''); if (m.includes('parse entities')) { const o = Object.assign({}, opts); delete o.parse_mode; return _send(chatId, text, o); } throw e; } }; const _edit = bot.editMessageText.bind(bot); bot.editMessageText = async function (text, opts) { try { return await _edit(text, opts); } catch (e) { const m = String((e && e.message) || ''); if (m.includes('parse entities')) { const o = Object.assign({}, opts); delete o.parse_mode; return _edit(text, o); } throw e; } }; })(); // Clear webhook to ensure polling works (Sync) bot.deleteWebHook().then(() => { bot.getMe().then(me => { logger.info(`Telegram Bot: Terhubung sebagai @${me.username}`); }).catch(e => logger.error('Telegram Bot Error (getMe):', e.message)); }).catch(e => logger.error('Telegram Bot Error (deleteWebHook):', e.message)); // Middleware Admin Check (Fetch latest ID every time) const isAdmin = (msg) => { const currentAdminId = getSetting('telegram_admin_id', '').toString(); return msg.from.id.toString() === currentAdminId; }; // Helper Mikhmon Parser const parseMikhmon = (script) => { if (!script) return null; const s = String(script).trim(); // Cari pattern :put (",rem, ... , ... , ... // Updated regex untuk support format: :put (",rem,4000,2d,5000,,Disable,"); const putMatch = s.match(/:\s*put\s*\(\s*[",]rem[",]?\s*,\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)/i); if (putMatch) { const cost = String(putMatch[1] || '').trim(); const validity = String(putMatch[2] || '').trim(); const priceStr = String(putMatch[3] || '').trim(); const price = Number(priceStr.replace(/[^\d]/g, '')) || 0; if (validity && price > 0) { return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 }; } } // Fallback: split by comma const parts = s.split(',').map(p => String(p).trim()); let remIdx = -1; for (let i = 0; i < parts.length; i++) { if (parts[i].includes('rem')) { remIdx = i; break; } } if (remIdx >= 0 && remIdx + 3 < parts.length) { const cost = String(parts[remIdx + 1] || '').trim(); const validity = String(parts[remIdx + 2] || '').trim(); const priceStr = String(parts[remIdx + 3] || '').trim(); const price = Number(priceStr.replace(/[^\d]/g, '')) || 0; if (validity && price > 0) { return { validity, price, cost: Number(cost.replace(/[^\d]/g, '')) || 0 }; } } return null; }; const isTruthy = (value) => { if (value === true || value === 1) return true; const normalized = String(value == null ? '' : value).trim().toLowerCase(); return normalized === 'true' || normalized === 'yes' || normalized === '1' || normalized === 'on'; }; const formatGangguanCount = (scriptRow) => { const raw = scriptRow && scriptRow.source != null ? String(scriptRow.source) : '0'; const count = parseInt(raw.replace(/[^\d-]/g, ''), 10); return Number.isFinite(count) && count > 0 ? count : 0; }; const loadPppoeSnapshot = async () => { const [secrets, active, scripts] = await Promise.all([ mikrotikSvc.getPppoeSecrets(), mikrotikSvc.getPppoeActive(), mikrotikSvc.getSystemScripts() ]); const customers = customerSvc.getAllCustomers(); const activeMap = new Map(); (active || []).forEach((row) => { const name = String(row && row.name ? row.name : '').trim(); if (name) activeMap.set(name, row); }); const scriptMap = new Map(); (scripts || []).forEach((row) => { const name = String(row && row.name ? row.name : '').trim(); if (name) scriptMap.set(name, row); }); const customerMap = new Map(); (customers || []).forEach((row) => { const username = String(row && row.pppoe_username ? row.pppoe_username : '').trim(); if (username) customerMap.set(username, row); }); return { secrets: Array.isArray(secrets) ? secrets : [], active: Array.isArray(active) ? active : [], activeMap, scriptMap, customerMap }; }; const buildOfflineEntries = (snapshot) => { return snapshot.secrets .filter((secret) => { const username = String(secret && secret.name ? secret.name : '').trim(); if (!username) return false; if (isTruthy(secret && secret.disabled)) return false; return !snapshot.activeMap.has(username); }) .map((secret) => { const username = String(secret.name || '').trim(); const customer = snapshot.customerMap.get(username) || null; const script = snapshot.scriptMap.get(username) || null; return { username, customerName: customer && customer.name ? customer.name : '-', phone: customer && customer.phone ? customer.phone : '-', profile: secret && secret.profile ? secret.profile : '-', service: secret && secret.service ? secret.service : '-', password: secret && secret.password ? secret.password : '-', failCount: formatGangguanCount(script) }; }) .sort((a, b) => { if (b.failCount !== a.failCount) return b.failCount - a.failCount; return a.username.localeCompare(b.username, 'id'); }); }; const buildOfflineTelegramText = (snapshot) => { const now = getNowLocal(); const offline = buildOfflineEntries(snapshot); const lines = []; lines.push('USER PPPoE OFFLINE'); lines.push('============================'); lines.push(`Waktu : ${now}`); lines.push('============================'); lines.push(''); lines.push('RINGKASAN'); lines.push(`Total Secret : ${snapshot.secrets.length}`); lines.push(`Total Aktif : ${snapshot.active.length}`); lines.push(`Total Offline: ${offline.length}`); lines.push(''); lines.push('DAFTAR USER OFFLINE'); if (offline.length === 0) { lines.push('Semua user sedang online.'); } else { offline.slice(0, 20).forEach((row, index) => { lines.push(`${index + 1}. ${row.username}`); lines.push(` Pelanggan : ${row.customerName}`); lines.push(` WA : ${row.phone}`); lines.push(` Paket : ${row.profile}`); lines.push(` Gangguan : ${row.failCount}x`); }); if (offline.length > 20) { lines.push(`...dan ${offline.length - 20} user offline lainnya.`); } } lines.push(''); lines.push('Cek detail 1 user: /cekpppoe username'); lines.push('Contoh: /cekpppoe budi001'); return lines.join('\n'); }; const buildPppoeUserDetailText = async (username) => { const target = String(username || '').trim(); if (!target) throw new Error('Username PPPoE wajib diisi'); const snapshot = await loadPppoeSnapshot(); const secret = snapshot.secrets.find((row) => String(row && row.name ? row.name : '').trim() === target); if (!secret) { throw new Error(`PPPoE user "${target}" tidak ditemukan`); } const activeRow = snapshot.activeMap.get(target) || null; const customer = snapshot.customerMap.get(target) || null; const script = snapshot.scriptMap.get(target) || null; const offline = buildOfflineEntries(snapshot); const date = getNowLocal(); const online = !!activeRow; const lines = []; lines.push('DETAIL CEK PPPoE'); lines.push('============================'); lines.push(`Waktu : ${date}`); lines.push(`Status : ${online ? 'ONLINE' : 'OFFLINE'}`); lines.push('============================'); lines.push('INFO LAYANAN'); lines.push(`Pelanggan : ${customer && customer.name ? customer.name : '-'}`); lines.push(`No. WA : ${customer && customer.phone ? customer.phone : '-'}`); lines.push(`Username : ${secret.name || '-'}`); lines.push(`Password : ${secret.password || '-'}`); lines.push(`Service : ${secret.service || '-'}`); lines.push(`Profile : ${secret.profile || '-'}`); lines.push(''); lines.push('INFO PERANGKAT'); lines.push(`IP Aktif : ${activeRow && activeRow.address ? activeRow.address : '-'}`); lines.push(`MAC/Caller : ${activeRow && (activeRow.callerId || activeRow['caller-id']) ? (activeRow.callerId || activeRow['caller-id']) : '-'}`); lines.push(''); lines.push('STATUS GANGGUAN'); lines.push(`Jumlah Gangguan : ${formatGangguanCount(script)}x Terputus`); lines.push(`Total Secret : ${snapshot.secrets.length}`); lines.push(`Total Active : ${snapshot.active.length}`); lines.push(`Total Offline : ${offline.length}`); if (!online && offline.length) { lines.push(''); lines.push('User Offline Lain'); offline.slice(0, 10).forEach((row) => { lines.push(`- ${row.username} (${row.failCount}x)`); }); if (offline.length > 10) lines.push(`...dan ${offline.length - 10} lainnya.`); } return lines.join('\n'); }; // Main Menu (Inline Keyboard for better visibility) const mainMenu = { reply_markup: { inline_keyboard: [ [{ text: '๐Ÿ“Š Statistik', callback_data: 'menu_stats' }, { text: '๐Ÿ‘ฅ Pelanggan', callback_data: 'menu_cust' }], [{ text: '๐ŸŽซ Voucher', callback_data: 'menu_vouch' }, { text: '๐Ÿ’ฐ Tagihan', callback_data: 'menu_bill' }], [{ text: 'โš™๏ธ MikroTik Status', callback_data: 'menu_mt' }], [{ text: '๐Ÿ–ฅ๏ธ Sistem', callback_data: 'menu_sys' }, { text: '๐Ÿ’ฐ Tunggakan', callback_data: 'menu_arrears' }], [{ text: '๐Ÿ’ฐ Pendapatan', callback_data: 'menu_pendapatan' }], [{ text: '๐ŸŽซ Tiket', callback_data: 'menu_tiket' }, { text: '๐Ÿ“ก OLT', callback_data: 'menu_olt' }, { text: '๐Ÿ“ถ Trafik', callback_data: 'menu_trafik' }], [{ text: '๐Ÿ”„ Refresh', callback_data: 'menu_main' }] ] } }; bot.onText(/\/start|\/menu/i, (msg) => { if (!isAdmin(msg)) return bot.sendMessage(msg.chat.id, `Maaf, Anda tidak memiliki akses admin.\nChat ID Anda: ${msg.from.id}`); bot.sendMessage(msg.chat.id, '๐Ÿ  *PANEL ADMIN ' + getSetting('company_header', 'OPEN-ISP') + '*\nSilakan pilih menu di bawah ini:', { parse_mode: 'Markdown', ...mainMenu }); }); bot.on('message', async (msg) => { if (!isAdmin(msg)) return; const text = msg.text; if (text === '/start' || text === '/menu') return; // Handled by onText // Logika handle text manual jika diperlukan (misal untuk perintah kick/edit) }); // Callback Query Handling bot.on('callback_query', async (query) => { const chatId = query.message.chat.id; const data = query.data; if (!isAdmin(query)) return bot.answerCallbackQuery(query.id, { text: 'Akses Ditolak' }); if (data === 'menu_main') { bot.editMessageText('๐Ÿ  *PANEL ADMIN ' + getSetting('company_header', 'OPEN-ISP') + '*\nSilakan pilih menu di bawah ini:', { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown', ...mainMenu }); } else if (data === 'menu_stats') { const stats = customerSvc.getCustomerStats(); const billing = billingSvc.getDashboardStats(); let res = `*๐Ÿ“Š STATISTIK SISTEM*\n\n`; res += `๐Ÿ‘ฅ Pelanggan: ${stats.total}\n`; res += `โœ… Aktif: ${stats.active}\n`; res += `๐Ÿšซ Terisolir: ${stats.suspended}\n\n`; res += `๐Ÿ’ฐ Pendapatan Bulan Ini: Rp ${billing.thisMonth.toLocaleString('id-ID')}\n`; res += `โณ Belum Dibayar: ${billing.unpaidCount} Tagihan`; bot.sendMessage(chatId, res, { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } else if (data === 'menu_cust') { bot.sendMessage(chatId, '๐Ÿ‘ฅ *MANAJEMEN PELANGGAN*\nPilih aksi:', { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [{ text: '๐Ÿ” Cari Pelanggan', callback_data: 'cust_search' }], [{ text: '๐Ÿšซ Daftar Terisolir', callback_data: 'cust_suspended' }], [{ text: '๐Ÿ“ก List ONU (GenieACS)', callback_data: 'cust_listonu' }], [{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }] ] } }); } else if (data === 'menu_bill') { bot.sendMessage(chatId, '๐Ÿ’ฐ *MANAJEMEN TAGIHAN*\nPilih aksi:', { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [{ text: 'โณ Tagihan Belum Bayar', callback_data: 'bill_unpaid' }], [{ text: '๐Ÿ“ˆ Pendapatan Hari Ini', callback_data: 'bill_today' }], [{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }] ] } }); } else if (data === 'menu_vouch') { bot.sendMessage(chatId, '๐ŸŽซ *MANAJEMEN VOUCHER*\nPilih aksi:', { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [{ text: 'โž• Buat Voucher Baru', callback_data: 'vouch_create' }], [{ text: '๐Ÿ“œ Daftar Hotspot Profile', callback_data: 'vouch_profiles' }], [{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }] ] } }); } else if (data === 'menu_mt') { bot.sendMessage(chatId, 'โš™๏ธ *STATUS MIKROTIK*\nPilih data:', { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [{ text: '๐Ÿš€ Resource System', callback_data: 'mt_resource' }], [{ text: '๐ŸŸข User Aktif (PPPoE/HS)', callback_data: 'mt_active' }], [{ text: '๐Ÿ”ด User Offline (PPPoE)', callback_data: 'mt_offline' }], [{ text: '๐Ÿ”‘ List PPPoE Secrets', callback_data: 'mt_pppoe' }], [{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }] ] } }); } else if (data === 'menu_sys') { try { bot.sendMessage(chatId, tgSistemText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'menu_arrears') { try { bot.sendMessage(chatId, tgTunggakanText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'menu_tiket') { try { bot.sendMessage(chatId, tgTiketText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'menu_olt') { bot.sendMessage(chatId, '๐Ÿ“ก Mengambil status OLT, mohon tungguโ€ฆ'); try { bot.sendMessage(chatId, await tgOltText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'menu_trafik') { try { bot.sendMessage(chatId, await tgTrafikText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'menu_pendapatan') { try { bot.sendMessage(chatId, tgPendapatanText(), { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] } }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } else if (data === 'mt_resource') { try { const res = await mikrotikSvc.getSystemResource(); let txt = `*โš™๏ธ MIKROTIK STATUS*\n\n`; txt += `Model: ${res.boardName || res['board-name'] || '-'}\n`; txt += `CPU: ${res.cpuLoad || res['cpu-load'] || '0'}%\n`; txt += `Uptime: ${res.uptime}\n`; txt += `Version: ${res.version}`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Gagal mengambil data MikroTik: ' + e.message); } } else if (data === 'mt_active') { try { const pppoe = await mikrotikSvc.getPppoeActive(); const hs = await mikrotikSvc.getHotspotActive(); const scripts = await mikrotikSvc.getSystemScripts(); let txt = `*๐ŸŸข USER AKTIF*\n\n`; txt += `๐ŸŒ *PPPoE (${pppoe.length}):*\n`; pppoe.slice(0, 15).forEach(a => { const s = scripts.find(sc => sc.name === a.name); const failCount = s ? (s.source || '0') : '0'; txt += `โ€ข \`${a.name}\` (${a.address}) [โšก${failCount}]\n`; }); txt += `\n๐Ÿ“ถ *Hotspot (${hs.length}):*\n`; hs.slice(0, 5).forEach(h => { txt += `โ€ข \`${h.user}\` (${h.address})\n`; }); txt += `\n_โšก = Jumlah Gangguan Terdeteksi_`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Error: ' + e.message); } } else if (data === 'mt_offline') { try { const snapshot = await loadPppoeSnapshot(); bot.sendMessage(chatId, buildOfflineTelegramText(snapshot)); } catch (e) { bot.sendMessage(chatId, 'Error: ' + e.message); } } else if (data === 'mt_pppoe') { try { const secrets = await mikrotikSvc.getPppoeSecrets(); let txt = `*๐Ÿ”‘ PPPoE SECRETS (${secrets.length})*\n\n`; secrets.slice(0, 20).forEach(s => { txt += `โ€ข \`${s.name}\` (${s.profile})\n`; }); if (secrets.length > 20) txt += `\n_Menampilkan 20 dari ${secrets.length}..._`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Error: ' + e.message); } } else if (data === 'cust_search') { bot.sendMessage(chatId, '๐Ÿ” *CARI PELANGGAN*\nKetik perintah `/cari [nama/wa]`\n\nContoh: `/cari budi` atau `/cari 0812`', { parse_mode: 'Markdown' }); } else if (data === 'cust_listonu') { const customerDevice = require('./customerDeviceService'); let res = await customerDevice.listDevicesWithTags(30); // Jika kosong, coba ambil semua perangkat if (!res.ok || res.devices.length === 0) { res = await customerDevice.listAllDevices(30); } if (!res.ok || res.devices.length === 0) { return bot.sendMessage(chatId, '๐Ÿ“ญ Tidak ada perangkat ONU yang terdeteksi di GenieACS.'); } let txt = `*๐Ÿ“ก DAFTAR ONU (GenieACS)*\n\n`; res.devices.forEach(d => { const id = d._id || 'Unknown ID'; const tags = Array.isArray(d._tags) ? d._tags.join(', ') : (d._tags || '-'); txt += `โ€ข \`${id}\`\n โ”” Tag: ${tags}\n`; }); bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } else if (data === 'cust_suspended') { const customers = customerSvc.getAllCustomers().filter(c => c.status === 'suspended'); if (customers.length === 0) return bot.sendMessage(chatId, 'โœ… Tidak ada pelanggan yang terisolir.'); let txt = `*๐Ÿšซ PELANGGAN TERISOLIR (${customers.length})*\n\n`; customers.slice(0, 15).forEach(c => { txt += `โ€ข *${c.name}* (${c.phone})\n`; }); if (customers.length > 15) txt += `\n_...dan ${customers.length - 15} lainnya._`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } else if (data === 'bill_unpaid') { const invoices = billingSvc.getAllInvoices().filter(i => i.status === 'unpaid'); if (invoices.length === 0) return bot.sendMessage(chatId, 'โœ… Semua tagihan sudah lunas!'); let txt = `*โณ TAGIHAN BELUM BAYAR (${invoices.length})*\n\n`; invoices.slice(0, 15).forEach(i => { const c = customerSvc.getCustomerById(i.customer_id); txt += `โ€ข ${c ? c.name : 'Unknown'} - Rp ${i.amount.toLocaleString('id-ID')}\n`; }); if (invoices.length > 15) txt += `\n_...dan ${invoices.length - 15} lainnya._`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } else if (data === 'bill_today') { try { const stats = billingSvc.getTodayRevenue(); const total = stats.total || 0; const count = stats.count || 0; let txt = `*๐Ÿ“ˆ PENDAPATAN HARI INI*\n\n`; txt += `๐Ÿ’ฐ Total: *Rp ${total.toLocaleString('id-ID')}*\n`; txt += `๐Ÿงพ Jumlah: ${count} Transaksi\n\n`; txt += `_Data berdasarkan pembayaran yang diverifikasi hari ini (Waktu Lokal)._`; bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Error: ' + e.message); } } else if (data === 'vouch_profiles') { try { const profiles = await mikrotikSvc.getHotspotUserProfiles(); const buttons = []; // Filter profiles that have Mikhmon Price const filtered = profiles.filter(p => parseMikhmon(p.onLogin)); if (filtered.length === 0) { return bot.sendMessage(chatId, 'โš ๏ธ Tidak ditemukan paket yang memiliki harga jual (Format Mikhmon).'); } filtered.forEach((p, index) => { const meta = parseMikhmon(p.onLogin); if (index % 2 === 0) buttons.push([]); buttons[buttons.length - 1].push({ text: `๐ŸŽซ ${p.name} (Rp ${meta.price})`, callback_data: `vouch_gen:${p.name}` }); }); buttons.push([{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_vouch' }]); bot.sendMessage(chatId, '*๐Ÿ“œ PILIH PAKET VOUCHER*\nSilakan klik paket untuk langsung membuat PIN:', { parse_mode: 'Markdown', reply_markup: { inline_keyboard: buttons } }); } catch (e) { bot.sendMessage(chatId, 'Error: ' + e.message); } } else if (data.startsWith('vouch_gen:')) { const profileName = data.split(':')[1]; try { const profiles = await mikrotikSvc.getHotspotUserProfiles(); const profile = profiles.find(p => p.name === profileName); if (!profile) throw new Error('Profil tidak ditemukan'); const meta = parseMikhmon(profile.onLogin); if (!meta) throw new Error('Data harga/durasi profil tidak ditemukan (Format Mikhmon)'); const pin = Math.floor(1000 + Math.random() * 9000).toString(); await mikrotikSvc.addHotspotUser({ server: 'all', name: pin, password: pin, profile: profileName, 'limit-uptime': meta.validity, comment: `vc-${pin}-${profileName}` }); let res = `*๐ŸŽซ VOUCHER BERHASIL (INSTAN)*\n\n`; res += `๐ŸŽซ KODE VOUCHER: \`${pin}\`\n`; res += `๐Ÿ’ฐ Harga: Rp ${meta.price}\n`; res += `โณ Durasi: ${meta.validity}\n`; res += `๐Ÿ“ฆ Paket: ${profileName}\n`; res += `\n_Silakan masukkan kode di atas pada halaman login hotspot._`; bot.sendMessage(chatId, res, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } } bot.answerCallbackQuery(query.id); }); // Custom Commands bot.onText(/\/vouch (\S+) (\S+) (.+)/, async (msg, match) => { if (!isAdmin(msg)) return; const [_, profile, limit, comment] = match; try { const pin = Math.floor(1000 + Math.random() * 9000).toString(); await mikrotikSvc.addHotspotUser({ server: 'all', name: pin, password: pin, profile, 'limit-uptime': limit, comment }); bot.sendMessage(msg.chat.id, `*๐ŸŽซ VOUCHER BERHASIL*\n\n๐ŸŽซ KODE VOUCHER: \`${pin}\`\n๐Ÿ“ฆ Paket: ${profile}\nโณ Limit: ${limit}`, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/vcr\s+(\S+)\s+(\S+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const code = match[1]; const profile = match[2]; try { const now = new Date(); const dateStr = `${String(now.getDate()).padStart(2, '0')}/${String(now.getMonth() + 1).padStart(2, '0')}/${now.getFullYear()}`; const comment = `vc ${code} ${dateStr}`; await mikrotikSvc.addHotspotUser({ name: code, password: code, profile: profile, comment: comment }); bot.sendMessage(chatId, `โœ… Voucher Hotspot *${code}* berhasil dibuat.\n\n๐Ÿ‘ค User: \`${code}\`\n๐Ÿ”‘ Pass: \`${code}\`\n๐Ÿ“ฆ Profile: *${profile}*\n๐Ÿ“ Comment: *${comment}*`, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal buat voucher: ' + e.message); } }); bot.onText(/\/kick (\S+)/, async (msg, match) => { if (!isAdmin(msg)) return; try { const user = match[1]; await mikrotikSvc.kickPppoeUser(user); await mikrotikSvc.kickHotspotUser(user); bot.sendMessage(msg.chat.id, `โœ… Session *${user}* berhasil diputus.`); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/editpppoe (\S+) (\S+)/, async (msg, match) => { if (!isAdmin(msg)) return; try { const [_, user, profile] = match; await mikrotikSvc.setPppoeProfile(user, profile); bot.sendMessage(msg.chat.id, `โœ… Profile *${user}* diubah ke *${profile}*.`); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/cekpppoe (\S+)/i, async (msg, match) => { if (!isAdmin(msg)) return; try { const username = match[1]; const detail = await buildPppoeUserDetailText(username); bot.sendMessage(msg.chat.id, detail); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal cek PPPoE: ' + e.message); } }); bot.onText(/\/cari (.+)/, async (msg, match) => { if (!isAdmin(msg)) return; const query = match[1].toLowerCase(); const customers = customerSvc.getAllCustomers().filter(c => c.name.toLowerCase().includes(query) || c.phone.includes(query) ); if (customers.length === 0) return bot.sendMessage(msg.chat.id, `โŒ Pelanggan dengan keyword "${query}" tidak ditemukan.`); let res = `*๐Ÿ” HASIL PENCARIAN (${customers.length})*\n\n`; customers.slice(0, 10).forEach(c => { res += `๐Ÿ‘ค *${c.name}*\n๐Ÿ“ž ${c.phone}\n๐Ÿšฆ Status: ${c.status === 'active' ? 'โœ… Aktif' : '๐Ÿšซ Terisolir'}\n\n`; }); if (customers.length > 10) res += `_...dan ${customers.length - 10} lainnya._`; bot.sendMessage(msg.chat.id, res, { parse_mode: 'Markdown' }); }); // โ”€โ”€ Monitoring (admin): helper + perintah /sistem /tunggakan /tiket /olt /trafik โ”€โ”€ function tgSistemText() { const h = monitoringSvc.getHealthStatus(); const m = (h && h.metrics) ? h.metrics : monitoringSvc.getAllMetrics(); const sys = (m && m.system) || {}; const cpu = (sys.cpu && sys.cpu.usage != null) ? sys.cpu.usage : '-'; const cores = (sys.cpu && sys.cpu.cores) ? sys.cpu.cores : '-'; const ram = (sys.memory && sys.memory.percentage != null) ? sys.memory.percentage : '-'; const ramUsed = (sys.memory && sys.memory.used) ? sys.memory.used : '-'; const ramTotal = (sys.memory && sys.memory.total) ? sys.memory.total : '-'; const disk = (m && m.disk && m.disk.percentage) ? m.disk.percentage : '-'; const la = sys.loadAverage || {}; let t = `๐Ÿ–ฅ๏ธ *MONITORING SISTEM*\n\n`; t += `โš™๏ธ CPU : *${cpu}%* (${cores} core)\n`; t += `๐Ÿง  RAM : *${ram}%* (${ramUsed}/${ramTotal})\n`; t += `๐Ÿ“€ Disk: *${disk}*\n`; t += `โณ Load: ${la['1min'] != null ? la['1min'] : '-'} / ${la['5min'] != null ? la['5min'] : '-'} / ${la['15min'] != null ? la['15min'] : '-'}\n`; t += `๐Ÿ•’ Uptime: ${sys.uptime || '-'}\n`; if (h && h.issues && h.issues.length) t += `\n๐Ÿ”ด *Masalah:*\n- ${h.issues.join('\n- ')}`; if (h && h.warnings && h.warnings.length) t += `\n๐ŸŸก *Peringatan:*\n- ${h.warnings.join('\n- ')}`; return t; } function tgTunggakanText() { const row = db.prepare("SELECT COUNT(*) c, COALESCE(SUM(amount),0) s FROM invoices WHERE status='unpaid'").get(); const top = db.prepare("SELECT c.name, COALESCE(c.customer_code,'-') code, SUM(i.amount) t FROM invoices i JOIN customers c ON c.id=i.customer_id WHERE i.status='unpaid' GROUP BY i.customer_id ORDER BY t DESC LIMIT 8").all(); let t = `๐Ÿ’ฐ *TUNGGAKAN*\n\nTotal: *${row.c} tagihan*\nNominal: *Rp ${Number(row.s).toLocaleString('id-ID')}*\n`; if (top.length) { t += `\n*Top ${top.length}:*\n`; top.forEach((r, i) => { t += `${i + 1}. ${r.name} (${r.code}) โ€” Rp ${Number(r.t).toLocaleString('id-ID')}\n`; }); } return t; } function tgTiketText() { const s = ticketSvc.getTicketStats(); const open = ticketSvc.getAllTickets('open') || []; let t = `๐ŸŽซ *TIKET*\n\nOpen: *${s.open}* ยท Proses: *${s.inProgress}* ยท Selesai: *${s.resolved}*\n`; if (open.length) { t += `\n*Open terbaru:*\n`; open.slice(0, 8).forEach(x => { t += `#${x.id} ${x.subject} โ€” ${x.customer_name || '-'}\n`; }); } return t; } async function tgOltText() { const o = await oltSvc.getAllOltsStats(); let t = `๐Ÿ“ก *OLT STATUS*\n\nONU total: *${o.onus_total}*\n๐ŸŸข Online: *${o.onus_online}*\n๐Ÿ”ด Offline: *${o.onus_offline}*\n๐ŸŸก Lemah: *${o.onus_weak}*\n`; if (o.error) t += `\n_Catatan: ${o.error}_`; return t; } async function tgTrafikText() { let count = 0; try { const map = await mikrotikSvc.getAllActiveSessionsMap(); count = (map && map.size != null) ? map.size : ((map && map.length) || 0); } catch (e) {} return `๐Ÿ“ถ *SESI AKTIF*\n\nPPPoE/Hotspot online: *${count}* sesi\n`; } const backBtn = { inline_keyboard: [[{ text: 'โฌ…๏ธ Kembali', callback_data: 'menu_main' }]] }; bot.onText(/\/(sistem|monitor|health)/i, async (msg) => { if (!isAdmin(msg)) return; try { bot.sendMessage(msg.chat.id, tgSistemText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/tunggakan/i, async (msg) => { if (!isAdmin(msg)) return; try { bot.sendMessage(msg.chat.id, tgTunggakanText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/tiket/i, async (msg) => { if (!isAdmin(msg)) return; try { bot.sendMessage(msg.chat.id, tgTiketText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/(olt|oltstatus|olt-status)/i, async (msg) => { if (!isAdmin(msg)) return; bot.sendMessage(msg.chat.id, '๐Ÿ“ก Mengambil status OLT, mohon tungguโ€ฆ'); try { bot.sendMessage(msg.chat.id, await tgOltText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/(trafik|online)/i, async (msg) => { if (!isAdmin(msg)) return; try { bot.sendMessage(msg.chat.id, await tgTrafikText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/(offline|pppoeoffline|pppoe-offline)/i, async (msg) => { if (!isAdmin(msg)) return; try { const snap = await loadPppoeSnapshot(); bot.sendMessage(msg.chat.id, buildOfflineTelegramText(snap)); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/(aktif|pppoeonline|pppoe-online)/i, async (msg) => { if (!isAdmin(msg)) return; try { const snap = await loadPppoeSnapshot(); const active = snap.active || []; let t = `๐ŸŸข *PPPoE ONLINE*\n\nTotal Aktif: *${active.length}*\n`; active.slice(0, 20).forEach((s, i) => { t += `${i + 1}. ${(s && (s.name || s.user)) || '-'}\n`; }); if (active.length > 20) t += `\n_...dan ${active.length - 20} lainnya._`; bot.sendMessage(msg.chat.id, t, { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); function tgPendapatanText() { const tr = billingSvc.getTodayRevenue(); const today = Number((tr && tr.total != null) ? tr.total : (tr || 0)) || 0; const stats = billingSvc.getDashboardStats() || {}; const month = Number(stats.thisMonth || 0); return `๐Ÿ’ฐ *PENDAPATAN*\n\n๐Ÿ“… Hari ini: *Rp ${today.toLocaleString('id-ID')}*\n๐Ÿ—“๏ธ Bulan ini: *Rp ${month.toLocaleString('id-ID')}*\nโณ Belum dibayar: *${stats.unpaidCount || 0} tagihan*\n`; } bot.onText(/\/(pendapatan|kas|revenue)/i, async (msg) => { if (!isAdmin(msg)) return; try { bot.sendMessage(msg.chat.id, tgPendapatanText(), { parse_mode: 'Markdown', reply_markup: backBtn }); } catch (e) { bot.sendMessage(msg.chat.id, 'Gagal: ' + e.message); } }); bot.onText(/\/cekterhubung\s+(\S+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const tag = match[1]; try { let dev = null; try { dev = await customerDevice.findDeviceByTag(tag); } catch (e) {} if (!dev) { try { dev = await customerDevice.findDeviceWithTagVariants(tag); } catch (e) {} } if (!dev) return bot.sendMessage(chatId, 'โŒ Perangkat/tag "' + tag + '" tidak ditemukan.'); const m = customerDevice.mapDeviceData(dev, tag, false) || {}; const st = String(m.status || '').toLowerCase() === 'online' ? '๐ŸŸข Online' : '๐Ÿ”ด Offline'; let t = '๐Ÿ“ถ *CEK TERHUBUNG*\n\n'; t += '๐Ÿท๏ธ Tag: ' + tag + '\n'; t += '๐Ÿ“ก Status: ' + st + '\n'; if (m.lastInform) t += '๐Ÿ•’ Last Inform: ' + m.lastInform + '\n'; if (m.ssid && m.ssid !== '-') t += '๐Ÿ“ถ SSID: ' + m.ssid + '\n'; bot.sendMessage(chatId, t, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'Gagal: ' + e.message); } }); bot.onText(/\/ringkasan/i, async (msg) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; try { const stats = billingSvc.getDashboardStats(); const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }); let res = `๐Ÿ’ฐ *RINGKASAN BILLING*\n\n` + `๐Ÿ“ˆ *Total Pendapatan:* ${formatter.format(stats.totalRevenue)}\n` + `๐Ÿ“… *Bulan Ini:* ${formatter.format(stats.thisMonth)}\n` + `โณ *Piutang (Pending):* ${formatter.format(stats.pendingAmount)}\n` + `๐Ÿงพ *Tagihan Belum Lunas:* ${stats.unpaidCount} invoice\n\n` + `๐Ÿ’ก _Gunakan perintah lain untuk detail._`; bot.sendMessage(chatId, res, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal mengambil ringkasan billing: ' + e.message); } }); bot.onText(/\/lunas(?:\s+(.+))?/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const keyRaw = String(match[1] || '').trim(); if (!keyRaw) return bot.sendMessage(chatId, 'โŒ Format: `/lunas IDTAGIHAN` atau `/lunas nama/nohp/pppoe/tag`', { parse_mode: 'Markdown' }); try { let targetInvId = null; let targetInv = null; const isNumeric = /^\d+$/.test(keyRaw); if (isNumeric) { targetInvId = Number(keyRaw); targetInv = billingSvc.getInvoiceById(targetInvId); } if (!targetInv) { let cust = (isNumeric ? customerSvc.getCustomerById(Number(keyRaw)) : null) || customerSvc.findCustomerByAny(keyRaw); if (!cust) { const candidates = customerSvc.getAllCustomers(keyRaw) || []; const unique = Array.from(new Map(candidates.map(c => [c.id, c])).values()); if (unique.length === 1) { cust = customerSvc.getCustomerById(unique[0].id); } else if (unique.length > 1) { const top = unique.slice(0, 5).map(c => `- ID:${c.id} โ€ข ${c.name || '-'} โ€ข ${c.phone || '-'} โ€ข PPPoE:${c.pppoe_username || '-'}` ).join('\n'); return bot.sendMessage(chatId, `โš ๏ธ Nama/ID tidak spesifik. Ditemukan ${unique.length} pelanggan:\n\n${top}\n\nKirim ulang: \`/lunas IDPELANGGAN\``); } } if (cust) { const unpaid = billingSvc.getUnpaidInvoicesByCustomerId(cust.id); if (unpaid && unpaid.length > 0) { targetInv = unpaid[0]; targetInvId = targetInv.id; } else { return bot.sendMessage(chatId, `โœ… Pelanggan *${cust.name}* tidak memiliki tagihan menunggak.`, { parse_mode: 'Markdown' }); } } } if (!targetInv) return bot.sendMessage(chatId, `โŒ Tagihan atau Pelanggan *${keyRaw}* tidak ditemukan.`, { parse_mode: 'Markdown' }); if (targetInvId != null) { const enriched = billingSvc.getInvoiceById(targetInvId); if (enriched) targetInv = enriched; } if (targetInv && targetInv.status === 'paid') { return bot.sendMessage(chatId, `โœ… Invoice *#${targetInv.id}* sudah berstatus LUNAS.`, { parse_mode: 'Markdown' }); } billingSvc.markAsPaid(targetInvId, 'Telegram Bot Admin', 'Paid via Telegram Command'); const customer = customerSvc.getCustomerById(targetInv.customer_id); const formatter = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }); const customerName = String(targetInv.customer_name || customer?.name || targetInv.customer_name || '-'); const notifyTag = customer?.genieacs_tag || customer?.pppoe_username || customer?.phone || targetInv.customer_phone || targetInv.genieacs_tag || ''; const appUrl = (getSetting('public_base_url', '') || '').replace(/\/$/, ''); const portalUrl = appUrl ? `${appUrl}/customer` : ''; const template = db.getAppSetting('whatsapp_payment_success_message', ''); const waMessage = whatsappService.formatPaymentSuccessMessage({ customerName, invoiceId: targetInvId, customerUsername: customer?.pppoe_username || customer?.id || '-', customerCode: customer?.customer_code || '', packageName: targetInv.package_name || '-', periodMonth: targetInv.period_month, periodYear: targetInv.period_year, amount: targetInv.amount, paymentMethod: 'Telegram Admin / Kasir', paidAt: new Date(), companyName: getSetting('company_header', 'OPEN-ISP'), companyPhone: getSetting('company_phone', ''), portalUrl, customTemplate: template }); // Dynamic import of whatsappBot const waBot = await import('./whatsappBot.mjs').catch(() => null); let waNotifStatus = 'tidak dikirim (WA offline)'; if (customer && customer.status === 'suspended') { const freshCustomer = customerSvc.getAllCustomers().find(c => c.id === targetInv.customer_id); const unpaidCount = freshCustomer && Number.isFinite(Number(freshCustomer.unpaid_count)) ? Number(freshCustomer.unpaid_count) : 1; if (unpaidCount === 0) { await customerSvc.activateCustomer(targetInv.customer_id); if (waBot && waBot.currentSock) { const ok = await waBot.notifyCustomer(waBot.currentSock, null, notifyTag, waMessage + `\n\n๐ŸŸข Layanan internet Anda sudah aktif kembali.`); waNotifStatus = ok ? 'terkirim' : 'gagal'; } await bot.sendMessage(chatId, `โœ… Invoice *#${targetInvId}* LUNAS. Pelanggan *${customerName}* otomatis diaktifkan kembali.\n๐Ÿ“ฉ Notif WA pelanggan: ${waNotifStatus}`); } else { if (waBot && waBot.currentSock) { const ok = await waBot.notifyCustomer(waBot.currentSock, null, notifyTag, waMessage + `\n\nโš ๏ธ Masih ada ${unpaidCount} tagihan lain yang belum dibayar.`); waNotifStatus = ok ? 'terkirim' : 'gagal'; } await bot.sendMessage(chatId, `โœ… Invoice *#${targetInvId}* LUNAS. Masih ada ${unpaidCount} tagihan lain.\n๐Ÿ“ฉ Notif WA pelanggan: ${waNotifStatus}`); } } else { if (waBot && waBot.currentSock) { const ok = await waBot.notifyCustomer(waBot.currentSock, null, notifyTag, waMessage); waNotifStatus = ok ? 'terkirim' : 'gagal'; } await bot.sendMessage(chatId, `โœ… Invoice *#${targetInvId}* (a.n ${customerName}) berhasil ditandai LUNAS.\n๐Ÿ“ฉ Notif WA pelanggan: ${waNotifStatus}`); } } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal update status: ' + e.message); } }); bot.onText(/\/generate\s+(\d+)\s+(\d+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const month = Number(match[1]); const year = Number(match[2]); try { const res = billingSvc.generateMonthlyInvoices(month, year); bot.sendMessage(chatId, `โœ… Berhasil generate tagihan untuk periode *${month}/${year}*.\n๐Ÿงพ Jumlah: ${res.generatedCount} tagihan baru.`, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal generate tagihan: ' + e.message); } }); bot.onText(/\/isolir(?:\s+(.+))?/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const targetId = String(match[1] || '').trim(); if (!targetId) return bot.sendMessage(chatId, 'โŒ Format: `/isolir ID/Nama/PPPoE/NoHP`'); try { const cust = customerSvc.findCustomerByAny(targetId); if (!cust) return bot.sendMessage(chatId, `โŒ Pelanggan *${targetId}* tidak ditemukan.`, { parse_mode: 'Markdown' }); await customerSvc.suspendCustomer(cust.id); bot.sendMessage(chatId, `โœ… Pelanggan *${cust.name}* (ID: ${cust.id}) berhasil di-isolir.`, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal isolir: ' + e.message); } }); bot.onText(/\/buka(?:\s+(.+))?/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const targetId = String(match[1] || '').trim(); if (!targetId) return bot.sendMessage(chatId, 'โŒ Format: `/buka ID/Nama/PPPoE/NoHP`'); try { const cust = customerSvc.findCustomerByAny(targetId); if (!cust) return bot.sendMessage(chatId, `โŒ Pelanggan *${targetId}* tidak ditemukan.`, { parse_mode: 'Markdown' }); await customerSvc.activateCustomer(cust.id); bot.sendMessage(chatId, `โœ… Pelanggan *${cust.name}* (ID: ${cust.id}) berhasil diaktifkan kembali.`, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal mengaktifkan pelanggan: ' + e.message); } }); bot.onText(/\/listonu/i, async (msg) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; try { let res = await customerDevice.listDevicesWithTags(50); if (!res.ok || res.devices.length === 0) { res = await customerDevice.listAllDevices(50); } if (!res.ok || res.devices.length === 0) { return bot.sendMessage(chatId, '๐Ÿ“ญ Tidak ada perangkat ONU terdeteksi di GenieACS.'); } let txt = `*๐Ÿ“ก DAFTAR ONU (GenieACS)*\n\n`; res.devices.forEach(d => { const id = d._id || 'Unknown ID'; const tags = Array.isArray(d._tags) ? d._tags.join(', ') : (d._tags || '-'); txt += `โ€ข \`${id}\`\n โ”” Tag: ${tags}\n`; }); bot.sendMessage(chatId, txt, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal list ONU: ' + e.message); } }); bot.onText(/\/(?:info|cekstatus)\s+(\S+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const inputTag = match[1]; try { const cust = customerSvc.findCustomerByAny(inputTag); const targetTag = cust ? (cust.genieacs_tag || cust.pppoe_username || cust.phone || inputTag) : inputTag; const targetDevice = await customerDevice.resolveDeviceToken(targetTag); if (!targetDevice) { return bot.sendMessage(chatId, `โŒ Target *${inputTag}* tidak ditemukan di GenieACS.`); } const data = await customerDevice.getCustomerDeviceData(targetTag); let t = `*๐Ÿ“ก DETAIL ONU: ${data.lokasi || targetTag}*\n\n`; t += `๐ŸŸข Status: ${data.status || '-'}\n`; t += `๐Ÿ“ถ SSID: ${data.ssid || '-'}\n`; t += `โฑ๏ธ Last Inform: ${data.lastInform || '-'}\n`; t += `๐Ÿ“ก RX Power: ${data.rxPower || '-'}\n`; t += `๐ŸŒ PPPoE IP: ${data.pppoeIP || '-'}\n`; t += `๐Ÿ‘ค PPPoE User: ${data.pppoeUsername || '-'}\n`; t += `โณ Uptime: ${data.uptime || '-'}\n`; t += `โณ PPPoE Uptime: ${data.pppoeUptime || '-'}\n`; t += `๐Ÿ“ฑ User WiFi: ${data.totalAssociations || '0'}\n`; t += `๐Ÿ”ง Model: ${data.model || '-'}\n`; t += `๐Ÿท๏ธ Serial Number: ${data.serialNumber || '-'}\n`; t += `๐Ÿ’พ Firmware: ${data.softwareVersion || '-'}`; bot.sendMessage(chatId, t, { parse_mode: 'Markdown' }); } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal cek status ONU: ' + e.message); } }); bot.onText(/\/reboot\s+(\S+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const inputTag = match[1]; try { const cust = customerSvc.findCustomerByAny(inputTag); const targetTag = cust ? (cust.genieacs_tag || cust.pppoe_username || cust.phone || inputTag) : inputTag; const targetDevice = await customerDevice.resolveDeviceToken(targetTag); if (!targetDevice) { return bot.sendMessage(chatId, `โŒ Target *${inputTag}* tidak ditemukan di GenieACS.`); } await bot.sendMessage(chatId, `โณ Mencoba me-reboot ONU *${targetTag}*...`, { parse_mode: 'Markdown' }); const ok = await customerDevice.rebootDevice(targetTag); if (ok) { bot.sendMessage(chatId, `โœ… ONU *${targetTag}* berhasil diperintahkan untuk reboot.`, { parse_mode: 'Markdown' }); } else { bot.sendMessage(chatId, `โŒ Gagal reboot ONU *${targetTag}*.`, { parse_mode: 'Markdown' }); } } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal reboot ONU: ' + e.message); } }); bot.onText(/\/gantissid\s+(\S+)\s+(.+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const inputTag = match[1]; const newSSID = match[2]; try { const cust = customerSvc.findCustomerByAny(inputTag); const targetTag = cust ? (cust.genieacs_tag || cust.pppoe_username || cust.phone || inputTag) : inputTag; const targetDevice = await customerDevice.resolveDeviceToken(targetTag); if (!targetDevice) { return bot.sendMessage(chatId, `โŒ Target *${inputTag}* tidak ditemukan di GenieACS.`); } await bot.sendMessage(chatId, `โณ Mengubah SSID untuk *${targetTag}* menjadi *${newSSID}*...`, { parse_mode: 'Markdown' }); const ok = await customerDevice.updateSSID(targetTag, newSSID); if (ok) { bot.sendMessage(chatId, `โœ… SSID untuk *${targetTag}* berhasil diubah menjadi *${newSSID}*.`, { parse_mode: 'Markdown' }); } else { bot.sendMessage(chatId, `โŒ Gagal mengubah SSID untuk *${targetTag}*.`, { parse_mode: 'Markdown' }); } } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal ganti SSID: ' + e.message); } }); bot.onText(/\/gantisandi\s+(\S+)\s+(.+)/i, async (msg, match) => { if (!isAdmin(msg)) return; const chatId = msg.chat.id; const inputTag = match[1]; const newPass = match[2]; try { const cust = customerSvc.findCustomerByAny(inputTag); const targetTag = cust ? (cust.genieacs_tag || cust.pppoe_username || cust.phone || inputTag) : inputTag; const targetDevice = await customerDevice.resolveDeviceToken(targetTag); if (!targetDevice) { return bot.sendMessage(chatId, `โŒ Target *${inputTag}* tidak ditemukan di GenieACS.`); } if (newPass.length < 8) { return bot.sendMessage(chatId, `โš ๏ธ Password WiFi minimal harus 8 karakter.`); } await bot.sendMessage(chatId, `โณ Mengubah sandi WiFi untuk *${targetTag}*...`); const ok = await customerDevice.updatePassword(targetTag, newPass); if (ok) { bot.sendMessage(chatId, `โœ… Sandi WiFi untuk *${targetTag}* berhasil diubah menjadi: \`${newPass}\``, { parse_mode: 'Markdown' }); } else { bot.sendMessage(chatId, `โŒ Gagal mengubah sandi WiFi untuk *${targetTag}*.`); } } catch (e) { bot.sendMessage(chatId, 'โŒ Gagal ganti sandi WiFi: ' + e.message); } }); bot.on('polling_error', (error) => { logger.error('Telegram Polling Error:', error.message); }); } // Export for manual re-init from settings async function notifyAdmin(text) { try { const adminId = String(getSetting('telegram_admin_id', '') || '').trim(); if (!bot || !adminId || !text) return false; const ids = adminId.split(/[,s]+/).filter(Boolean); let ok = false; for (const id of ids) { try { await bot.sendMessage(id, text, { parse_mode: 'Markdown' }); ok = true; } catch (e) { try { await bot.sendMessage(id, String(text)); ok = true; } catch (e2) {} } } return ok; } catch (e) { return false; } } module.exports = { initTelegram, notifyAdmin };