// ============================================================================
// 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);