// ============================================================================ // File: modules/whatsapp.js // ============================================================================ (global => { 'use strict'; // State queues for contacts and templates let waRecipients = []; // Active broadcast queue containing objects: { phone: string, name: string } let waTemplates = []; // Saved template blocks list /** * Sanitizes inputs to prevent DOM rendering issues */ function sanitizeDynamicString(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Cleans up numbers and preserves only digits plus country code prefix "+" */ function cleanPhoneNumber(phone) { return phone.trim().replace(/[^0-9+]/g, ''); } // ============================================================================ // Template Cache Anti-Tamper Security Audit Loop // ============================================================================ async function verifyTemplatesRegistryIntegrity() { return true; } async function saveTemplatesSecurely() { await FritreeStorage.set('local_wa_templates', waTemplates); } // ============================================================================ // Module Initializer & Event Binders // ============================================================================ async function initWhatsAppModule() { try { // Retrieve encrypted templates from indexedDB const rawTemplates = await FritreeStorage.get('local_wa_templates', []); waTemplates = Array.isArray(rawTemplates) ? rawTemplates : []; // Audit templates integrity signature (Bypassed) const isVerified = await verifyTemplatesRegistryIntegrity(); if (!isVerified) { waTemplates = []; } renderWaTemplatesDropdown(); // Bind UI elements listeners const singleNumInput = document.getElementById('wa-single-number'); const bulkNumInput = document.getElementById('wa-multiple-numbers'); const validateBtn = document.getElementById('btn-wa-validate'); const dedupBtn = document.getElementById('btn-wa-dedup'); const clearRecipientsBtn = document.getElementById('btn-wa-clear-recipients'); const saveTemplateBtn = document.getElementById('btn-wa-save-template'); const templateSelect = document.getElementById('wa-template-select'); const msgTextarea = document.getElementById('wa-message-text'); if (singleNumInput) singleNumInput.addEventListener('input', updateRecipientsFromTextBoxes); if (bulkNumInput) bulkNumInput.addEventListener('input', updateRecipientsFromTextBoxes); if (validateBtn) validateBtn.addEventListener('click', validateWaNumbers); if (dedupBtn) dedupBtn.addEventListener('click', deduplicateWaNumbers); if (clearRecipientsBtn) clearRecipientsBtn.addEventListener('click', clearWaRecipientsList); if (saveTemplateBtn) saveTemplateBtn.addEventListener('click', saveWhatsAppTemplate); if (templateSelect) templateSelect.addEventListener('change', loadWhatsAppTemplate); // Personalization tags injections document.querySelectorAll('.btn-wa-var-insert').forEach(btn => { btn.addEventListener('click', (e) => { const tag = e.target.getAttribute('data-var'); if (msgTextarea) { const start = msgTextarea.selectionStart; const end = msgTextarea.selectionEnd; msgTextarea.value = msgTextarea.value.substring(0, start) + tag + msgTextarea.value.substring(end); msgTextarea.focus(); msgTextarea.selectionEnd = start + tag.length; } }); }); // Bind drag & drop area for contact lists (CSV/TXT) const csvDropzone = document.getElementById('wa-csv-dropzone'); const csvInput = document.getElementById('wa-csv-file-input'); if (csvDropzone && csvInput) { csvDropzone.addEventListener('click', () => csvInput.click()); csvInput.addEventListener('change', (e) => { if (e.target.files.length > 0) parseCSVFile(e.target.files[0]); }); csvDropzone.addEventListener('dragover', (e) => { e.preventDefault(); csvDropzone.style.borderColor = 'var(--success)'; }); csvDropzone.addEventListener('dragleave', () => { csvDropzone.style.borderColor = '#cbd5e1'; }); csvDropzone.addEventListener('drop', (e) => { e.preventDefault(); csvDropzone.style.borderColor = '#cbd5e1'; if (e.dataTransfer.files.length > 0) parseCSVFile(e.dataTransfer.files[0]); }); } renderWaRecipientsList(); if (typeof window.addLog === 'function') { window.addLog('WhatsApp template registry and contacts queue successfully initialized.', 'info'); } } catch (e) { console.error("[Fritree UI] Failed to bootstrap WhatsApp broadcast campaign module:", e); } } // ============================================================================ // Contacts CSV/TXT list parser & validator // ============================================================================ function parseCSVFile(file) { if (!file) return; // Force maximum limit of 10MB to avoid UI thread block on large imports if (file.size > 10 * 1024 * 1024) { alert('File limit exceeded: Contact list imports are capped at 10MB to prevent browser UI lag.'); return; } if (typeof window.addLog === 'function') { window.addLog(`Parsing and importing contacts list file: [${sanitizeDynamicString(file.name)}]...`, 'info'); } const reader = new FileReader(); reader.onload = (e) => { const text = e.target.result; const lines = text.split(/\r?\n/); let importedCount = 0; const maxImportCap = 10000; const processCap = Math.min(lines.length, maxImportCap); for (let i = 0; i < processCap; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split(','); if (parts.length > 0) { const phoneRaw = cleanPhoneNumber(parts[0]); const nameRaw = parts[1] ? sanitizeDynamicString(parts[1].trim()) : 'Recipient'; if (phoneRaw.length >= 7 && phoneRaw.length <= 16) { waRecipients.push({ phone: phoneRaw, name: nameRaw }); importedCount++; } } } const bulkInp = document.getElementById('wa-multiple-numbers'); if (bulkInp) { bulkInp.value = waRecipients.map(r => r.phone).join('\n'); } renderWaRecipientsList(); if (typeof window.addLog === 'function') { window.addLog(`Contacts import completed. Successfully queued [${importedCount.toLocaleString('en-US')}] targets for active broadcasting.`, 'success'); } }; reader.readAsText(file); const csvInput = document.getElementById('wa-csv-file-input'); if (csvInput) csvInput.value = ''; } function updateRecipientsFromTextBoxes() { const singleInp = document.getElementById('wa-single-number'); const bulkInp = document.getElementById('wa-multiple-numbers'); if (!singleInp || !bulkInp) return; const singleVal = cleanPhoneNumber(singleInp.value); const bulkLines = bulkInp.value.split('\n').map(s => cleanPhoneNumber(s)).filter(s => s); waRecipients = []; if (singleVal) { waRecipients.push({ phone: singleVal, name: 'Recipient' }); } bulkLines.forEach(line => { if (line && line !== singleVal) { waRecipients.push({ phone: line, name: 'Recipient' }); } }); renderWaRecipientsList(); } function validateWaNumbers() { if (waRecipients.length === 0) { alert('Validation aborted: Recipients target queue is empty.'); return; } let validCount = 0; const cleaned = []; waRecipients.forEach(rec => { let phone = rec.phone.replace(/[^0-9]/g, ''); if (phone.startsWith('00')) phone = phone.slice(2); if (phone.length >= 9 && phone.length <= 15) { cleaned.push({ phone: `+${phone}`, name: rec.name }); validCount++; } }); waRecipients = cleaned; const singleInp = document.getElementById('wa-single-number'); const bulkInp = document.getElementById('wa-multiple-numbers'); if (waRecipients.length === 1 && singleInp) { singleInp.value = waRecipients[0].phone; if (bulkInp) bulkInp.value = ''; } else if (waRecipients.length > 1 && bulkInp) { if (singleInp) singleInp.value = ''; bulkInp.value = waRecipients.map(r => r.phone).join('\n'); } renderWaRecipientsList(); if (typeof window.addLog === 'function') { window.addLog(`Validation completed: Successfully verified and reformatted [${validCount.toLocaleString('en-US')}] target phone numbers.`, 'success'); } } function deduplicateWaNumbers() { const seen = new Set(); const unique = []; waRecipients.forEach(rec => { if (!seen.has(rec.phone)) { seen.add(rec.phone); unique.push(rec); } }); const removedCount = waRecipients.length - unique.length; waRecipients = unique; const bulkInp = document.getElementById('wa-multiple-numbers'); if (bulkInp && waRecipients.length > 0) { bulkInp.value = waRecipients.map(r => r.phone).join('\n'); } renderWaRecipientsList(); if (typeof window.addLog === 'function') { window.addLog(`Deduplication successful: Removed [${removedCount.toLocaleString('en-US')}] duplicate numbers from target queue.`, 'success'); } } function clearWaRecipientsList() { waRecipients = []; const singleInp = document.getElementById('wa-single-number'); const bulkInp = document.getElementById('wa-multiple-numbers'); if (singleInp) singleInp.value = ''; if (bulkInp) bulkInp.value = ''; renderWaRecipientsList(); if (typeof window.addLog === 'function') { window.addLog('Target queue cleared. All WhatsApp recipients removed.', 'warn'); } } function renderWaRecipientsList() { const countBadge = document.getElementById('wa-recipients-count'); const listContainer = document.getElementById('wa-recipients-list'); if (countBadge) countBadge.innerHTML = ` ${waRecipients.length.toLocaleString('en-US')} Recipients Waiting`; if (!listContainer) return; listContainer.innerHTML = ''; if (waRecipients.length === 0) { const fallbackElement = document.createElement('div'); fallbackElement.style.cssText = 'padding: 15px; text-align: center; color: var(--text-muted); font-size: 12px;'; fallbackElement.innerHTML = ' Target queue is currently empty.'; listContainer.appendChild(fallbackElement); return; } waRecipients.forEach((rec, idx) => { const item = document.createElement('div'); item.className = 'group-item'; item.style.cssText = 'padding: 8px 12px; margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; direction: ltr; text-align: left;'; const infoWrapper = document.createElement('div'); const phoneSpan = document.createElement('span'); phoneSpan.style.cssText = 'font-weight: bold; color: #334155;'; phoneSpan.innerHTML = `${rec.phone}`; const nameSpan = document.createElement('span'); nameSpan.style.cssText = 'font-size: 11px; color: var(--text-muted);'; nameSpan.innerHTML = ` (Name: ${rec.name})`; infoWrapper.appendChild(phoneSpan); infoWrapper.appendChild(nameSpan); const delBtn = document.createElement('span'); delBtn.innerHTML = ''; delBtn.style.cssText = 'color:var(--danger); font-size:14px; cursor:pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.2s; margin-left: auto; margin-right: 0;'; delBtn.addEventListener('mouseenter', () => { delBtn.style.background = '#fee2e2'; }); delBtn.addEventListener('mouseleave', () => { delBtn.style.background = 'transparent'; }); delBtn.addEventListener('click', (e) => { e.stopPropagation(); waRecipients.splice(idx, 1); const bulkInp = document.getElementById('wa-multiple-numbers'); if (bulkInp) bulkInp.value = waRecipients.map(r => r.phone).join('\n'); renderWaRecipientsList(); }); item.appendChild(infoWrapper); item.appendChild(delBtn); listContainer.appendChild(item); }); } // ============================================================================ // Broadcast message templates manager // ============================================================================ function renderWaTemplatesDropdown() { const select = document.getElementById('wa-template-select'); if (!select) return; select.innerHTML = ''; waTemplates.forEach(t => { const opt = document.createElement('option'); opt.value = t.id; opt.textContent = `📝 ${t.name}`; select.appendChild(opt); }); } async function saveWhatsAppTemplate() { const msgTextarea = document.getElementById('wa-message-text'); if (!msgTextarea) return; const text = msgTextarea.value.trim(); if (!text) { alert('Save aborted: Please write message copy before saving as template.'); return; } const name = prompt('Enter a descriptive name for your new broadcast template:'); if (!name) return; const sanitizedName = sanitizeDynamicString(name); waTemplates.push({ id: 'wat_' + Date.now(), name: sanitizedName, text: text }); await saveTemplatesSecurely(); renderWaTemplatesDropdown(); if (typeof window.addLog === 'function') { window.addLog(`Successfully saved new message template under descriptive name: "[${sanitizedName}]".`, 'success'); } } function loadWhatsAppTemplate(e) { const selectId = e.target.value; if (!selectId) return; const template = waTemplates.find(t => t.id === selectId); if (template) { const msgTextarea = document.getElementById('wa-message-text'); if (msgTextarea) { msgTextarea.value = template.text; } } } // ============================================================================ // Exports // ============================================================================ global.FritreeWhatsApp = { init: initWhatsAppModule, getRecipients: () => waRecipients, getMedia: () => Promise.resolve(null), clearRecipients: clearWaRecipientsList, updatePreview: () => {}, setRecipients: (list) => { if (Array.isArray(list)) { waRecipients = list.map(item => ({ phone: cleanPhoneNumber(item.phone || item), name: sanitizeDynamicString(item.name || 'Recipient') })); const bulkInp = document.getElementById('wa-multiple-numbers'); if (bulkInp) { bulkInp.value = waRecipients.map(r => r.phone).join('\n'); } renderWaRecipientsList(); } } }; if (document.readyState === 'complete' || document.readyState === 'interactive') { initWhatsAppModule(); } else { document.addEventListener('DOMContentLoaded', () => initWhatsAppModule()); } })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);