259 lines
9.5 KiB
JavaScript
Executable File
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 };
|