OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
This commit is contained in:
commit
eaf05f2811
280 files changed
+123975
No files matched your search
@@ -0,0 +1,283 @@
|
||||
const { logger } = require('../config/logger');
|
||||
const { getSetting } = require('../config/settingsManager');
|
||||
const db = require('../config/database');
|
||||
|
||||
/**
|
||||
* Service untuk Meta WhatsApp Cloud API (Official Meta Graph API)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format nomor telepon ke format E.164 tanpa tanda '+' (misal 628123456789)
|
||||
*/
|
||||
function normalizePhone(phone) {
|
||||
let cleaned = String(phone || '').replace(/\D/g, '');
|
||||
if (!cleaned) return '';
|
||||
if (cleaned.startsWith('0')) {
|
||||
cleaned = '62' + cleaned.slice(1);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifikasi Webhook dari Meta (GET request saat pendaftaran Webhook di Meta Developer Console)
|
||||
*/
|
||||
function verifyWebhook(req, res) {
|
||||
try {
|
||||
const mode = req.query['hub.mode'];
|
||||
const token = req.query['hub.verify_token'];
|
||||
const challenge = req.query['hub.challenge'];
|
||||
|
||||
const configuredVerifyToken = getSetting('meta_verify_token', 'antigravity_meta_wa_secret');
|
||||
|
||||
if (mode === 'subscribe' && token === configuredVerifyToken) {
|
||||
logger.info('[Meta WA Webhook] Webhook successfully verified by Meta!');
|
||||
return res.status(200).send(challenge);
|
||||
} else {
|
||||
logger.warn('[Meta WA Webhook] Webhook verification failed! Invalid token.');
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[Meta WA Webhook] Verification error:', err.message);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memproses Event Masuk dari Meta Webhook (Pesan Baru & Status Delivery)
|
||||
*/
|
||||
async function processWebhookEvent(req, res) {
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
if (body.object !== 'whatsapp_business_account') {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
const entries = body.entry || [];
|
||||
for (const entry of entries) {
|
||||
const changes = entry.changes || [];
|
||||
for (const change of changes) {
|
||||
const value = change.value;
|
||||
if (!value) continue;
|
||||
|
||||
// 1. Tangani Pesan Masuk dari Pelanggan
|
||||
if (value.messages && value.messages.length > 0) {
|
||||
for (const msg of value.messages) {
|
||||
const senderPhone = msg.from; // format: 628123456789
|
||||
const msgId = msg.id;
|
||||
let msgText = '';
|
||||
|
||||
if (msg.type === 'text' && msg.text) {
|
||||
msgText = msg.text.body;
|
||||
} else if (msg.type === 'button' && msg.button) {
|
||||
msgText = msg.button.text;
|
||||
} else if (msg.type === 'interactive' && msg.interactive) {
|
||||
msgText = msg.interactive.button_reply?.title || msg.interactive.list_reply?.title || 'Interactive response';
|
||||
} else {
|
||||
msgText = `[Pesan ${msg.type}]`;
|
||||
}
|
||||
|
||||
logger.info(`[Meta WA Webhook] Pesan masuk dari ${senderPhone}: "${msgText}"`);
|
||||
|
||||
// Cari nama pelanggan dari DB jika ada
|
||||
let customerName = 'Pelanggan';
|
||||
let customerId = null;
|
||||
try {
|
||||
const cust = db.prepare('SELECT id, name FROM customers WHERE phone LIKE ? OR phone LIKE ?').get(`%${senderPhone.slice(-8)}%`, `%${senderPhone}%`);
|
||||
if (cust) {
|
||||
customerName = cust.name;
|
||||
customerId = cust.id;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Simpan ke tabel wa_chat_messages
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO wa_chat_messages
|
||||
(direction, gateway, sender_phone, recipient_phone, customer_id, customer_name, message_text, status, meta_message_id)
|
||||
VALUES ('inbound', 'meta', ?, ?, ?, ?, ?, 'read', ?)
|
||||
`).run(senderPhone, getSetting('meta_business_phone', ''), customerId, customerName, msgText, msgId);
|
||||
} catch (dbErr) {
|
||||
logger.error('[Meta WA DB Error] Gagal simpan pesan masuk:', dbErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tangani Update Status Pengiriman (Sent, Delivered, Read, Failed)
|
||||
if (value.statuses && value.statuses.length > 0) {
|
||||
for (const statusObj of value.statuses) {
|
||||
const statusId = statusObj.id;
|
||||
const newStatus = statusObj.status; // sent, delivered, read, failed
|
||||
|
||||
try {
|
||||
db.prepare(`UPDATE wa_chat_messages SET status = ? WHERE meta_message_id = ?`).run(newStatus, statusId);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send('EVENT_RECEIVED');
|
||||
} catch (err) {
|
||||
logger.error('[Meta WA Webhook] Error processing webhook event:', err.message);
|
||||
return res.status(500).send('ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim Pesan Teks Bebas via Meta Cloud API (Graph API)
|
||||
*/
|
||||
async function sendMetaTextMessage(toPhone, messageText) {
|
||||
const phone = normalizePhone(toPhone);
|
||||
if (!phone) throw new Error('Nomor tujuan tidak valid.');
|
||||
|
||||
const phoneNumberId = getSetting('meta_phone_number_id', '');
|
||||
const accessToken = getSetting('meta_access_token', '');
|
||||
|
||||
if (!phoneNumberId || !accessToken) {
|
||||
throw new Error('Meta Cloud API belum dikonfigurasi. Harap isi Phone Number ID dan Access Token di Pengaturan WA.');
|
||||
}
|
||||
|
||||
const url = `https://graph.facebook.com/v18.0/${phoneNumberId}/messages`;
|
||||
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: phone,
|
||||
type: 'text',
|
||||
text: {
|
||||
preview_url: false,
|
||||
body: String(messageText || '')
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const resData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const errMessage = resData?.error?.message || resData?.error?.error_user_msg || JSON.stringify(resData);
|
||||
logger.error(`[Meta WA API Error] ${errMessage}`);
|
||||
throw new Error(`Meta API Error: ${errMessage}`);
|
||||
}
|
||||
|
||||
const metaMsgId = resData?.messages?.[0]?.id || '';
|
||||
|
||||
// Simpan ke DB Chat Log
|
||||
try {
|
||||
let custName = 'Pelanggan';
|
||||
let custId = null;
|
||||
const cust = db.prepare('SELECT id, name FROM customers WHERE phone LIKE ? OR phone LIKE ?').get(`%${phone.slice(-8)}%`, `%${phone}%`);
|
||||
if (cust) {
|
||||
custName = cust.name;
|
||||
custId = cust.id;
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO wa_chat_messages
|
||||
(direction, gateway, sender_phone, recipient_phone, customer_id, customer_name, message_text, status, meta_message_id)
|
||||
VALUES ('outbound', 'meta', ?, ?, ?, ?, ?, 'sent', ?)
|
||||
`).run(getSetting('meta_business_phone', ''), phone, custId, custName, messageText, metaMsgId);
|
||||
} catch (e) {}
|
||||
|
||||
return { success: true, messageId: metaMsgId, raw: resData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim Pesan Template Resmi via Meta Cloud API
|
||||
* @param {string} toPhone
|
||||
* @param {string} templateName Nama template terdaftar di Meta
|
||||
* @param {string} languageCode Kode bahasa, misal 'id'
|
||||
* @param {Array} parameters Array parameter string misal ["Budi", "Rp 150.000", "10 Aug"]
|
||||
*/
|
||||
async function sendMetaTemplateMessage(toPhone, templateName, languageCode = 'id', parameters = []) {
|
||||
const phone = normalizePhone(toPhone);
|
||||
if (!phone) throw new Error('Nomor tujuan tidak valid.');
|
||||
|
||||
const phoneNumberId = getSetting('meta_phone_number_id', '');
|
||||
const accessToken = getSetting('meta_access_token', '');
|
||||
|
||||
if (!phoneNumberId || !accessToken) {
|
||||
throw new Error('Meta Cloud API belum dikonfigurasi. Harap isi Phone Number ID dan Access Token di Pengaturan WA.');
|
||||
}
|
||||
|
||||
const url = `https://graph.facebook.com/v18.0/${phoneNumberId}/messages`;
|
||||
|
||||
const components = [];
|
||||
if (Array.isArray(parameters) && parameters.length > 0) {
|
||||
components.push({
|
||||
type: 'body',
|
||||
parameters: parameters.map(val => ({
|
||||
type: 'text',
|
||||
text: String(val || '')
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
to: phone,
|
||||
type: 'template',
|
||||
template: {
|
||||
name: templateName,
|
||||
language: {
|
||||
code: languageCode
|
||||
},
|
||||
components: components
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const resData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const errMessage = resData?.error?.message || resData?.error?.error_user_msg || JSON.stringify(resData);
|
||||
logger.error(`[Meta WA API Error] ${errMessage}`);
|
||||
throw new Error(`Meta API Error: ${errMessage}`);
|
||||
}
|
||||
|
||||
const metaMsgId = resData?.messages?.[0]?.id || '';
|
||||
|
||||
// Simpan ke DB Chat Log
|
||||
try {
|
||||
let custName = 'Pelanggan';
|
||||
let custId = null;
|
||||
const cust = db.prepare('SELECT id, name FROM customers WHERE phone LIKE ? OR phone LIKE ?').get(`%${phone.slice(-8)}%`, `%${phone}%`);
|
||||
if (cust) {
|
||||
custName = cust.name;
|
||||
custId = cust.id;
|
||||
}
|
||||
|
||||
const summaryText = `[Template Meta: ${templateName}] Parameter: ${parameters.join(', ')}`;
|
||||
db.prepare(`
|
||||
INSERT INTO wa_chat_messages
|
||||
(direction, gateway, sender_phone, recipient_phone, customer_id, customer_name, message_text, status, meta_message_id)
|
||||
VALUES ('outbound', 'meta', ?, ?, ?, ?, ?, 'sent', ?)
|
||||
`).run(getSetting('meta_business_phone', ''), phone, custId, custName, summaryText, metaMsgId);
|
||||
} catch (e) {}
|
||||
|
||||
return { success: true, messageId: metaMsgId, raw: resData };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizePhone,
|
||||
verifyWebhook,
|
||||
processWebhookEvent,
|
||||
sendMetaTextMessage,
|
||||
sendMetaTemplateMessage
|
||||
};
|
||||
Reference in new issue
Block a user