|
|
|
|
|
|
|
|
| (global => {
|
| 'use strict';
|
|
|
| let localGroups = [];
|
| let selectedGroupIds = new Set();
|
| let savedTags = {};
|
| let uploadedImages = [];
|
|
|
|
|
| const FACEBOOK_INTEGRITY_SALT = "FritreeFacebookGroupRegistrySymmetricValidationMatrix_2026";
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
| async function initFacebookModule() {
|
| try {
|
|
|
| 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);
|
|
|
|
|
| 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);
|
|
|
|
|
| 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);
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| 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 = '<i class="fa-solid fa-circle-exclamation"></i> 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;';
|
|
|
|
|
| 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 ? '<i class="fa-solid fa-user-gear"></i> Group Administrator' : '<i class="fa-solid fa-users"></i> Member';
|
|
|
| const idSpan = document.createElement('span');
|
| idSpan.innerHTML = `<i class="fa-solid fa-hashtag"></i> ID: ${g.id}`;
|
|
|
| meta.appendChild(idSpan);
|
| meta.appendChild(document.createTextNode('|'));
|
| meta.appendChild(roleBadge);
|
|
|
| info.appendChild(name);
|
| info.appendChild(meta);
|
|
|
|
|
| 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);
|
| });
|
| }
|
|
|
| |
| |
|
|
| 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 = '<i class="fa-solid fa-cart-shopping" style="margin:0;"></i>';
|
| } else if (raw.includes('dev') || raw.includes('program') || raw.includes('tech') || raw.includes('code') || raw.includes('software') || raw.includes('it')) {
|
| avatar.innerHTML = '<i class="fa-solid fa-laptop-code" style="margin:0;"></i>';
|
| } else if (raw.includes('rent') || raw.includes('estate') || raw.includes('apart') || raw.includes('house') || raw.includes('property')) {
|
| avatar.innerHTML = '<i class="fa-solid fa-house-chimney" style="margin:0;"></i>';
|
| } else if (raw.includes('job') || raw.includes('work') || raw.includes('career') || raw.includes('hire') || raw.includes('employ')) {
|
| avatar.innerHTML = '<i class="fa-solid fa-briefcase" style="margin:0;"></i>';
|
| } else if (raw.includes('crypto') || raw.includes('bitcoin') || raw.includes('wallet') || raw.includes('coin')) {
|
| avatar.innerHTML = '<i class="fa-solid fa-wallet" style="margin:0;"></i>';
|
| } else {
|
| avatar.textContent = firstLetter;
|
| }
|
| return avatar;
|
| }
|
|
|
|
|
|
|
|
|
| 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");
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| 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 = `<i class="fa-solid fa-hashtag"></i> ${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 = `<i class="fa-solid fa-square-check"></i> Groups Selected: ${selectedGroupIds.size.toLocaleString('en-US')} of ${localGroups.length.toLocaleString('en-US')}`;
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| 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);
|
|
|
|
|
| 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 = '<i class="fa-solid fa-trash-can"></i>';
|
| closeBtn.addEventListener('click', async () => {
|
| URL.revokeObjectURL(img.localPreviewUrl);
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
| 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); |