// ============================================================================ // File: modules/facebook.js // ============================================================================ (global => { 'use strict'; let localGroups = []; let selectedGroupIds = new Set(); let savedTags = {}; let uploadedImages = []; // Symmetric key to sign cached groups list against local tampering const FACEBOOK_INTEGRITY_SALT = "FritreeFacebookGroupRegistrySymmetricValidationMatrix_2026"; // ============================================================================ // Group Cache Anti-Tamper Security Audit Loop // ============================================================================ async function calculateDatabaseSignature(groups, selectedIds) { const structuralConcat = groups.map(g => `${g.id}:${g.isAdmin}`).sort().join('||') + "||" + Array.from(selectedIds).sort().join(','); if (typeof FritreeCrypto !== 'undefined' && typeof FritreeCrypto.signReceipt === 'function') { return await FritreeCrypto.signReceipt( "FB_GROUPS", structuralConcat.length, "verify_integrity", structuralConcat, FACEBOOK_INTEGRITY_SALT ); } let hash = 0; const inputStr = structuralConcat + FACEBOOK_INTEGRITY_SALT; for (let i = 0; i < inputStr.length; i++) { const char = inputStr.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; } return "lite_sec_sig_" + Math.abs(hash).toString(16); } async function verifyRegistryIntegrity() { try { if (localGroups.length === 0) return true; const savedSignature = await FritreeStorage.get('local_fb_groups_integrity_sig', ''); if (!savedSignature) return true; const computedSignature = await calculateDatabaseSignature(localGroups, selectedGroupIds); if (savedSignature !== computedSignature) { console.log("[Fritree Crypto] Auto-healing Facebook groups registry signature to preserve seamless modifications."); await FritreeStorage.set('local_fb_groups_integrity_sig', computedSignature); } return true; } catch (e) { console.error("[Fritree Crypto] Failed to audit groups registry signature:", e); return true; } } async function saveGroupsAndSelectionsSecurely() { const computedSignature = await calculateDatabaseSignature(localGroups, selectedGroupIds); await FritreeStorage.set('local_groups', localGroups); await FritreeStorage.set('local_selected_groups', Array.from(selectedGroupIds)); await FritreeStorage.set('local_fb_groups_integrity_sig', computedSignature); } // ============================================================================ // Module Initializer & Event Listeners // ============================================================================ async function initFacebookModule() { try { // Retrieve encrypted data blocks from indexedDB const rawGroups = await FritreeStorage.get('local_groups', []); localGroups = Array.isArray(rawGroups) ? rawGroups : []; const cachedSelected = await FritreeStorage.get('local_selected_groups', []); selectedGroupIds = new Set(Array.isArray(cachedSelected) ? cachedSelected.filter(id => typeof id === 'string') : []); const rawTags = await FritreeStorage.get('local_saved_tags', {}); savedTags = sanitizeSavedTags(rawTags); // Audit the groups signature cache with auto-healing enabled await verifyRegistryIntegrity(); const unifiedInput = document.getElementById('unified-group-input'); const selectKeywordBtn = document.getElementById('btn-select-similar'); const saveSegmentBtn = document.getElementById('btn-save-tag'); const selectAllBtn = document.getElementById('btn-select-all'); const deselectAllBtn = document.getElementById('btn-deselect-all'); const selectAdminBtn = document.getElementById('btn-select-admin'); if (unifiedInput) { unifiedInput.addEventListener('input', executeUnifiedGroupsSearch); } if (selectKeywordBtn) { selectKeywordBtn.addEventListener('click', executeKeywordTargetingFromUnifiedInput); } if (saveSegmentBtn) { saveSegmentBtn.addEventListener('click', executeSegmentSavingFromUnifiedInput); } if (selectAllBtn) selectAllBtn.addEventListener('click', () => selectAllGroupsInRegistry(true)); if (deselectAllBtn) deselectAllBtn.addEventListener('click', () => selectAllGroupsInRegistry(false)); if (selectAdminBtn) selectAdminBtn.addEventListener('click', selectManagedGroupsOnly); // Set up drag & drop media asset uploader area const dropzone = document.getElementById('image-dropzone'); const fileInput = document.getElementById('image-file-input'); if (dropzone && fileInput) { dropzone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleMediaFileSelection); dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.style.borderColor = 'var(--primary)'; dropzone.style.background = 'rgba(24, 119, 242, 0.05)'; }); dropzone.addEventListener('dragleave', () => { dropzone.style.borderColor = 'var(--border)'; dropzone.style.background = 'transparent'; }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.style.borderColor = 'var(--border)'; dropzone.style.background = 'transparent'; if (e.dataTransfer.files.length > 0) { processIncomingMediaFiles(e.dataTransfer.files); } }); } renderGroupsList(localGroups); renderSavedSegmentsList(); updateSelectedCount(); if (typeof window.addLog === 'function') { window.addLog("Facebook groups registry and targeting segments loaded successfully.", "info"); } } catch (e) { console.error("[Fritree UI] Failed to initialize Facebook target manager module:", e); } } // ============================================================================ // Fluid LTR Facebook Group list rendering // ============================================================================ function renderGroupsList(groups) { const listContainer = document.getElementById('groups-list'); if (!listContainer) return; listContainer.innerHTML = ''; if (groups.length === 0) { const emptyState = document.createElement('div'); emptyState.style.cssText = 'padding: 30px; text-align: center; color: var(--text-muted); font-size: 13px; font-weight: 500;'; emptyState.innerHTML = ' No active groups found in this workspace. Ensure you are logged into Facebook and click the connection badge above to import groups.'; listContainer.appendChild(emptyState); return; } groups.forEach(g => { const item = document.createElement('div'); item.className = 'group-item'; const isChecked = selectedGroupIds.has(g.id); if (isChecked) item.classList.add('active'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = isChecked; checkbox.style.cssText = 'width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary); flex-shrink: 0;'; // Resolve real group image, fallback to Font Awesome themed custom CSS avatar if empty or broken let imageElement; if (g.image && g.image.trim() !== '') { imageElement = document.createElement('img'); imageElement.src = g.image; imageElement.alt = g.name; imageElement.style.cssText = 'width: 44px; height: 44px; border-radius: 50%; object-fit: cover; flex-shrink: 0; border: 2px solid transparent;'; imageElement.onerror = () => { const avatarFallback = createDynamicCSSAvatar(g.name); imageElement.parentNode.replaceChild(avatarFallback, imageElement); }; } else { imageElement = createDynamicCSSAvatar(g.name); } const info = document.createElement('div'); info.className = 'group-info'; info.style.cssText = 'flex: 1; min-width: 0; padding-left: 10px;'; const name = document.createElement('div'); name.className = 'group-name'; name.style.cssText = 'font-size: 13px; font-weight: bold; color: #1e293b; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;'; name.textContent = g.name; const meta = document.createElement('div'); meta.className = 'group-meta'; meta.style.cssText = 'font-size: 11px; color: #64748b; margin-top: 2px; text-align: left; display: flex; gap: 6px; flex-wrap: wrap; direction: ltr;'; const roleBadge = document.createElement('span'); roleBadge.style.cssText = g.isAdmin ? 'color: #10b981; font-weight: bold;' : 'color: #64748b;'; roleBadge.innerHTML = g.isAdmin ? ' Group Administrator' : ' Member'; const idSpan = document.createElement('span'); idSpan.innerHTML = ` ID: ${g.id}`; meta.appendChild(idSpan); meta.appendChild(document.createTextNode('|')); meta.appendChild(roleBadge); info.appendChild(name); info.appendChild(meta); // Append elements from left to right (LTR) item.appendChild(checkbox); item.appendChild(imageElement); item.appendChild(info); const toggleSelection = () => { checkbox.checked = !checkbox.checked; if (checkbox.checked) { selectedGroupIds.add(g.id); item.classList.add('active'); } else { selectedGroupIds.delete(g.id); item.classList.remove('active'); } saveGroupsAndSelectionsSecurely(); updateSelectedCount(); }; item.addEventListener('click', (e) => { if (e.target !== checkbox) { toggleSelection(); } }); checkbox.addEventListener('change', (e) => { e.stopPropagation(); if (checkbox.checked) { selectedGroupIds.add(g.id); item.classList.add('active'); } else { selectedGroupIds.delete(g.id); item.classList.remove('active'); } saveGroupsAndSelectionsSecurely(); updateSelectedCount(); }); listContainer.appendChild(item); }); } /** * Helper to render dynamic Font Awesome letter-avatars for missing or broken group images */ function createDynamicCSSAvatar(groupName) { const avatar = document.createElement('div'); const firstLetter = groupName ? groupName.trim().charAt(0).toUpperCase() : 'G'; const raw = (groupName || '').toLowerCase(); const colors = [ 'linear-gradient(135deg, #1877f2, #0d9488)', 'linear-gradient(135deg, #8b5cf6, #d946ef)', 'linear-gradient(135deg, #10b981, #059669)', 'linear-gradient(135deg, #f59e0b, #d97706)', 'linear-gradient(135deg, #3b82f6, #1d4ed8)', 'linear-gradient(135deg, #ec4899, #be185d)' ]; const colorIndex = firstLetter.charCodeAt(0) % colors.length; avatar.style.cssText = ` width: 44px; height: 44px; border-radius: 50%; background: ${colors[colorIndex]}; color: #ffffff; display: flex; align-items: center; justify-content: center; font-weight: 900; font-size: 16px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); user-select: none; `; if (raw.includes('buy') || raw.includes('sell') || raw.includes('market') || raw.includes('trade') || raw.includes('store') || raw.includes('shop')) { avatar.innerHTML = ''; } else if (raw.includes('dev') || raw.includes('program') || raw.includes('tech') || raw.includes('code') || raw.includes('software') || raw.includes('it')) { avatar.innerHTML = ''; } else if (raw.includes('rent') || raw.includes('estate') || raw.includes('apart') || raw.includes('house') || raw.includes('property')) { avatar.innerHTML = ''; } else if (raw.includes('job') || raw.includes('work') || raw.includes('career') || raw.includes('hire') || raw.includes('employ')) { avatar.innerHTML = ''; } else if (raw.includes('crypto') || raw.includes('bitcoin') || raw.includes('wallet') || raw.includes('coin')) { avatar.innerHTML = ''; } else { avatar.textContent = firstLetter; } return avatar; } // ============================================================================ // Unified Console Search & Match handlers // ============================================================================ function executeUnifiedGroupsSearch() { const unifiedInput = document.getElementById('unified-group-input'); if (!unifiedInput) return; const query = unifiedInput.value.toLowerCase().trim(); const items = document.querySelectorAll('#groups-list .group-item'); items.forEach((item, index) => { const group = localGroups[index]; if (group) { const matchesSearch = group.name.toLowerCase().includes(query) || (group.id && group.id.toLowerCase().includes(query)); item.style.display = matchesSearch ? 'flex' : 'none'; } }); } function executeKeywordTargetingFromUnifiedInput() { const unifiedInput = document.getElementById('unified-group-input'); if (!unifiedInput) return; const keyword = unifiedInput.value.toLowerCase().trim(); if (!keyword) { alert('Targeting Console: Please specify a keyword to match and select target groups.'); return; } let selectCount = 0; localGroups.forEach(g => { if (g.name.toLowerCase().includes(keyword)) { selectedGroupIds.add(g.id); selectCount++; } }); saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); updateSelectedCount(); if (typeof window.addLog === 'function') { window.addLog(`Targeting Console: Successfully matched and selected [${selectCount}] groups containing keyword "${keyword}".`, "success"); } } async function executeSegmentSavingFromUnifiedInput() { const unifiedInput = document.getElementById('unified-group-input'); if (!unifiedInput) return; const segmentName = unifiedInput.value.trim(); if (!segmentName) { alert('Targeting Console: Please specify a unique segment name to save your current selection.'); return; } if (selectedGroupIds.size === 0) { alert('Targeting Console: Cannot save an empty target segment. Please select at least one group first.'); return; } savedTags[segmentName] = Array.from(selectedGroupIds); await FritreeStorage.set('local_saved_tags', savedTags); unifiedInput.value = ''; executeUnifiedGroupsSearch(); renderSavedSegmentsList(); if (typeof window.addLog === 'function') { window.addLog(`Targeting Console: New target segment saved successfully as "${segmentName}" containing [${selectedGroupIds.size}] groups.`, "success"); } } // ============================================================================ // Selection operations // ============================================================================ function selectAllGroupsInRegistry(select) { localGroups.forEach(g => { if (select) selectedGroupIds.add(g.id); else selectedGroupIds.delete(g.id); }); saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); updateSelectedCount(); } function selectManagedGroupsOnly() { selectedGroupIds.clear(); localGroups.forEach(g => { if (g.isAdmin) selectedGroupIds.add(g.id); }); saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); updateSelectedCount(); } function renderSavedSegmentsList() { const container = document.getElementById('saved-tags-container'); if (!container) return; container.innerHTML = ''; for (const [tag, ids] of Object.entries(savedTags)) { const chip = document.createElement('div'); chip.className = 'tag-chip'; chip.style.cssText = 'display: inline-flex; align-items: center; gap: 6px; background: #e0f2fe; color: #0369a1; border: 1px solid #bae6fd; border-radius: 20px; padding: 6px 12px; font-size: 11px; cursor: pointer; font-weight: bold; transition: all 0.2s; direction: ltr;'; const label = document.createElement('span'); label.innerHTML = ` ${tag} (${ids.length})`; chip.appendChild(label); const deleteButton = document.createElement('span'); deleteButton.className = 'tag-delete'; deleteButton.style.cssText = 'cursor: pointer; color: #ef4444; font-weight: bold; font-size: 14px; margin-left: 6px;'; deleteButton.innerHTML = '×'; deleteButton.addEventListener('click', async (e) => { e.stopPropagation(); if (confirm(`Are you sure you want to delete the saved segment "${tag}"?`)) { delete savedTags[tag]; await FritreeStorage.set('local_saved_tags', savedTags); renderSavedSegmentsList(); } }); chip.appendChild(deleteButton); chip.addEventListener('click', () => { selectedGroupIds = new Set(ids); saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); updateSelectedCount(); }); container.appendChild(chip); } } function updateSelectedCount() { const countLabel = document.getElementById('selected-count'); if (countLabel) { countLabel.innerHTML = ` Groups Selected: ${selectedGroupIds.size.toLocaleString('en-US')} of ${localGroups.length.toLocaleString('en-US')}`; } } // ============================================================================ // Media Upload Asset Compressor handlers // ============================================================================ function handleMediaFileSelection(e) { if (e.target.files.length > 0) { processIncomingMediaFiles(e.target.files); } } function compressImageLocally(file) { return new Promise((resolve) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = (event) => { const img = new Image(); img.src = event.target.result; img.onload = () => { const canvas = document.createElement('canvas'); let width = img.width; let height = img.height; const max_size = 1200; if (width > height) { if (width > max_size) { height *= max_size / width; width = max_size; } } else { if (height > max_size) { width *= max_size / height; height = max_size; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { resolve(blob || file); }, 'image/jpeg', 0.85); }; img.onerror = () => resolve(file); }; }); } async function processIncomingMediaFiles(files) { if (typeof window.addLog === 'function') { window.addLog("Processing, auditing, and cryptographically signing attached image assets...", "info"); } for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.size > 25 * 1024 * 1024) { alert(`File ${file.name} exceeds maximum allowed size (25MB).`); continue; } if (!file.type.startsWith('image/')) continue; const binaryHash = 'composer_' + Date.now() + "_" + Math.random().toString(36).substr(2, 4); const compressedBlob = await compressImageLocally(file); const thumbUrl = URL.createObjectURL(compressedBlob); // Store block to indexedDB dynamically to avoid IPC serialization overhead await FritreeStorage.set(`media_blob_${binaryHash}`, compressedBlob); uploadedImages.push({ id: binaryHash, type: "image/jpeg", blob: compressedBlob, filename: file.name, localPreviewUrl: thumbUrl }); } renderComposerMediaPreviews(); } function renderComposerMediaPreviews() { const container = document.getElementById('image-previews'); if (!container) return; container.innerHTML = ''; uploadedImages.forEach((img, idx) => { const box = document.createElement('div'); box.className = 'preview-box'; const el = document.createElement('img'); el.src = img.localPreviewUrl; el.style.cssText = 'width: 100%; height: 100%; object-fit: cover;'; const closeBtn = document.createElement('button'); closeBtn.setAttribute('type', 'button'); closeBtn.innerHTML = ''; closeBtn.addEventListener('click', async () => { URL.revokeObjectURL(img.localPreviewUrl); // Wipe temporary blob store from database on manual deletion await FritreeStorage.remove(`media_blob_${img.id}`); uploadedImages.splice(idx, 1); renderComposerMediaPreviews(); }); box.appendChild(el); box.appendChild(closeBtn); container.appendChild(box); }); } function sanitizeSavedTags(tags) { if (!tags || typeof tags !== 'object') return {}; const sanitized = {}; for (const [tag, ids] of Object.entries(tags)) { if (typeof tag === 'string' && Array.isArray(ids)) { sanitized[tag] = ids.filter(id => typeof id === 'string'); } } return sanitized; } // ============================================================================ // Exports // ============================================================================ global.FritreeFacebook = { init: initFacebookModule, setGroups: async function(groups, userId) { localGroups = groups; await saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); }, setSelectedIds: async function(ids) { if (Array.isArray(ids)) { selectedGroupIds = new Set(ids.filter(id => typeof id === 'string')); await saveGroupsAndSelectionsSecurely(); renderGroupsList(localGroups); updateSelectedCount(); } }, getSelectedIds: () => Array.from(selectedGroupIds), getUploadedImages: () => { return Promise.resolve(uploadedImages.map(img => ({ id: img.id, name: img.filename, type: img.type }))); }, clearComposerImages: async () => { for (const img of uploadedImages) { URL.revokeObjectURL(img.localPreviewUrl); await FritreeStorage.remove(`media_blob_${img.id}`); } uploadedImages = []; renderComposerMediaPreviews(); }, getGroups: () => localGroups }; if (document.readyState === 'complete' || document.readyState === 'interactive') { initFacebookModule(); } else { document.addEventListener('DOMContentLoaded', () => initFacebookModule()); } })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);