Files
smanab/backend/services/fonnteQueue.js
2026-02-22 14:54:55 +08:00

259 lines
9.5 KiB
JavaScript
Executable File

// Fonnte WhatsApp Message Queue Service
// Mengirim pesan satu per satu dengan delay acak dan simulasi mengetik (typing indicator)
// Untuk menghindari deteksi spam dan pembatasan akun WhatsApp
// Optimized untuk 1000+ siswa - OPSI B (Moderat)
import axios from 'axios';
// ==================== CONFIGURATION - OPSI B (MODERAT) ====================
// Konfigurasi optimal untuk ~656-1000 siswa dengan 1 device
// Estimasi: ~140 pesan/jam, 656 siswa selesai dalam ~5 jam
// Delay Configuration (dalam milidetik)
const MIN_DELAY_BETWEEN_MESSAGES = 4000; // Minimum 4 detik
const MAX_DELAY_BETWEEN_MESSAGES = 9000; // Maximum 9 detik
// Rate Limiting
const MAX_MESSAGES_PER_HOUR = 140; // ~140 pesan per jam
// Typing configuration - dalam detik (sesuai Fonnte API)
const MIN_TYPING_DURATION = 2; // Minimum 2 detik mengetik
const MAX_TYPING_DURATION = 4; // Maximum 4 detik mengetik
// Batch configuration - istirahat setiap X pesan untuk menghindari deteksi pola
const BATCH_SIZE = 20; // Setiap 20 pesan
const BATCH_REST_DURATION = 45000; // Istirahat 45 detik
// Message Queue
const messageQueue = [];
let isProcessing = false;
let messagesSentThisHour = 0;
let hourStartTime = Date.now();
// Variasi salam dengan penutup yang berpasangan
const GREETING_PAIRS = [
{ greeting: 'Om Swastyastu', closing: 'Om Shanti, Shanti, Shanti, Om' },
{ greeting: 'Selamat pagi', closing: 'Terima kasih.' },
{ greeting: 'Selamat siang', closing: 'Terima kasih.' },
{ greeting: 'Halo Bapak/Ibu', closing: 'Salam hormat.' },
{ greeting: 'Salam sejahtera', closing: 'Hormat kami.' },
{ greeting: 'Kepada Yth. Orang Tua/Wali', closing: 'Matur suksma.' }
];
// Get random pair
const getRandomPair = () => GREETING_PAIRS[Math.floor(Math.random() * GREETING_PAIRS.length)];
// Add variation to message
export const addMessageVariation = (message) => {
const pair = getRandomPair();
let variedMessage = message;
// Add greeting at start
variedMessage = pair.greeting + ',\n\n' + variedMessage;
// Replace closing
variedMessage = variedMessage.replace(/Terima kasih\./gi, pair.closing);
return variedMessage;
};
// Sleep helper
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Random delay helper - memberikan jeda acak untuk lebih natural
const getRandomDelay = () => {
const delay = Math.floor(Math.random() * (MAX_DELAY_BETWEEN_MESSAGES - MIN_DELAY_BETWEEN_MESSAGES + 1)) + MIN_DELAY_BETWEEN_MESSAGES;
return delay;
};
// Random typing duration - dalam detik
const getRandomTypingDuration = () => {
return Math.floor(Math.random() * (MAX_TYPING_DURATION - MIN_TYPING_DURATION + 1)) + MIN_TYPING_DURATION;
};
// Reset hourly counter
const checkHourlyReset = () => {
if (Date.now() - hourStartTime > 3600000) {
messagesSentThisHour = 0;
hourStartTime = Date.now();
console.log('📱 [Fonnte] Hourly counter reset');
}
};
// Add message to queue (normal priority)
export const addToQueue = (phone, message, token, priority = 'normal') => {
if (!token || !phone || phone.length < 5) {
console.log('📱 [Fonnte] Invalid phone or token, skipping');
return false;
}
checkHourlyReset();
// Check hourly limit
if (messagesSentThisHour >= MAX_MESSAGES_PER_HOUR) {
console.log(`📱 [Fonnte] Hourly limit reached (${MAX_MESSAGES_PER_HOUR}). Skipping.`);
return false;
}
// Clean phone number
const cleanPhone = phone.replace(/[^\d]/g, '');
// Check duplicate
const existingIndex = messageQueue.findIndex(m => m.phone === cleanPhone);
if (existingIndex >= 0) {
console.log(`📱 [Fonnte] Phone ${cleanPhone} already in queue, updating`);
messageQueue[existingIndex].message = message;
return true;
}
// Add variation to message before queueing
const variedMessage = addMessageVariation(message);
const queueItem = {
phone: cleanPhone,
message: variedMessage,
token,
priority,
addedAt: Date.now()
};
// Priority handling: HIGH priority messages go to front of queue
if (priority === 'high') {
// Find the position after all existing high priority messages
const lastHighPriorityIndex = messageQueue.findLastIndex(m => m.priority === 'high');
if (lastHighPriorityIndex >= 0) {
messageQueue.splice(lastHighPriorityIndex + 1, 0, queueItem);
} else {
messageQueue.unshift(queueItem); // Add to front if no high priority exists
}
console.log(`📱 [Fonnte] ⚡ HIGH PRIORITY Queued (Guru BK). Size: ${messageQueue.length}`);
} else {
messageQueue.push(queueItem);
console.log(`📱 [Fonnte] Queued. Size: ${messageQueue.length}, This hour: ${messagesSentThisHour}/${MAX_MESSAGES_PER_HOUR}`);
}
if (!isProcessing) {
processQueue();
}
return true;
};
// Add message to queue with HIGH priority (for Guru BK notifications)
export const addToQueuePriority = (phone, message, token) => {
return addToQueue(phone, message, token, 'high');
};
// Process queue
const processQueue = async () => {
if (isProcessing) return;
isProcessing = true;
console.log('📱 [Fonnte] Starting queue processor...');
let messageCount = 0;
let batchCount = 0; // Counter untuk batch rest
while (messageQueue.length > 0) {
checkHourlyReset();
if (messagesSentThisHour >= MAX_MESSAGES_PER_HOUR) {
console.log(`📱 [Fonnte] Hourly limit. Waiting...`);
await sleep(60000); // Wait 1 minute then check again
continue;
}
const item = messageQueue.shift();
messageCount++;
batchCount++;
// Skip expired (> 2 hours old - diperpanjang untuk handling antrian besar)
if (Date.now() - item.addedAt > 7200000) {
console.log(`📱 [Fonnte] Expired: ${item.phone}`);
continue;
}
try {
// Generate random typing duration untuk simulasi mengetik
const typingDuration = getRandomTypingDuration();
console.log(`📱 [Fonnte] Sending #${messageCount} to ${item.phone} (typing: ${typingDuration}s)...`);
// Kirim pesan dengan typing indicator
// Parameter 'typing': menampilkan status "sedang mengetik" sebelum pesan terkirim
// Ini membuat pesan terlihat seperti diketik manual oleh manusia
// IMPORTANT: Fonnte API membutuhkan format form-urlencoded, bukan JSON
const formData = new URLSearchParams();
formData.append('target', item.phone);
formData.append('message', item.message);
formData.append('typing', 'true');
formData.append('delay', typingDuration.toString());
formData.append('countryCode', '62'); // Indonesia country code
const response = await axios.post('https://api.fonnte.com/send',
formData.toString(),
{
headers: {
'Authorization': item.token,
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 60000
}
);
// Check Fonnte API response
const fonnteResponse = response.data;
if (fonnteResponse.status === true || fonnteResponse.status === 'true') {
messagesSentThisHour++;
console.log(`📱 [Fonnte] ✓ Sent #${messageCount} (${messagesSentThisHour}/${MAX_MESSAGES_PER_HOUR} this hour)`);
} else {
// Fonnte returned error - log the actual reason
console.error(`📱 [Fonnte] ✗ REJECTED #${messageCount} to ${item.phone}`);
console.error(`📱 [Fonnte] ⚠️ Reason: ${fonnteResponse.reason || fonnteResponse.message || JSON.stringify(fonnteResponse)}`);
console.error(`📱 [Fonnte] 🔑 Token used (first 10 chars): ${item.token?.substring(0, 10)}...`);
}
} catch (error) {
console.error(`📱 [Fonnte] ✗ Failed: ${item.phone} - ${error.message}`);
}
// Check if we need batch rest (istirahat setelah X pesan)
if (batchCount >= BATCH_SIZE && messageQueue.length > 0) {
const restSeconds = Math.round(BATCH_REST_DURATION / 1000);
console.log(`📱 [Fonnte] 🛑 Batch rest (${batchCount} messages sent). Resting ${restSeconds}s...`);
await sleep(BATCH_REST_DURATION);
batchCount = 0; // Reset batch counter
}
// Random delay sebelum pesan berikutnya untuk menghindari pola yang terdeteksi sebagai bot
else if (messageQueue.length > 0) {
const nextDelay = getRandomDelay();
const delaySeconds = Math.round(nextDelay / 1000);
console.log(`📱 [Fonnte] Waiting ${delaySeconds}s before next message... Remaining: ${messageQueue.length}`);
await sleep(nextDelay);
}
}
isProcessing = false;
console.log(`📱 [Fonnte] ✅ Done. Total: ${messageCount}`);
};
// Status
export const getQueueStatus = () => {
checkHourlyReset();
return {
queueSize: messageQueue.length,
isProcessing,
messagesSentThisHour,
maxPerHour: MAX_MESSAGES_PER_HOUR
};
};
// Clear
export const clearQueue = () => {
const count = messageQueue.length;
messageQueue.length = 0;
return count;
};
export default { addToQueue, addToQueuePriority, getQueueStatus, clearQueue, addMessageVariation };