I decided I wanted to make some changes to my Facebook profile. Specifically, I wanted to remove all of the old posts, especially those which I may not stand by still today. I was a kid once and kids say silly, embarrassing things, including announcing their "rediscovered love for Oasis" (why was this a post).
Naturally, I started by investigating Facebook's own tools (lack lustre and full of manual labour). Then I checked out some extensions which offered the precise functionality I was looking for but... Required pricey subscriptions to delete more than so many posts.
So, I gave up and opened up Claude. Then I low-key vibe-coded for the next hour 😂
Here's my very polished Chrome extension...
Screenshot extension at work.
Chrome extensions are made up of a handful of requisite files...
{ "manifest_version": 3, "name": "Facebook Activity Log Trasher", "version": "1.0", "description": "Manage Facebook activity history via GraphQL", "permissions": [ "cookies", "storage", "scripting" ], "host_permissions": [ "*://*.facebook.com/*" ], "action": { "default_popup": "popup.html" }, "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["*://*.facebook.com/*"], "js": ["content.js"], "run_at": "document_start" } ] }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> body { width: 400px; padding: 15px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } h2 { margin-top: 0; font-size: 16px; } .section { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #ddd; } input, select { width: 100%; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } button { width: 100%; padding: 10px; margin: 5px 0; background: #1877f2; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; } button:hover { background: #166fe5; } button:disabled { background: #ccc; cursor: not-allowed; } .danger { background: #dc3545; } .danger:hover { background: #c82333; } #status { margin-top: 10px; padding: 10px; border-radius: 4px; font-size: 13px; max-height: 200px; overflow-y: auto; } .success { background: #d4edda; color: #155724; } .error { background: #f8d7da; color: #721c24; } .info { background: #d1ecf1; color: #0c5460; } #postsList { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 10px; margin: 10px 0; } .post-item { padding: 8px; margin: 5px 0; background: #f8f9fa; border-radius: 4px; font-size: 12px; } .post-item input[type="checkbox"] { width: auto; margin-right: 8px; } label { font-size: 13px; font-weight: 500; display: block; margin-top: 8px; } </style> </head> <body> <h2>Facebook Activity Manager</h2> <div class="section"> <button id="testConnection">Test Connection</button> </div> <div class="section"> <label>Year (optional):</label> <input type="number" id="year" placeholder="e.g., 2009" min="2004" max="2025"> <label>Posts per page:</label> <select id="count"> <option value="25">25</option> <option value="50">50</option> <option value="100">100</option> </select> <button id="loadPosts">Load Posts</button> <button id="loadMore" disabled>Load More</button> </div> <div id="postsList" style="display: none;"></div> <div class="section"> <button id="selectAll" disabled>Select All</button> <button id="deselectAll" disabled>Deselect All</button> <button id="deleteSelected" class="danger" disabled>Delete Selected Posts</button> </div> <div class="section"> <button id="loadAllPosts" disabled>Load All Posts (Auto-load until done)</button> </div> <div id="status"></div> <script src="popup.js"></script> </body> </html>
// popup.js - Handles the extension popup UI interactions const statusDiv = document.getElementById('status'); const postsListDiv = document.getElementById('postsList'); let loadedPosts = []; let currentCursor = null; let isAutoLoading = false; function showStatus(message, type = 'info') { statusDiv.textContent = message; statusDiv.className = type; } // Helper function to send messages with error handling async function sendMessageToTab(tabId, message) { try { // Try sending the message directly first const response = await chrome.tabs.sendMessage(tabId, message); return response; } catch (error) { // If that fails, try injecting the content script try { await chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] }); // Wait a bit for the script to initialize await new Promise(resolve => setTimeout(resolve, 200)); // Try sending the message again const response = await chrome.tabs.sendMessage(tabId, message); return response; } catch (retryError) { throw new Error(`Failed to communicate with page. Try refreshing the Facebook tab.`); } } } // Extract story IDs from the GraphQL response function extractPosts(response) { try { console.log('Extracting posts from response:', response); // Navigate the response structure - posts are at data.node.activity_log_stories.edges const edges = response?.data?.node?.activity_log_stories?.edges || []; console.log('Found edges:', edges.length); const posts = edges.map(edge => ({ storyId: edge.node?.id, text: edge.node?.message?.text || edge.node?.title?.text || 'No text', timestamp: edge.node?.creation_time })); console.log('Extracted posts:', posts); return posts; } catch (error) { console.error('Error extracting posts:', error); return []; } } // Extract pagination cursor from response function extractCursor(response) { try { return response?.data?.node?.activity_log_stories?.page_info?.end_cursor || null; } catch (error) { return null; } } // Display posts in the UI function displayPosts(posts) { if (posts.length === 0) { postsListDiv.innerHTML = '<p>No posts found</p>'; postsListDiv.style.display = 'block'; return; } postsListDiv.innerHTML = posts.map((post, index) => ` <div class="post-item"> <input type="checkbox" id="post-${index}" data-story-id="${post.storyId}"> <label for="post-${index}" style="display: inline;"> ${post.text.substring(0, 100)}${post.text.length > 100 ? '...' : ''} ${post.timestamp ? `(${new Date(post.timestamp * 1000).toLocaleDateString()})` : ''} </label> </div> `).join(''); postsListDiv.style.display = 'block'; document.getElementById('deleteSelected').disabled = false; document.getElementById('selectAll').disabled = false; document.getElementById('deselectAll').disabled = false; } // Test if we can get the required tokens from Facebook document.getElementById('testConnection').addEventListener('click', async () => { showStatus('Testing connection...', 'info'); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab.url.includes('facebook.com')) { showStatus('Please navigate to Facebook first', 'error'); return; } const response = await sendMessageToTab(tab.id, { action: 'getTokens' }); if (response.success) { showStatus(`✓ Connected! User ID: ${response.tokens.userId}`, 'success'); } else { showStatus('✗ Could not find auth tokens. Make sure you\'re logged in.', 'error'); } } catch (error) { showStatus(`Error: ${error.message}`, 'error'); } }); // Load posts document.getElementById('loadPosts').addEventListener('click', async () => { showStatus('Loading posts...', 'info'); currentCursor = null; try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab.url.includes('facebook.com')) { showStatus('Please navigate to Facebook first', 'error'); return; } const year = document.getElementById('year').value || null; const count = parseInt(document.getElementById('count').value); const response = await sendMessageToTab(tab.id, { action: 'loadPosts', year: year ? parseInt(year) : null, count: count, cursor: null }); if (response.success) { loadedPosts = extractPosts(response.result); currentCursor = extractCursor(response.result); displayPosts(loadedPosts); showStatus(`✓ Loaded ${loadedPosts.length} posts`, 'success'); document.getElementById('loadMore').disabled = !currentCursor; document.getElementById('loadAllPosts').disabled = !currentCursor; } else { showStatus(`✗ Failed: ${response.error}`, 'error'); } } catch (error) { showStatus(`Error: ${error.message}`, 'error'); } }); // Load more posts (pagination) document.getElementById('loadMore').addEventListener('click', async () => { if (!currentCursor) return; showStatus('Loading more posts...', 'info'); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const year = document.getElementById('year').value || null; const count = parseInt(document.getElementById('count').value); const response = await sendMessageToTab(tab.id, { action: 'loadPosts', year: year ? parseInt(year) : null, count: count, cursor: currentCursor }); if (response.success) { const newPosts = extractPosts(response.result); loadedPosts = [...loadedPosts, ...newPosts]; currentCursor = extractCursor(response.result); displayPosts(loadedPosts); showStatus(`✓ Loaded ${loadedPosts.length} total posts`, 'success'); document.getElementById('loadMore').disabled = !currentCursor; document.getElementById('loadAllPosts').disabled = !currentCursor; } else { showStatus(`✗ Failed: ${response.error}`, 'error'); } } catch (error) { showStatus(`Error: ${error.message}`, 'error'); } }); // Delete selected posts document.getElementById('deleteSelected').addEventListener('click', async () => { const checkboxes = postsListDiv.querySelectorAll('input[type="checkbox"]:checked'); const storyIds = Array.from(checkboxes).map(cb => cb.dataset.storyId); if (storyIds.length === 0) { showStatus('No posts selected', 'error'); return; } if (!confirm(`Are you sure you want to delete ${storyIds.length} post(s)?`)) { return; } try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); // Facebook allows max 250 posts per delete request, so batch them const BATCH_SIZE = 250; const batches = []; for (let i = 0; i < storyIds.length; i += BATCH_SIZE) { batches.push(storyIds.slice(i, i + BATCH_SIZE)); } let totalDeleted = 0; for (let i = 0; i < batches.length; i++) { const batch = batches[i]; showStatus(`Deleting batch ${i + 1}/${batches.length} (${batch.length} posts)...`, 'info'); const response = await sendMessageToTab(tab.id, { action: 'deleteActivity', storyIds: batch, categoryKey: 'MANAGEPOSTSPHOTOSANDVIDEOS' }); if (response.success) { totalDeleted += batch.length; // Small delay between batches to avoid rate limiting if (i < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)); } } else { showStatus(`✗ Failed at batch ${i + 1}: ${response.error}`, 'error'); break; } } if (totalDeleted > 0) { showStatus(`✓ Successfully deleted ${totalDeleted} post(s)`, 'success'); // Remove deleted posts from the display loadedPosts = loadedPosts.filter(post => !storyIds.includes(post.storyId)); displayPosts(loadedPosts); } } catch (error) { showStatus(`Error: ${error.message}`, 'error'); } }); // Select all posts document.getElementById('selectAll').addEventListener('click', () => { const checkboxes = postsListDiv.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(cb => cb.checked = true); showStatus(`Selected ${checkboxes.length} posts`, 'info'); }); // Deselect all posts document.getElementById('deselectAll').addEventListener('click', () => { const checkboxes = postsListDiv.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(cb => cb.checked = false); showStatus('Deselected all posts', 'info'); }); // Load all posts automatically document.getElementById('loadAllPosts').addEventListener('click', async () => { if (!currentCursor || isAutoLoading) return; isAutoLoading = true; document.getElementById('loadAllPosts').disabled = true; document.getElementById('loadPosts').disabled = true; document.getElementById('loadMore').disabled = true; const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const year = document.getElementById('year').value || null; const count = parseInt(document.getElementById('count').value); let batchCount = 0; let errorCount = 0; const MAX_ERRORS = 3; // Stop after 3 consecutive errors try { while (currentCursor && isAutoLoading && errorCount < MAX_ERRORS) { batchCount++; showStatus(`Auto-loading batch ${batchCount}... (${loadedPosts.length} posts so far)`, 'info'); try { const response = await sendMessageToTab(tab.id, { action: 'loadPosts', year: year ? parseInt(year) : null, count: count, cursor: currentCursor }); if (response.success) { const newPosts = extractPosts(response.result); // Reset error count on success errorCount = 0; // Stop if we got fewer posts than requested (reached the end) if (newPosts.length < count) { loadedPosts = [...loadedPosts, ...newPosts]; currentCursor = null; displayPosts(loadedPosts); showStatus(`✓ Finished! Loaded all ${loadedPosts.length} posts`, 'success'); break; } loadedPosts = [...loadedPosts, ...newPosts]; currentCursor = extractCursor(response.result); displayPosts(loadedPosts); // Small delay to avoid rate limiting await new Promise(resolve => setTimeout(resolve, 500)); } else { errorCount++; console.error(`Batch ${batchCount} failed:`, response.error); if (errorCount >= MAX_ERRORS) { showStatus(`⚠ Stopped after ${MAX_ERRORS} errors. Loaded ${loadedPosts.length} posts. Error: ${response.error}`, 'error'); } else { showStatus(`⚠ Error in batch ${batchCount}, retrying... (${errorCount}/${MAX_ERRORS})`, 'info'); await new Promise(resolve => setTimeout(resolve, 1000)); } } } catch (error) { errorCount++; console.error(`Batch ${batchCount} exception:`, error); if (errorCount >= MAX_ERRORS) { showStatus(`⚠ Stopped after ${MAX_ERRORS} errors. Loaded ${loadedPosts.length} posts. You can still use these posts.`, 'error'); } else { showStatus(`⚠ Error in batch ${batchCount}, retrying... (${errorCount}/${MAX_ERRORS})`, 'info'); await new Promise(resolve => setTimeout(resolve, 1000)); } } } } finally { isAutoLoading = false; document.getElementById('loadPosts').disabled = false; document.getElementById('loadMore').disabled = !currentCursor; document.getElementById('loadAllPosts').disabled = !currentCursor; // Make sure UI is updated with whatever we got if (loadedPosts.length > 0) { displayPosts(loadedPosts); } } });
// content.js - Runs in the context of Facebook pages // Wait for page to be ready function waitForPageReady() { return new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); } // Extract authentication tokens from the page function getAuthTokens() { try { // Method 1: Try to get DTSG from form input let dtsgToken = document.querySelector('[name="fb_dtsg"]')?.value; // Method 2: Try to get from JavaScript global variables if (!dtsgToken && typeof window !== 'undefined') { // Facebook often stores tokens in require/define modules const scripts = document.querySelectorAll('script'); for (const script of scripts) { const text = script.textContent || ''; // Look for DTSGInitialData const dtsgMatch = text.match(/"DTSGInitialData"[^}]*"token":"([^"]+)"/); if (dtsgMatch) { dtsgToken = dtsgMatch[1]; break; } // Alternative pattern const dtsgMatch2 = text.match(/\["DTSGInitialData"[^\]]*\]\s*,\s*\[.*?"token":"([^"]+)"/); if (dtsgMatch2) { dtsgToken = dtsgMatch2[1]; break; } } } // Method 3: Try document.cookie for DTSG if (!dtsgToken) { const cookieMatch = document.cookie.match(/fb_dtsg=([^;]+)/); if (cookieMatch) { dtsgToken = decodeURIComponent(cookieMatch[1]); } } // Get LSD token let lsdToken = document.querySelector('[name="lsd"]')?.value; if (!lsdToken) { const scripts = document.querySelectorAll('script'); for (const script of scripts) { const text = script.textContent || ''; const lsdMatch = text.match(/"LSD"[^}]*"token":"([^"]+)"/); if (lsdMatch) { lsdToken = lsdMatch[1]; break; } } } // Get User ID from cookie const userId = document.cookie.match(/c_user=(\d+)/)?.[1]; // Get jazoest token let jazoest = document.querySelector('[name="jazoest"]')?.value; if (!jazoest) { const scripts = document.querySelectorAll('script'); for (const script of scripts) { const text = script.textContent || ''; const jazoestMatch = text.match(/jazoest["\s]*[:=]["\s]*(\d+)/); if (jazoestMatch) { jazoest = jazoestMatch[1]; break; } } } // Get __rev, __spin_r, __spin_b, __spin_t from page HTML let rev, spinR, spinB, spinT, hsi, dyn, csr; const pageHtml = document.documentElement.innerHTML; const revMatch = pageHtml.match(/"__rev":(\d+)/); if (revMatch) rev = revMatch[1]; const spinRMatch = pageHtml.match(/"__spin_r":(\d+)/); if (spinRMatch) spinR = spinRMatch[1]; const spinBMatch = pageHtml.match(/"__spin_b":"([^"]+)"/); if (spinBMatch) spinB = spinBMatch[1]; const spinTMatch = pageHtml.match(/"__spin_t":(\d+)/); if (spinTMatch) spinT = spinTMatch[1]; const hsiMatch = pageHtml.match(/"hsi":"(\d+)"/); if (hsiMatch) hsi = hsiMatch[1]; const dynMatch = pageHtml.match(/"__dyn":"([^"]+)"/); if (dynMatch) dyn = dynMatch[1]; const csrMatch = pageHtml.match(/"__csr":"([^"]+)"/); if (csrMatch) csr = csrMatch[1]; console.log('Token extraction:', { hasDtsg: !!dtsgToken, hasLsd: !!lsdToken, hasUserId: !!userId, hasJazoest: !!jazoest, hasRev: !!rev }); return { dtsg: dtsgToken, lsd: lsdToken, userId: userId, jazoest: jazoest, rev: rev, spinR: spinR, spinB: spinB, spinT: spinT, hsi: hsi, dyn: dyn, csr: csr }; } catch (error) { console.error('Error extracting tokens:', error); return null; } } // Make a GraphQL request to Facebook async function makeGraphQLRequest(docId, variables, tokens, friendlyName) { try { console.log('makeGraphQLRequest called with:', { docId, friendlyName, variables }); // Start with your working request payload as a template const basePayload = 'BASEPAYLOAD'; // sanitised - copied from a working request performed by Facebook proper // Parse the base payload const formData = new URLSearchParams(basePayload); // Override with actual user-specific values formData.set('fb_dtsg', tokens.dtsg); formData.set('lsd', tokens.lsd); formData.set('av', tokens.userId); formData.set('__user', tokens.userId); // Set the GraphQL-specific parameters formData.set('fb_api_req_friendly_name', friendlyName); formData.set('variables', JSON.stringify(variables)); formData.set('doc_id', docId); console.log('Sending fetch request...'); const response = await fetch('https://www.facebook.com/api/graphql/', { method: 'POST', body: formData, credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', } }); console.log('Response status:', response.status); const text = await response.text(); console.log('Response text (first 500 chars):', text.substring(0, 500)); // Facebook sometimes returns text with for(;;); prefix const cleanText = text.split(/\n/)[0].replace(/^for\s*\(\s*;\s*;\s*\)\s*;\s*/, ''); const result = JSON.parse(cleanText); return result; } catch (error) { console.error('GraphQL request failed:', error); throw error; } } // Listen for messages from the popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'getTokens') { (async () => { await waitForPageReady(); // Give it a bit more time for React to inject tokens await new Promise(resolve => setTimeout(resolve, 500)); const tokens = getAuthTokens(); sendResponse({ success: tokens && tokens.dtsg !== undefined, tokens: tokens }); })(); return true; } if (request.action === 'deleteActivity') { (async () => { try { const tokens = getAuthTokens(); if (!tokens || !tokens.dtsg) { sendResponse({ success: false, error: 'No auth tokens found' }); return; } console.log('Deleting posts with tokens:', { hasDtsg: !!tokens.dtsg, hasLsd: !!tokens.lsd, userId: tokens.userId, storyCount: request.storyIds?.length }); // Delete activity mutation - different doc_id than loading posts const docId = 'DOCIDHERE'; // sanitised const friendlyName = 'CometActivityLogBulkActionMutation'; const variables = { input: { action: 'MOVE_TO_TRASH', category_key: request.categoryKey || 'MANAGEPOSTSPHOTOSANDVIDEOS', render_location: 'ACTIVITY_LOG', story_ids: request.storyIds || [], transaction_id: `${Date.now()}_batch_0`, actor_id: tokens.userId, client_mutation_id: '1' } }; console.log('Making delete GraphQL request with variables:', variables); const result = await makeGraphQLRequest(docId, variables, tokens, friendlyName); console.log('Delete GraphQL response:', result); sendResponse({ success: true, result: result }); } catch (error) { console.error('Error in deleteActivity:', error); sendResponse({ success: false, error: error.message }); } })(); return true; } if (request.action === 'loadPosts') { (async () => { try { const tokens = getAuthTokens(); if (!tokens || !tokens.dtsg) { sendResponse({ success: false, error: 'No auth tokens found' }); return; } console.log('Loading posts with tokens:', { hasDtsg: !!tokens.dtsg, hasLsd: !!tokens.lsd, userId: tokens.userId }); // Use the pagination doc_id and friendly name from your example const docId = 'DOCIDHERE'; // sanitised const friendlyName = 'CometActivityLogStoriesListPaginationQuery'; // Match Facebook's exact variable structure const variables = { audience: null, category: request.category || 'MANAGEPOSTSPHOTOSANDVIDEOS', category_key: request.categoryKey || 'MANAGEPOSTSPHOTOSANDVIDEOS', count: request.count || 25, cursor: request.cursor || null, feedLocation: null, media_content_filters: [], month: request.month || null, person_id: null, privacy: 'NONE', scale: 1, timeline_visibility: 'ALL', year: request.year || null, id: tokens.userId }; console.log('Making GraphQL request with variables:', variables); const result = await makeGraphQLRequest(docId, variables, tokens, friendlyName); console.log('GraphQL response:', result); sendResponse({ success: true, result: result }); } catch (error) { console.error('Error in loadPosts:', error); sendResponse({ success: false, error: error.message }); } })(); return true; } });
// background.js - Service worker for background tasks // Listen for installation chrome.runtime.onInstalled.addListener(() => { console.log('Facebook Activity Manager installed'); }); // You can add additional background functionality here // For example, scheduling tasks, managing storage, etc. // Example: Listen for messages from content script or popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Handle background tasks here if needed return true; });