From b75f2cc461a0475de68eac296b16eb9aee5d6419 Mon Sep 17 00:00:00 2001 From: Saptak S Date: Wed, 17 Jun 2026 18:20:27 +0530 Subject: [PATCH 01/22] Adds activity log URI endpoints and action selector scripts --- .../FacebookViewModel/jobs_delete.ts | 615 +++--------------- 1 file changed, 80 insertions(+), 535 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index 7c4b577c..e5ee19fb 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -5,6 +5,17 @@ import { checkRateLimit } from "./rate_limit"; import { AutomationErrorType } from "../../automation_errors"; const FACEBOOK_PROFILE_URL = "https://www.facebook.com/me/"; +const FACEBOOK_CATEGORY_KEYS = { + "comments": "COMMENTSCLUSTER", + "reactions": "LIKEDPOSTS", + "user_posts": "MANAGEPOSTSPHOTOSANDVIDEOS", + "posts_on_others": "POSTSONOTHERSTIMELINES", + "others_posts": "WALLCLUSTER", + "checkins": "CHECKINS", + "tagged_posts": "MANAGETAGSBYOTHERSCLUSTER", + "tagged_media": "TAGGEDPHOTOS", +} +const ACTIVITY_LOG_CHECKBOX_NAME = "comet_activity_log_select_all_checkbox" async function reportDeleteWallPostsError( vm: FacebookViewModel, @@ -301,58 +312,26 @@ export function getHighestPriority(actions: PostAction[]): PostAction | null { } /** - * Toggle a checkbox by index and return success + * Toggle a checkbox by name and return success */ -async function toggleCheckbox( - vm: FacebookViewModel, - listIndex: number, - itemIndex: number, - shouldCheck: boolean, -): Promise { +async function toggleSelectAllCheckbox(vm: FacebookViewModel, shouldCheck: boolean): Promise { const result = await vm.safeExecuteJavaScript( `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; + const checkbox = document.querySelector('input[name="${ACTIVITY_LOG_CHECKBOX_NAME}"]'); + if (!checkbox) return false; - const lists = dialog.querySelectorAll('div[role="list"]'); - if (${listIndex} >= lists.length) return false; - - const list = lists[${listIndex}]; - const items = list.querySelectorAll('div[role="listitem"]'); - if (${itemIndex} >= items.length) return false; - - const item = items[${itemIndex}]; - const checkbox = item.querySelector('input[type="checkbox"]'); - const checkboxControl = item.querySelector('[role="checkbox"]'); - if (!checkbox && !checkboxControl) return false; - - const ariaChecked = - checkboxControl?.getAttribute('aria-checked') ?? - checkbox?.getAttribute('aria-checked'); - let isChecked; - - if (ariaChecked === 'true') { - isChecked = true; - } else if (ariaChecked === 'false') { - isChecked = false; - } else if (checkbox instanceof HTMLInputElement) { - isChecked = checkbox.checked; - } else { - return false; - } + const isChecked = checkbox?.getAttribute('aria-checked') || checkbox.checked; const shouldCheck = ${shouldCheck}; - const clickTarget = checkboxControl ?? checkbox; - if (!clickTarget) return false; // Only click if we need to change the state if (isChecked !== shouldCheck) { - clickTarget.click(); + checkbox.click(); return true; } return true; })()`, - "toggleCheckbox", + "toggleSelectAllCheckbox", ); return result.success && result.value; } @@ -416,114 +395,30 @@ async function clickNextButton(vm: FacebookViewModel): Promise { } /** - * Select the "Delete posts" radio button in the action selection dialog - * Looks for a div with text "delete posts" (case insensitive), checks it's not disabled, - * and clicks the radio button (i tag) inside it + * Click the "Trash" button in the header section + * Looks for a div with role "button" and aria-label "trash" (case insensitive), checks it's not disabled, + * and clicks the button */ -async function selectDeletePostsOption( +async function clickDeletePostsOption( vm: FacebookViewModel, ): Promise { const result = await vm.safeExecuteJavaScript( `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; + const deleteButton = document.querySelector('div[aria-label="Trash"][role="button"]'); + if (!deleteButton) return false; - // Find all divs that might contain the delete posts option - const divs = dialog.querySelectorAll('div[aria-disabled]'); - - for (const div of divs) { - // Check if this div or its children contain text about deleting posts - const text = div.textContent?.toLowerCase() || ''; - if (text.includes('delete posts')) { - // Check that it's not disabled - if (div.getAttribute('aria-disabled') === 'false') { - // Find the radio button (i tag) inside this div - const radioButton = div.querySelector('i'); - if (radioButton) { - radioButton.click(); - return true; - } - } else { - console.log('Delete posts option is disabled'); - return false; - } - } + if (deleteButton.getAttribute('aria-disabled') === 'false') { + deleteButton.click(); + return true; + } else { + console.log('Delete posts option is disabled'); + return false; } console.log('Could not find delete posts option'); return false; })()`, - "selectDeletePostsOption", - ); - return result.success && result.value; -} - -/** - * Select the "Untag yourself" radio button in the action selection dialog - */ -async function selectUntagPostsOption(vm: FacebookViewModel): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; - - const divs = dialog.querySelectorAll('div[aria-disabled]'); - - for (const div of divs) { - const text = div.textContent?.toLowerCase() || ''; - if (text.includes('untag') || text.includes('remove tags')) { - if (div.getAttribute('aria-disabled') === 'false') { - const radioButton = div.querySelector('i'); - if (radioButton) { - radioButton.click(); - return true; - } - } else { - console.log('Untag option is disabled'); - return false; - } - } - } - - console.log('Could not find untag option'); - return false; - })()`, - "selectUntagPostsOption", - ); - return result.success && result.value; -} - -/** - * Select the "Hide posts" radio button in the action selection dialog - */ -async function selectHidePostsOption(vm: FacebookViewModel): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; - - const divs = dialog.querySelectorAll('div[aria-disabled]'); - - for (const div of divs) { - const text = div.textContent?.toLowerCase() || ''; - if (text.includes('hide')) { - if (div.getAttribute('aria-disabled') === 'false') { - const radioButton = div.querySelector('i'); - if (radioButton) { - radioButton.click(); - return true; - } - } else { - console.log('Hide option is disabled'); - return false; - } - } - } - - console.log('Could not find hide option'); - return false; - })()`, - "selectHidePostsOption", + "clickDeletePostsOption", ); return result.success && result.value; } @@ -549,429 +444,79 @@ async function clickDoneButton(vm: FacebookViewModel): Promise { return result.success && result.value; } -export async function runJobDeleteWallPosts( +async function loadActivityLog( vm: FacebookViewModel, - jobIndex: number, + categoryKey: keyof typeof FACEBOOK_CATEGORY_KEYS ): Promise { - vm.runJobsState = RunJobsState.DeleteWallPosts; + if (vm.account.facebookAccount) { + vm.log("loadActivityLog", `Loading activity log for category key: ${categoryKey}`); - vm.showBrowser = true; - vm.showAutomationNotice = true; - vm.instructions = vm.t("viewModels.facebook.jobs.removingWallPosts"); - - vm.log("runJobDeleteWallPosts", "Loading profile page"); - - await vm.waitForPause(); + const FACEBOOK_ACTIVITY_LOG_URL = `https://www.facebook.com/${vm.account.facebookAccount.accountID}/\ +allactivity?activity_history=false&category_key=${FACEBOOK_CATEGORY_KEYS[categoryKey]}\ +&manage_mode=false&should_load_landing_page=false`; - // Load the user's profile page - await vm.loadURL(FACEBOOK_PROFILE_URL); - await vm.waitForLoadingToFinish(); - - await vm.waitForPause(); + await vm.loadURL(FACEBOOK_ACTIVITY_LOG_URL); + await vm.waitForLoadingToFinish(); - // Keep deleting posts until there are no more to delete - let totalDeleted = 0; - let totalUntagged = 0; - let totalHidden = 0; - let batchNumber = 0; - const maxToCheck = 10; - - while (true) { - // Check for rate limits - await checkRateLimit(vm); - - batchNumber++; - vm.log("runJobDeleteWallPosts", `Starting batch ${batchNumber}`); - - vm.log("runJobDeleteWallPosts", "Clicking Manage posts button"); - - // Click the Manage posts button - // safeExecuteJavaScript handles webview validation and errors - const buttonClicked = await clickManagePostsButton(vm); - if (!buttonClicked) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_ClickManagePostsFailed, - { - batchNumber, - message: "Failed to click Manage posts button", - }, - ); - return; - } + await vm.pause(); await vm.waitForPause(); + } +} - // Wait for the dialog to open - const dialogOpened = await waitForManagePostsDialog(vm); - if (!dialogOpened) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_DialogNotFound, - { - batchNumber, - message: "Manage posts dialog did not appear", - }, - ); - return; - } +export async function runJobDeleteWallPosts( + vm: FacebookViewModel, + jobIndex: number, +): Promise { + vm.runJobsState = RunJobsState.DeleteWallPosts; - vm.log("runJobDeleteWallPosts", "Dialog opened, waiting for posts to load"); + vm.showBrowser = true; + vm.showAutomationNotice = true; + vm.instructions = vm.t("viewModels.facebook.jobs.removingWallPosts"); + // TODO: might want to not hardcode but based on the options selected by users + const postCategoryKeys = [ + "user_posts", + "posts_on_others", + "others_posts", + "checkins", + "tagged_posts", + "tagged_media" + ] as (keyof typeof FACEBOOK_CATEGORY_KEYS)[]; + + for (const categoryKey of postCategoryKeys) { + // Load activity log page based on category key await vm.waitForPause(); + await loadActivityLog(vm, categoryKey); - // Wait for items to appear in the dialog (with 30 second timeout) - // On slow connections, the dialog content may take time to load - let allItems: { listIndex: number; itemIndex: number }[] = []; - const maxWaitTime = 30000; // 30 seconds - const pollInterval = 500; // Check every 500ms - const startTime = Date.now(); - - while (Date.now() - startTime < maxWaitTime) { - allItems = await getListsAndItems(vm); - if (allItems.length > 0) { - vm.log( - "runJobDeleteWallPosts", - `Found ${allItems.length} items after ${Date.now() - startTime}ms`, - ); - break; - } - await vm.sleep(pollInterval); - } - - if (allItems.length === 0) { - vm.log( - "runJobDeleteWallPosts", - `No items found after ${maxWaitTime}ms timeout, proceeding anyway`, - ); - } - - vm.log( - "runJobDeleteWallPosts", - `Found ${allItems.length} items with checkboxes`, - ); - - let checkedCount = 0; - const batchActions: PostAction[] = ["delete", "untag", "hide"]; // Check all actions in priority order - let batchAction: PostAction = "delete"; - - // loop through different actions - for (const action of batchActions) { - batchAction = action; - vm.instructions = vm.t( - "viewModels.facebook.jobs.checkBatchActionWallPosts", - { - action: vm.t(actionVerbKeys[batchAction]), - }, - ); - // Loop through items, checking if any item match the current batchAction priority action. - // Stop when adding a new item would reduce the priority (e.g. from delete -> hide). - for (const { listIndex, itemIndex } of allItems) { - // Check for rate limits - await checkRateLimit(vm); - - if (checkedCount >= maxToCheck) { - vm.log( - "runJobDeleteWallPosts", - `Reached maximum of ${maxToCheck} items`, - ); - break; - } - - await vm.waitForPause(); - - // Check this checkbox - const toggled = await toggleCheckbox(vm, listIndex, itemIndex, true); - if (!toggled) { - vm.log( - "runJobDeleteWallPosts", - `Failed to check item [${listIndex}][${itemIndex}]`, - ); - continue; - } - - const checkboxChecked = await waitForCheckboxState( - vm, - listIndex, - itemIndex, - true, - ); - if (!checkboxChecked) { - vm.log( - "runJobDeleteWallPosts", - `Timed out waiting for item [${listIndex}][${itemIndex}] to become checked`, - ); - continue; - } + // Keep deleting posts until there are no more to delete + let totalDeleted = 0; + while (true) { + // Check for rate limits + await checkRateLimit(vm); - // Read the combined action description (reflects all currently-checked items) - const actionDescription = await waitForActionDescriptionStable(vm); + // Check select all checkbox + const toggled = await toggleSelectAllCheckbox(vm, true); + if (!toggled) { vm.log( "runJobDeleteWallPosts", - `Action description: "${actionDescription}"`, + `Failed to check "All" checkbox`, ); - - const combinedPriority = getHighestPriority( - parseActions(actionDescription), - ); - - if (combinedPriority === null) { - // Unrecognized description, skip this item - vm.log( - "runJobDeleteWallPosts", - `Item [${listIndex}][${itemIndex}] has unrecognized action description, unchecking`, - ); - await toggleCheckbox(vm, listIndex, itemIndex, false); - await waitForCheckboxState(vm, listIndex, itemIndex, false); - continue; - } else if (combinedPriority === batchAction) { - // Same priority: keep this item checked and continue - checkedCount++; - vm.log( - "runJobDeleteWallPosts", - `Item keeps batch action "${batchAction}", checked ${checkedCount}/${maxToCheck}`, - ); - } else { - // Adding this item changes the priority — uncheck it and go to next item - vm.log( - "runJobDeleteWallPosts", - `Item [${listIndex}][${itemIndex}] changes priority from "${batchAction}" to "${combinedPriority}", unchecking`, - ); - await toggleCheckbox(vm, listIndex, itemIndex, false); - const checkboxUnchecked = await waitForCheckboxState( - vm, - listIndex, - itemIndex, - false, - ); - if (!checkboxUnchecked) { - vm.log( - "runJobDeleteWallPosts", - `Timed out waiting for item [${listIndex}][${itemIndex}] to become unchecked`, - ); - } - - const batchActionRestored = await waitForBatchAction(vm, batchAction); - if (!batchActionRestored.success && checkedCount !== 0) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed, - { - batchNumber, - message: `Batch action did not return to "${batchAction}" after unchecking item [${listIndex}][${itemIndex}]`, - actionDescription: batchActionRestored.actionDescription, - }, - ); - return; - } - continue; - } + continue; } - vm.log( - "runJobDeleteWallPosts", - `Selected ${checkedCount} items for action "${batchAction}"`, - ); - - if (checkedCount !== 0) { - // If actionable items found, no need to loop through other actions - vm.instructions = vm.t( - "viewModels.facebook.jobs.removeActionWallPosts", - { - action: vm.t(actionPresentKeys[batchAction]), - count: checkedCount, - }, - ); - break; - } + await vm.waitForPause(); - // If nothing was checked, see if more items get selected by next priority action in the list - if (batchAction !== "hide") { + // Click on trash + const deletedBtnClicked = await clickDeletePostsOption(vm); + if(deletedBtnClicked) { vm.log( "runJobDeleteWallPosts", - `No actionable items found for action "${batchAction}", checking next priority action`, + `Failed to click "Trash" button`, ); + continue; } } - - if (checkedCount === 0 && batchAction === "hide") { - // If the current action is hide and still checked item is 0, means all priority actions have - // been checked and nothing left to do. - vm.log("runJobDeleteWallPosts", "No actionable items found, finishing"); - break; - } - - await vm.waitForPause(); - - const batchActionReady = await waitForBatchAction(vm, batchAction); - if (!batchActionReady.success) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed, - { - batchNumber, - message: `Action description did not settle on "${batchAction}" before clicking Next`, - actionDescription: batchActionReady.actionDescription, - }, - ); - return; - } - - // Click the Next button - vm.log("runJobDeleteWallPosts", "Clicking Next button"); - const nextClicked = await clickNextButton(vm); - if (!nextClicked) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_ClickNextFailed, - { - batchNumber, - message: "Failed to click Next button", - }, - ); - return; - } - - // Wait for the dialog to update with the action options - const actionOptionsReady = await waitForActionOptionsDialog(vm); - if (!actionOptionsReady) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_DialogNotFound, - { - batchNumber, - message: "Action options did not appear after clicking Next", - }, - ); - return; - } - - await vm.waitForPause(); - - // Select the appropriate action radio button - vm.log("runJobDeleteWallPosts", `Selecting "${batchAction}" option`); - let actionSelected = false; - let actionErrorType = - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed; - let actionErrorMessage = "Failed to select delete posts option"; - - if (batchAction === "delete") { - actionSelected = await selectDeletePostsOption(vm); - actionErrorType = - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed; - actionErrorMessage = "Failed to select delete posts option"; - } else if (batchAction === "untag") { - actionSelected = await selectUntagPostsOption(vm); - actionErrorType = - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectUntagOptionFailed; - actionErrorMessage = "Failed to select untag posts option"; - } else { - // hide - actionSelected = await selectHidePostsOption(vm); - actionErrorType = - AutomationErrorType.facebook_runJob_deleteWallPosts_SelectHideOptionFailed; - actionErrorMessage = "Failed to select hide posts option"; - } - - if (!actionSelected) { - await reportDeleteWallPostsError(vm, jobIndex, actionErrorType, { - batchNumber, - message: actionErrorMessage, - }); - return; - } - - vm.log("runJobDeleteWallPosts", `"${batchAction}" option selected`); - - await vm.waitForPause(); - - // Click the Done button - vm.log("runJobDeleteWallPosts", "Clicking Done button"); - const doneClicked = await clickDoneButton(vm); - if (!doneClicked) { - await reportDeleteWallPostsError( - vm, - jobIndex, - AutomationErrorType.facebook_runJob_deleteWallPosts_ClickDoneFailed, - { - batchNumber, - message: "Failed to click Done button", - }, - ); - return; - } - - vm.log("runJobDeleteWallPosts", "Done button clicked"); - - await vm.waitForPause(); - - // Wait for the dialog to disappear (indicates deletion is complete) - vm.log("runJobDeleteWallPosts", "Waiting for deletion to complete..."); - const dialogDisappeared = await waitForManagePostsDialogToDisappear(vm); - if (!dialogDisappeared) { - vm.log( - "runJobDeleteWallPosts", - "Timeout waiting for dialog to disappear", - ); - // Continue anyway - the deletion might have worked - } else { - vm.log("runJobDeleteWallPosts", "Deletion completed successfully"); - } - - // Update progress - if (batchAction === "delete") { - totalDeleted += checkedCount; - vm.progress.wallPostsDeleted = totalDeleted; - } else if (batchAction === "untag") { - totalUntagged += checkedCount; - vm.progress.wallPostsUntagged = totalUntagged; - } else { - totalHidden += checkedCount; - vm.progress.wallPostsHidden = totalHidden; - } - vm.log( - "runJobDeleteWallPosts", - `Batch ${batchNumber} complete: ${batchAction} ${checkedCount} posts (deleted: ${totalDeleted}, untagged: ${totalUntagged}, hidden: ${totalHidden})`, - ); - - // Update the persistent counter in the database - if (batchAction === "delete") { - await window.electron.Facebook.incrementTotalWallPostsDeleted( - vm.account.id, - checkedCount, - ); - } else if (batchAction === "untag") { - await window.electron.Facebook.incrementTotalWallPostsUntagged( - vm.account.id, - checkedCount, - ); - } else { - await window.electron.Facebook.incrementTotalWallPostsHidden( - vm.account.id, - checkedCount, - ); - } - - // Submit progress to the API - vm.emitter?.emit(`facebook-submit-progress-${vm.account.id}`); - - await vm.waitForPause(); - - // Give Facebook a few seconds before refreshing - // It seems that this helps - await vm.sleep(3000); - - // Reload the profile page to see any newly available posts - vm.log("runJobDeleteWallPosts", "Reloading profile page for next batch"); - vm.instructions = vm.t("viewModels.facebook.jobs.managePostsLoading"); - await vm.loadURL(FACEBOOK_PROFILE_URL); - await vm.waitForLoadingToFinish(); } vm.log( From b85503b8b3e289fb1e1ba4f0afd696143fb69b2b Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:46:10 -0400 Subject: [PATCH 02/22] add additional variants to `FacebookProgress` --- .../view_models/FacebookViewModel/types.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/types.ts b/src/renderer/src/view_models/FacebookViewModel/types.ts index 8edae668..70643719 100644 --- a/src/renderer/src/view_models/FacebookViewModel/types.ts +++ b/src/renderer/src/view_models/FacebookViewModel/types.ts @@ -8,7 +8,7 @@ export enum RunJobsState { Default = "", SaveUserLang = "SaveUserLang", SetLangToEnglish = "SetLangToEnglish", - DeleteWallPosts = "DeleteWallPosts", + DeleteActivity = "DeleteActivity", RestoreUserLang = "RestoreUserLang", } @@ -17,7 +17,7 @@ export type FacebookJobType = | "login" | "saveUserLang" | "setLangToEnglish" - | "deleteWallPosts" + | "deleteActivity" | "restoreUserLang"; // Facebook job @@ -31,22 +31,40 @@ export type FacebookJob = { error: string | null; }; -// Facebook progress tracking +// Facebook progress tracking. export type FacebookProgress = { currentJob: string; + // The category currently being processed by the deleteActivity job (a category + // `setting` id, e.g. "deleteComments"), or "" when not deleting. + currentCategory: string; wallPostsDeleted: number; wallPostsUntagged: number; wallPostsHidden: number; - isDeleteWallPostsFinished: boolean; + commentsDeleted: number; + reactionsDeleted: number; + postsOnOthersDeleted: number; + othersPostsDeleted: number; + checkinsDeleted: number; + taggedPostsDeleted: number; + taggedMediaDeleted: number; + isDeleteActivityFinished: boolean; }; export function emptyFacebookProgress(): FacebookProgress { return { currentJob: "", + currentCategory: "", wallPostsDeleted: 0, wallPostsUntagged: 0, wallPostsHidden: 0, - isDeleteWallPostsFinished: false, + commentsDeleted: 0, + reactionsDeleted: 0, + postsOnOthersDeleted: 0, + othersPostsDeleted: 0, + checkinsDeleted: 0, + taggedPostsDeleted: 0, + taggedMediaDeleted: 0, + isDeleteActivityFinished: false, }; } From a90f23c5833ab3ac9b1e7f695bd67f262fae913f Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:47:49 -0400 Subject: [PATCH 03/22] add categories --- .../FacebookViewModel/categories.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/renderer/src/view_models/FacebookViewModel/categories.ts diff --git a/src/renderer/src/view_models/FacebookViewModel/categories.ts b/src/renderer/src/view_models/FacebookViewModel/categories.ts new file mode 100644 index 00000000..31adf7c2 --- /dev/null +++ b/src/renderer/src/view_models/FacebookViewModel/categories.ts @@ -0,0 +1,86 @@ +import type { FacebookAccount } from "../../../../shared_types"; +import type { FacebookProgress } from "./types"; + +// The account boolean settings that enable deleting each data category. +export type FacebookDeleteSetting = + | "deleteWallPosts" + | "deleteComments" + | "deleteReactions" + | "deletePostsOnOthers" + | "deleteOthersPosts" + | "deleteCheckins" + | "deleteTaggedPosts" + | "deleteTaggedMedia"; + +export type FacebookDeleteCategory = { + // The FacebookAccount boolean field that enables deleting this category. + setting: FacebookDeleteSetting & keyof FacebookAccount; + // The category_key URL parameter for the Facebook activity log. + categoryKey: string; + // The FacebookProgress counter incremented as items in this category are deleted. + counter: keyof FacebookProgress; + // The i18n key for the checkbox label shown on the delete options page. + labelKey: string; +}; + +// Data categories the user can choose to delete. +// The deleteActivity job, defineJobs(), and the delete options UI all derive from this. +export const FACEBOOK_DELETE_CATEGORIES: FacebookDeleteCategory[] = [ + { + setting: "deleteWallPosts", + categoryKey: "MANAGEPOSTSPHOTOSANDVIDEOS", + counter: "wallPostsDeleted", + labelKey: "facebook.deleteOptions.deleteWallPosts", + }, + { + setting: "deleteComments", + categoryKey: "COMMENTSCLUSTER", + counter: "commentsDeleted", + labelKey: "facebook.deleteOptions.deleteComments", + }, + { + setting: "deleteReactions", + categoryKey: "LIKEDPOSTS", + counter: "reactionsDeleted", + labelKey: "facebook.deleteOptions.deleteReactions", + }, + { + setting: "deletePostsOnOthers", + categoryKey: "POSTSONOTHERSTIMELINES", + counter: "postsOnOthersDeleted", + labelKey: "facebook.deleteOptions.deletePostsOnOthers", + }, + { + setting: "deleteOthersPosts", + categoryKey: "WALLCLUSTER", + counter: "othersPostsDeleted", + labelKey: "facebook.deleteOptions.deleteOthersPosts", + }, + { + setting: "deleteCheckins", + categoryKey: "CHECKINS", + counter: "checkinsDeleted", + labelKey: "facebook.deleteOptions.deleteCheckins", + }, + { + setting: "deleteTaggedPosts", + categoryKey: "MANAGETAGSBYOTHERSCLUSTER", + counter: "taggedPostsDeleted", + labelKey: "facebook.deleteOptions.deleteTaggedPosts", + }, + { + setting: "deleteTaggedMedia", + categoryKey: "TAGGEDPHOTOS", + counter: "taggedMediaDeleted", + labelKey: "facebook.deleteOptions.deleteTaggedMedia", + }, +]; + +// Returns the categories the user has enabled for deletion on this account. +export function selectedDeleteCategories( + account: FacebookAccount, +): FacebookDeleteCategory[] { + return FACEBOOK_DELETE_CATEGORIES.filter((category) => + Boolean(account[category.setting]), + ); +} From 9871776ad2fc804d68e923cc52198cc794135ee1 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:53:59 -0400 Subject: [PATCH 04/22] facebook: update account --- src/database/facebook_account.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/database/facebook_account.ts b/src/database/facebook_account.ts index 57ab8329..1e1027fe 100644 --- a/src/database/facebook_account.ts +++ b/src/database/facebook_account.ts @@ -10,6 +10,13 @@ interface FacebookAccountRow { profileImageDataURI: string; accountID: string | null; deleteWallPosts: number; + deleteComments: number; + deleteReactions: number; + deletePostsOnOthers: number; + deleteOthersPosts: number; + deleteCheckins: number; + deleteTaggedPosts: number; + deleteTaggedMedia: number; userLang: string; } @@ -25,6 +32,13 @@ function facebookAccountRowToFacebookAccount( profileImageDataURI: row.profileImageDataURI, accountID: row.accountID, deleteWallPosts: row.deleteWallPosts === 1, + deleteComments: row.deleteComments === 1, + deleteReactions: row.deleteReactions === 1, + deletePostsOnOthers: row.deletePostsOnOthers === 1, + deleteOthersPosts: row.deleteOthersPosts === 1, + deleteCheckins: row.deleteCheckins === 1, + deleteTaggedPosts: row.deleteTaggedPosts === 1, + deleteTaggedMedia: row.deleteTaggedMedia === 1, userLang: row.userLang || "English (US)", }; } @@ -81,6 +95,13 @@ export const saveFacebookAccount = (account: FacebookAccount) => { profileImageDataURI = ?, accountID = ?, deleteWallPosts = ?, + deleteComments = ?, + deleteReactions = ?, + deletePostsOnOthers = ?, + deleteOthersPosts = ?, + deleteCheckins = ?, + deleteTaggedPosts = ?, + deleteTaggedMedia = ?, userLang = ? WHERE id = ? `, @@ -89,6 +110,13 @@ export const saveFacebookAccount = (account: FacebookAccount) => { account.profileImageDataURI, account.accountID, account.deleteWallPosts ? 1 : 0, + account.deleteComments ? 1 : 0, + account.deleteReactions ? 1 : 0, + account.deletePostsOnOthers ? 1 : 0, + account.deleteOthersPosts ? 1 : 0, + account.deleteCheckins ? 1 : 0, + account.deleteTaggedPosts ? 1 : 0, + account.deleteTaggedMedia ? 1 : 0, account.userLang || "English (US)", account.id, ], From 0d7ed66a3b576c9450c4c0db1ba2c33b7c405b88 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:54:50 -0400 Subject: [PATCH 05/22] facebook: in shared type track desired deletion categories --- src/shared_types/account.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/shared_types/account.ts b/src/shared_types/account.ts index 3075eee2..1ca95460 100644 --- a/src/shared_types/account.ts +++ b/src/shared_types/account.ts @@ -96,6 +96,13 @@ export type FacebookAccount = { profileImageDataURI: string; accountID: string | null; deleteWallPosts: boolean; + deleteComments: boolean; + deleteReactions: boolean; + deletePostsOnOthers: boolean; + deleteOthersPosts: boolean; + deleteCheckins: boolean; + deleteTaggedPosts: boolean; + deleteTaggedMedia: boolean; userLang: string; }; From e04d92cf4056f11ab322147046ec0967e3f5a3f8 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:56:50 -0400 Subject: [PATCH 06/22] facebook: add `deleteActivity` jobs in view model --- .../view_models/FacebookViewModel/view_model.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/view_model.ts b/src/renderer/src/view_models/FacebookViewModel/view_model.ts index 06025664..b2360d6b 100644 --- a/src/renderer/src/view_models/FacebookViewModel/view_model.ts +++ b/src/renderer/src/view_models/FacebookViewModel/view_model.ts @@ -21,6 +21,7 @@ import { import * as AuthOps from "./auth"; import * as DeleteJobs from "./jobs_delete"; import * as LangJobs from "./jobs_lang"; +import { selectedDeleteCategories } from "./categories"; interface CurrentUserInitialData { ACCOUNT_ID: string; @@ -139,16 +140,19 @@ export class FacebookViewModel extends BaseViewModel { // Always login first jobTypes.push("login"); - // Add delete jobs if enabled - if (this.account.facebookAccount?.deleteWallPosts) { + // Add deleteActivity job if the user wants at least one data category deleted + const hasDeleteCategories = + this.account.facebookAccount && + selectedDeleteCategories(this.account.facebookAccount).length > 0; + if (hasDeleteCategories) { // Language jobs wrap around delete jobs: // 1. Save user's current language // 2. Set language to English (needed for automation) - // 3. Delete wall posts + // 3. Delete the selected activity categories // 4. Restore user's original language jobTypes.push("saveUserLang"); jobTypes.push("setLangToEnglish"); - jobTypes.push("deleteWallPosts"); + jobTypes.push("deleteActivity"); jobTypes.push("restoreUserLang"); } @@ -319,8 +323,8 @@ export class FacebookViewModel extends BaseViewModel { await LangJobs.runJobSetLangToEnglish(this, jobIndex); break; - case "deleteWallPosts": - await DeleteJobs.runJobDeleteWallPosts(this, jobIndex); + case "deleteActivity": + await DeleteJobs.runJobDeleteActivity(this, jobIndex); break; case "restoreUserLang": From ecc854258edf20c34567cc1dc047ea46245a1e77 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:57:09 -0400 Subject: [PATCH 07/22] facebook: add migrations for data deletion --- src/database/migrations.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/database/migrations.ts b/src/database/migrations.ts index 65b35874..6de5f33a 100644 --- a/src/database/migrations.ts +++ b/src/database/migrations.ts @@ -202,5 +202,19 @@ export const runMainMigrations = () => { );`, ], }, + // Add per-category delete settings so users can choose which activity-log + // data to delete (comments, reactions, etc.) independently of wall posts. + { + name: "add per-category delete settings to facebookAccount table", + sql: [ + `ALTER TABLE facebookAccount ADD COLUMN deleteComments INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deleteReactions INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deletePostsOnOthers INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deleteOthersPosts INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deleteCheckins INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deleteTaggedPosts INTEGER DEFAULT 0;`, + `ALTER TABLE facebookAccount ADD COLUMN deleteTaggedMedia INTEGER DEFAULT 0;`, + ], + }, ]); }; From 4e8f273f7e7d1ae9b388456253295e8c370d70a3 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 16:58:38 -0400 Subject: [PATCH 08/22] facebook: track aggregate deletion activity for server --- src/shared_types/facebook.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/shared_types/facebook.ts b/src/shared_types/facebook.ts index 345c418c..558b700e 100644 --- a/src/shared_types/facebook.ts +++ b/src/shared_types/facebook.ts @@ -12,6 +12,13 @@ export type FacebookProgress = { wallPostsDeleted: number; wallPostsUntagged: number; wallPostsHidden: number; + commentsDeleted: number; + reactionsDeleted: number; + postsOnOthersDeleted: number; + othersPostsDeleted: number; + checkinsDeleted: number; + taggedPostsDeleted: number; + taggedMediaDeleted: number; }; export function emptyFacebookProgress(): FacebookProgress { @@ -19,9 +26,29 @@ export function emptyFacebookProgress(): FacebookProgress { wallPostsDeleted: 0, wallPostsUntagged: 0, wallPostsHidden: 0, + commentsDeleted: 0, + reactionsDeleted: 0, + postsOnOthersDeleted: 0, + othersPostsDeleted: 0, + checkinsDeleted: 0, + taggedPostsDeleted: 0, + taggedMediaDeleted: 0, }; } +// getProgressInfo aggregates these for the server to track cumulative +// deletion totals (across runs) +export const FACEBOOK_DELETE_COUNTERS: (keyof FacebookProgress)[] = [ + "wallPostsDeleted", + "commentsDeleted", + "reactionsDeleted", + "postsOnOthersDeleted", + "othersPostsDeleted", + "checkinsDeleted", + "taggedPostsDeleted", + "taggedMediaDeleted", +]; + export type FacebookRateLimitInfo = { isRateLimited: boolean; rateLimitReset: number; From 83faf4ff3e821b03762de890d9266685880f280e Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:00:26 -0400 Subject: [PATCH 09/22] temp: sum all deletion activity for `totalWallPostsDeleted` --- .../controller/stats/getProgressInfo.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/account_facebook/controller/stats/getProgressInfo.ts b/src/account_facebook/controller/stats/getProgressInfo.ts index 173b45c6..6b6a3971 100644 --- a/src/account_facebook/controller/stats/getProgressInfo.ts +++ b/src/account_facebook/controller/stats/getProgressInfo.ts @@ -2,6 +2,7 @@ import type { FacebookAccountController } from "../../facebook_account_controlle import { FacebookProgressInfo, emptyFacebookProgressInfo, + FACEBOOK_DELETE_COUNTERS, } from "../../../shared_types"; export async function getProgressInfo( @@ -11,33 +12,20 @@ export async function getProgressInfo( controller.initDB(); } - const totalWallPostsDeletedConfig: string | null = await controller.getConfig( - "totalWallPostsDeleted", - ); - let totalWallPostsDeleted: number = 0; - if (totalWallPostsDeletedConfig) { - totalWallPostsDeleted = parseInt(totalWallPostsDeletedConfig); - } - - const totalWallPostsUntaggedConfig: string | null = - await controller.getConfig("totalWallPostsUntagged"); - let totalWallPostsUntagged: number = 0; - if (totalWallPostsUntaggedConfig) { - totalWallPostsUntagged = parseInt(totalWallPostsUntaggedConfig); - } - - const totalWallPostsHiddenConfig: string | null = await controller.getConfig( - "totalWallPostsHidden", - ); - let totalWallPostsHidden: number = 0; - if (totalWallPostsHiddenConfig) { - totalWallPostsHidden = parseInt(totalWallPostsHiddenConfig); + // temp: we need to update the server, currently it is summing all deletion + // activity and stuffing it into totalWallPostsDeleted + let totalWallPostsDeleted = 0; + for (const counter of FACEBOOK_DELETE_COUNTERS) { + const value = await controller.getConfig(`total_${counter}`); + if (value) { + totalWallPostsDeleted += parseInt(value); + } } const progressInfo = emptyFacebookProgressInfo(); progressInfo.accountUUID = controller.accountUUID; progressInfo.totalWallPostsDeleted = totalWallPostsDeleted; - progressInfo.totalWallPostsUntagged = totalWallPostsUntagged; - progressInfo.totalWallPostsHidden = totalWallPostsHidden; + progressInfo.totalWallPostsUntagged = 0; + progressInfo.totalWallPostsHidden = 0; return progressInfo; } From 7b44393fcffe3bcf83e43d0229a7620d437ba3c8 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:01:20 -0400 Subject: [PATCH 10/22] facebook: add helper function for `incrementCumulativeTotal` --- .../view_models/FacebookViewModel/helpers.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/renderer/src/view_models/FacebookViewModel/helpers.ts b/src/renderer/src/view_models/FacebookViewModel/helpers.ts index 1babb2cc..b45a6705 100644 --- a/src/renderer/src/view_models/FacebookViewModel/helpers.ts +++ b/src/renderer/src/view_models/FacebookViewModel/helpers.ts @@ -34,6 +34,25 @@ export async function errorJob( vm.log("errorJob", vm.jobs[jobIndex].jobType); } +// Persist a cumulative deletion total for a progress counter (for the server) +export async function incrementCumulativeTotal( + vm: FacebookViewModel, + counter: string, + count: number, +): Promise { + if (count <= 0) { + return; + } + const key = `total_${counter}`; + const current = await window.electron.Facebook.getConfig(vm.account.id, key); + const newValue = (current ? parseInt(current) : 0) + count; + await window.electron.Facebook.setConfig( + vm.account.id, + key, + newValue.toString(), + ); +} + export async function syncProgress(vm: FacebookViewModel): Promise { // For now, just log progress - can be expanded to persist to database vm.log("syncProgress", JSON.stringify(vm.progress)); From b73ee47b38839a9124968ff7645311ad29a13040 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:02:13 -0400 Subject: [PATCH 11/22] facebook: add `FacebookDeleteCounter` --- .../view_models/FacebookViewModel/categories.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/view_models/FacebookViewModel/categories.ts b/src/renderer/src/view_models/FacebookViewModel/categories.ts index 31adf7c2..4f8aa155 100644 --- a/src/renderer/src/view_models/FacebookViewModel/categories.ts +++ b/src/renderer/src/view_models/FacebookViewModel/categories.ts @@ -1,5 +1,4 @@ import type { FacebookAccount } from "../../../../shared_types"; -import type { FacebookProgress } from "./types"; // The account boolean settings that enable deleting each data category. export type FacebookDeleteSetting = @@ -12,13 +11,24 @@ export type FacebookDeleteSetting = | "deleteTaggedPosts" | "deleteTaggedMedia"; +// The numeric FacebookProgress fields, one per category. +export type FacebookDeleteCounter = + | "wallPostsDeleted" + | "commentsDeleted" + | "reactionsDeleted" + | "postsOnOthersDeleted" + | "othersPostsDeleted" + | "checkinsDeleted" + | "taggedPostsDeleted" + | "taggedMediaDeleted"; + export type FacebookDeleteCategory = { // The FacebookAccount boolean field that enables deleting this category. setting: FacebookDeleteSetting & keyof FacebookAccount; // The category_key URL parameter for the Facebook activity log. categoryKey: string; // The FacebookProgress counter incremented as items in this category are deleted. - counter: keyof FacebookProgress; + counter: FacebookDeleteCounter; // The i18n key for the checkbox label shown on the delete options page. labelKey: string; }; From 786a2ef0eace86677701b9a9b95b53c8ded13a05 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:18:43 -0400 Subject: [PATCH 12/22] facebook: update and generalize the delete runner --- src/renderer/src/util_facebook.ts | 8 +- .../FacebookViewModel/jobs_delete.ts | 554 +++++------------- 2 files changed, 152 insertions(+), 410 deletions(-) diff --git a/src/renderer/src/util_facebook.ts b/src/renderer/src/util_facebook.ts index e747e0f6..390a37e1 100644 --- a/src/renderer/src/util_facebook.ts +++ b/src/renderer/src/util_facebook.ts @@ -36,13 +36,13 @@ export async function facebookPostProgress( export async function facebookGetLastDelete( accountID: number, ): Promise { - const lastFinishedJob_deleteWallPosts = + const lastFinishedJob_deleteActivity = await window.electron.Facebook.getConfig( accountID, - "lastFinishedJob_deleteWallPosts", + "lastFinishedJob_deleteActivity", ); - if (lastFinishedJob_deleteWallPosts) { - return new Date(lastFinishedJob_deleteWallPosts); + if (lastFinishedJob_deleteActivity) { + return new Date(lastFinishedJob_deleteActivity); } return null; } diff --git a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts index e5ee19fb..cf054290 100644 --- a/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts +++ b/src/renderer/src/view_models/FacebookViewModel/jobs_delete.ts @@ -3,18 +3,11 @@ import { RunJobsState } from "./types"; import * as Helpers from "./helpers"; import { checkRateLimit } from "./rate_limit"; import { AutomationErrorType } from "../../automation_errors"; +import { + selectedDeleteCategories, + type FacebookDeleteCategory, +} from "./categories"; -const FACEBOOK_PROFILE_URL = "https://www.facebook.com/me/"; -const FACEBOOK_CATEGORY_KEYS = { - "comments": "COMMENTSCLUSTER", - "reactions": "LIKEDPOSTS", - "user_posts": "MANAGEPOSTSPHOTOSANDVIDEOS", - "posts_on_others": "POSTSONOTHERSTIMELINES", - "others_posts": "WALLCLUSTER", - "checkins": "CHECKINS", - "tagged_posts": "MANAGETAGSBYOTHERSCLUSTER", - "tagged_media": "TAGGEDPHOTOS", -} const ACTIVITY_LOG_CHECKBOX_NAME = "comet_activity_log_select_all_checkbox" async function reportDeleteWallPostsError( @@ -29,288 +22,6 @@ async function reportDeleteWallPostsError( await Helpers.errorJob(vm, jobIndex); } -/** - * Click the "Manage posts" button on the profile page - */ -async function clickManagePostsButton(vm: FacebookViewModel): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const buttons = document.querySelectorAll('div[aria-label="Manage posts"][role="button"]'); - if (buttons.length > 0) { - buttons[0].click(); - return true; - } - return false; - })()`, - "clickManagePostsButton", - ); - return result.success && result.value; -} - -/** - * Wait for the "Manage posts" dialog to appear - */ -async function waitForManagePostsDialog( - vm: FacebookViewModel, -): Promise { - // Wait up to 30 seconds for dialog to appear - for (let i = 0; i < 60; i++) { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - return !!dialog; - })()`, - "waitForManagePostsDialog", - ); - - if (!result.success) { - return false; - } - - if (result.value) { - // Give it a moment for content to load - await vm.sleep(500); - return true; - } - await vm.sleep(500); - } - return false; -} - -/** - * Wait for the "Manage posts" dialog to disappear - * This indicates the deletion process has completed - */ -async function waitForManagePostsDialogToDisappear( - vm: FacebookViewModel, -): Promise { - // Wait up to 60 seconds for dialog to disappear (deletion might take a while) - for (let i = 0; i < 120; i++) { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - return !!dialog; - })()`, - "waitForManagePostsDialogToDisappear", - ); - - if (!result.success) { - return false; - } - - if (!result.value) { - vm.log("waitForManagePostsDialogToDisappear", "Dialog has disappeared"); - return true; - } - await vm.sleep(500); - } - - vm.log( - "waitForManagePostsDialogToDisappear", - "Timeout waiting for dialog to disappear", - ); - return false; -} - -/** - * Get the action description text from the dialog - * Returns text like "You can hide or delete the posts selected." or empty string - */ -async function getActionDescription(vm: FacebookViewModel): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return ""; - - // Find the actions description span - // It's nested in the structure described by the user - // We'll search for spans that contain text about deletion/hiding - const spans = dialog.querySelectorAll('span'); - for (const span of spans) { - const text = span.textContent?.trim() || ""; - if (text.startsWith("You can")) { - return text; - } - } - return ""; - })()`, - "getActionDescription", - ); - return result.success ? result.value || "" : ""; -} - -type PostAction = "delete" | "untag" | "hide"; - -const actionVerbKeys: Record = { - delete: "viewModels.facebook.jobs.actionDelete", - untag: "viewModels.facebook.jobs.actionUntag", - hide: "viewModels.facebook.jobs.actionHide", -}; - -const actionPresentKeys: Record = { - delete: "viewModels.facebook.jobs.actionDeletePresent", - untag: "viewModels.facebook.jobs.actionUntagPresent", - hide: "viewModels.facebook.jobs.actionHidePresent", -}; - -async function getCheckboxState( - vm: FacebookViewModel, - listIndex: number, - itemIndex: number, -): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return null; - - const lists = dialog.querySelectorAll('div[role="list"]'); - if (${listIndex} >= lists.length) return null; - - const list = lists[${listIndex}]; - const items = list.querySelectorAll('div[role="listitem"]'); - if (${itemIndex} >= items.length) return null; - - const item = items[${itemIndex}]; - const checkbox = item.querySelector('input[type="checkbox"]'); - const checkboxControl = item.querySelector('[role="checkbox"]'); - const ariaChecked = - checkboxControl?.getAttribute('aria-checked') ?? - checkbox?.getAttribute('aria-checked'); - - if (ariaChecked === 'true') return true; - if (ariaChecked === 'false') return false; - if (checkbox instanceof HTMLInputElement) return checkbox.checked; - - return null; - })()`, - "getCheckboxState", - ); - - return result.success ? result.value : null; -} - -async function waitForCheckboxState( - vm: FacebookViewModel, - listIndex: number, - itemIndex: number, - expectedChecked: boolean, - timeoutMs: number = 5000, -): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeoutMs) { - const checked = await getCheckboxState(vm, listIndex, itemIndex); - if (checked === expectedChecked) { - return true; - } - await vm.sleep(200); - } - - return false; -} - -async function waitForActionDescriptionStable( - vm: FacebookViewModel, - timeoutMs: number = 5000, -): Promise { - const startTime = Date.now(); - let lastDescription = ""; - - while (Date.now() - startTime < timeoutMs) { - const description = await getActionDescription(vm); - if (description !== "" && description === lastDescription) { - return description; - } - lastDescription = description; - await vm.sleep(200); - } - - return lastDescription; -} - -async function waitForBatchAction( - vm: FacebookViewModel, - expectedAction: PostAction, - timeoutMs: number = 5000, -): Promise<{ success: boolean; actionDescription: string }> { - const startTime = Date.now(); - let lastDescription = ""; - - while (Date.now() - startTime < timeoutMs) { - const actionDescription = await getActionDescription(vm); - lastDescription = actionDescription; - - if ( - getHighestPriority(parseActions(actionDescription)) === expectedAction - ) { - return { success: true, actionDescription }; - } - - await vm.sleep(200); - } - - return { success: false, actionDescription: lastDescription }; -} - -async function waitForActionOptionsDialog( - vm: FacebookViewModel, - timeoutMs: number = 10000, -): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeoutMs) { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; - - const hasActionOptions = dialog.querySelector('div[aria-disabled]'); - const hasDoneButton = dialog.querySelector('div[aria-label="Done"][role="button"]'); - return Boolean(hasActionOptions && hasDoneButton); - })()`, - "waitForActionOptionsDialog", - ); - - if (result.success && result.value) { - return true; - } - - await vm.sleep(200); - } - - return false; -} - -/** - * Parse the available actions from an action description string. - * e.g. "You can hide or delete the posts selected." -> ['delete', 'hide'] - * "You can untag yourself from or hide the posts selected." -> ['untag', 'hide'] - * "You can hide the posts selected." -> ['hide'] - */ -export function parseActions(actionDescription: string): PostAction[] { - if (typeof actionDescription !== "string") { - return []; - } - - const actions: PostAction[] = []; - const text = actionDescription.toLowerCase(); - if (text.includes("delete")) actions.push("delete"); - if (text.includes("untag")) actions.push("untag"); - if (text.includes("hide")) actions.push("hide"); - return actions; -} - -/** - * Return the highest-priority action from a list. - * Priority order: delete > untag > hide - */ -export function getHighestPriority(actions: PostAction[]): PostAction | null { - if (actions.includes("delete")) return "delete"; - if (actions.includes("untag")) return "untag"; - if (actions.includes("hide")) return "hide"; - return null; -} - /** * Toggle a checkbox by name and return success */ @@ -320,7 +31,9 @@ async function toggleSelectAllCheckbox(vm: FacebookViewModel, shouldCheck: boole const checkbox = document.querySelector('input[name="${ACTIVITY_LOG_CHECKBOX_NAME}"]'); if (!checkbox) return false; - const isChecked = checkbox?.getAttribute('aria-checked') || checkbox.checked; + // aria-checked is the string "true"/"false"; fall back to the native checked prop. + const ariaChecked = checkbox.getAttribute('aria-checked'); + const isChecked = ariaChecked === 'true' ? true : (ariaChecked === 'false' ? false : checkbox.checked); const shouldCheck = ${shouldCheck}; @@ -337,61 +50,20 @@ async function toggleSelectAllCheckbox(vm: FacebookViewModel, shouldCheck: boole } /** - * Get the total number of lists and items + * Count the selectable items (row checkboxes, excluding the "select all" header checkbox) + * so we can report how many items a delete batch removed. */ -async function getListsAndItems( - vm: FacebookViewModel, -): Promise<{ listIndex: number; itemIndex: number }[]> { - const result = await vm.safeExecuteJavaScript< - { listIndex: number; itemIndex: number }[] - >( +async function countSelectableItems(vm: FacebookViewModel): Promise { + const result = await vm.safeExecuteJavaScript( `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return []; - - const lists = dialog.querySelectorAll('div[role="list"]'); - const result = []; - - for (let listIndex = 0; listIndex < lists.length; listIndex++) { - const list = lists[listIndex]; - const listItems = list.querySelectorAll('div[role="listitem"]'); - - for (let itemIndex = 0; itemIndex < listItems.length; itemIndex++) { - const item = listItems[itemIndex]; - const checkbox = item.querySelector('input[type="checkbox"]'); - if (checkbox) { - result.push({ listIndex, itemIndex }); - } - } - } - - return result; + const checkboxes = document.querySelectorAll( + 'input[type="checkbox"]:not([name="${ACTIVITY_LOG_CHECKBOX_NAME}"])' + ); + return checkboxes.length; })()`, - "getListsAndItems", + "countSelectableItems", ); - if (!result.success || !Array.isArray(result.value)) return []; - return result.value; -} - -/** - * Click the Next button in the dialog - */ -async function clickNextButton(vm: FacebookViewModel): Promise { - const result = await vm.safeExecuteJavaScript( - `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); - if (!dialog) return false; - - const nextButton = dialog.querySelector('div[aria-label="Next"][role="button"]'); - if (nextButton) { - nextButton.click(); - return true; - } - return false; - })()`, - "clickNextButton", - ); - return result.success && result.value; + return result.success && typeof result.value === "number" ? result.value : 0; } /** @@ -424,109 +96,179 @@ async function clickDeletePostsOption( } /** - * Click the Done button in the dialog + * Confirm the "Move to trash?" dialog that appears after clicking Trash. + * Best-effort: some flows delete without a confirmation step. */ -async function clickDoneButton(vm: FacebookViewModel): Promise { +async function confirmDeletion(vm: FacebookViewModel): Promise { const result = await vm.safeExecuteJavaScript( `(() => { - const dialog = document.querySelector('div[aria-label="Manage posts"][role="dialog"]'); + const dialog = document.querySelector('div[role="dialog"]'); if (!dialog) return false; - - const doneButton = dialog.querySelector('div[aria-label="Done"][role="button"]'); - if (doneButton) { - doneButton.click(); - return true; + const buttons = dialog.querySelectorAll('div[role="button"], button'); + for (const button of buttons) { + const label = (button.getAttribute('aria-label') || button.textContent || '').trim().toLowerCase(); + if (['delete', 'move to trash', 'confirm', 'remove'].includes(label)) { + if (button.getAttribute('aria-disabled') === 'true') return false; + button.click(); + return true; + } } return false; })()`, - "clickDoneButton", + "confirmDeletion", ); return result.success && result.value; } +/** + * Wait for a delete batch to be applied: the select-all checkbox clears or disappears. + */ +async function waitForBatchToComplete( + vm: FacebookViewModel, + timeoutMs: number = 30000, +): Promise { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutMs) { + const settled = await vm.safeExecuteJavaScript( + `(() => { + const checkbox = document.querySelector('input[name="${ACTIVITY_LOG_CHECKBOX_NAME}"]'); + if (!checkbox) return true; // no items left + const ariaChecked = checkbox.getAttribute('aria-checked'); + const isChecked = ariaChecked === 'true' ? true : (ariaChecked === 'false' ? false : checkbox.checked); + return !isChecked; // selection cleared => batch applied + })()`, + "waitForBatchToComplete", + ); + if (settled.success && settled.value) { + return true; + } + await vm.sleep(500); + } + return false; +} + async function loadActivityLog( vm: FacebookViewModel, - categoryKey: keyof typeof FACEBOOK_CATEGORY_KEYS + categoryKey: string, ): Promise { if (vm.account.facebookAccount) { vm.log("loadActivityLog", `Loading activity log for category key: ${categoryKey}`); const FACEBOOK_ACTIVITY_LOG_URL = `https://www.facebook.com/${vm.account.facebookAccount.accountID}/\ -allactivity?activity_history=false&category_key=${FACEBOOK_CATEGORY_KEYS[categoryKey]}\ -&manage_mode=false&should_load_landing_page=false`; +allactivity?activity_history=false&category_key=${categoryKey}\ +&manage_mode=true&should_load_landing_page=false`; await vm.loadURL(FACEBOOK_ACTIVITY_LOG_URL); await vm.waitForLoadingToFinish(); - await vm.pause(); + await vm.waitForPause(); + } +} + +/** + * Delete every item in a single activity-log category, batch by batch, until none remain. + * Returns the number of items deleted, or null if the job errored. + */ +async function deleteCategory( + vm: FacebookViewModel, + jobIndex: number, + category: FacebookDeleteCategory, +): Promise { + await loadActivityLog(vm, category.categoryKey); + + // Keep deleting until there are no more items to delete + while (true) { + // Check for rate limits + await checkRateLimit(vm); + await vm.waitForPause(); + + // Select all currently loaded items. The checkbox only exists when there are + // items, so a false result means we're done with this category. + const toggled = await toggleSelectAllCheckbox(vm, true); + if (!toggled) { + vm.log("deleteCategory", `No more items for category ${category.setting}`); + break; + } + + // Count what we're about to delete so we can report progress + const batchCount = await countSelectableItems(vm); await vm.waitForPause(); + + // Click on trash + const deletedBtnClicked = await clickDeletePostsOption(vm); + if (!deletedBtnClicked) { + vm.log("deleteCategory", `Failed to click "Trash" button`); + await reportDeleteWallPostsError( + vm, + jobIndex, + AutomationErrorType.facebook_runJob_deleteWallPosts_SelectDeleteOptionFailed, + { category: category.setting, message: "Failed to click Trash button" }, + ); + return null; + } + + // Confirm the deletion dialog if one appears, then wait for the batch to apply + await vm.sleep(1000); + await confirmDeletion(vm); + const completed = await waitForBatchToComplete(vm); + if (!completed) { + await reportDeleteWallPostsError( + vm, + jobIndex, + AutomationErrorType.facebook_runJob_deleteWallPosts_CompletionTimeout, + { category: category.setting, message: "Batch did not complete" }, + ); + return null; + } + + // Record progress for this batch + vm.progress[category.counter] += batchCount; + await Helpers.incrementCumulativeTotal(vm, category.counter, batchCount); + vm.emitter?.emit(`facebook-submit-progress-${vm.account.id}`); } + + return vm.progress[category.counter]; } -export async function runJobDeleteWallPosts( +export async function runJobDeleteActivity( vm: FacebookViewModel, jobIndex: number, ): Promise { - vm.runJobsState = RunJobsState.DeleteWallPosts; + vm.runJobsState = RunJobsState.DeleteActivity; vm.showBrowser = true; vm.showAutomationNotice = true; - vm.instructions = vm.t("viewModels.facebook.jobs.removingWallPosts"); - - // TODO: might want to not hardcode but based on the options selected by users - const postCategoryKeys = [ - "user_posts", - "posts_on_others", - "others_posts", - "checkins", - "tagged_posts", - "tagged_media" - ] as (keyof typeof FACEBOOK_CATEGORY_KEYS)[]; - - for (const categoryKey of postCategoryKeys) { - // Load activity log page based on category key + vm.progress.isDeleteActivityFinished = false; + + // Delete each data category the user selected. Every category uses the identical + // activity-log flow and differs only by its category_key. + const categories = vm.account.facebookAccount + ? selectedDeleteCategories(vm.account.facebookAccount) + : []; + + for (const category of categories) { await vm.waitForPause(); - await loadActivityLog(vm, categoryKey); - - // Keep deleting posts until there are no more to delete - let totalDeleted = 0; - while (true) { - // Check for rate limits - await checkRateLimit(vm); - - // Check select all checkbox - const toggled = await toggleSelectAllCheckbox(vm, true); - if (!toggled) { - vm.log( - "runJobDeleteWallPosts", - `Failed to check "All" checkbox`, - ); - continue; - } - await vm.waitForPause(); + vm.progress.currentCategory = category.setting; + vm.instructions = vm.t("viewModels.facebook.jobs.deletingCategory", { + category: vm.t(category.labelKey), + }); - // Click on trash - const deletedBtnClicked = await clickDeletePostsOption(vm); - if(deletedBtnClicked) { - vm.log( - "runJobDeleteWallPosts", - `Failed to click "Trash" button`, - ); - continue; - } + const deleted = await deleteCategory(vm, jobIndex, category); + if (deleted === null) { + // deleteCategory already marked the job as errored + return; } } - vm.log( - "runJobDeleteWallPosts", - `All done! Total posts deleted: ${totalDeleted}`, - ); + vm.progress.currentCategory = ""; + vm.progress.isDeleteActivityFinished = true; + vm.log("runJobDeleteActivity", "All done!"); await vm.waitForPause(); - // Always submit final progress to the API (even if 0 posts were deleted) + // Always submit final progress to the API (even if 0 items were deleted) vm.emitter?.emit(`facebook-submit-progress-${vm.account.id}`); await Helpers.finishJob(vm, jobIndex); From 47dfb6c82accb335dfc543967f57964effecadf1 Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:20:34 -0400 Subject: [PATCH 13/22] facebook: add i18n strings --- src/renderer/src/i18n/locales/en.json | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index e3cc3a52..1944dba8 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -420,20 +420,39 @@ "deleteWallDescription": "Remove all posts from your Facebook wall." }, "deleteOptions": { - "title": "Delete My Wall", - "description": "Select the data you want to remove from your Facebook wall.", - "deleteWallPosts": "Remove all posts from my wall" + "title": "Delete My Facebook Data", + "description": "Select the data you want to delete from your Facebook activity log.", + "deleteWallPosts": "Posts, photos, and videos", + "deleteComments": "Comments", + "deleteReactions": "Likes and reactions", + "deletePostsOnOthers": "Your posts on other people's timelines", + "deleteOthersPosts": "Other people's posts on your timeline", + "deleteCheckins": "Check-ins", + "deleteTaggedPosts": "Posts you're tagged in", + "deleteTaggedMedia": "Photos and videos you're tagged in" }, "review": { - "deleteWallPosts": "All posts from your Facebook wall", + "deleteWallPosts": "Your posts, photos, and videos", + "deleteComments": "Your comments", + "deleteReactions": "Your likes and reactions", + "deletePostsOnOthers": "Your posts on other people's timelines", + "deleteOthersPosts": "Other people's posts on your timeline", + "deleteCheckins": "Your check-ins", + "deleteTaggedPosts": "Posts you're tagged in", + "deleteTaggedMedia": "Photos and videos you're tagged in", "languageSettingsMightChange": "Your Facebook language settings might temporarily change.", "languageSettingsDescription": "If your Facebook account isn't already set to English, Cyd will set it to English, delete your data, and then change it back to your preferred language." }, "finished": { "title": "Jobs Completed", - "wallPostsDeleted": "wall posts deleted", - "wallPostsUntagged": "wall posts untagged", - "wallPostsHidden": "wall posts hidden" + "wallPostsDeleted": "posts, photos, and videos deleted", + "commentsDeleted": "comments deleted", + "reactionsDeleted": "likes and reactions deleted", + "postsOnOthersDeleted": "posts on others' timelines deleted", + "othersPostsDeleted": "others' posts on your timeline deleted", + "checkinsDeleted": "check-ins deleted", + "taggedPostsDeleted": "tagged posts deleted", + "taggedMediaDeleted": "tagged photos and videos deleted" }, "premium": { "readyToDelete": "You're all set! Let's continue to configure what you want to delete.", @@ -632,6 +651,7 @@ "savingLanguage": "I'm checking your language settings.", "settingLanguageToEnglish": "I'm temporarily changing your language to English (US) for automation.", "restoringLanguage": "I'm restoring your original language setting.", + "deletingCategory": "# I'm deleting **{category}** from your Facebook activity log.", "removingWallPosts": "# I'm removing all posts from your Facebook wall.", "managePostsLoading": "# I'm checking what posts are left to remove.", "checkBatchActionWallPosts": "# I'm looking for a batch of posts to **{action}**...", From f2a30047412bf646ef1e0783c2b1c6b995cb870d Mon Sep 17 00:00:00 2001 From: redshiftzero Date: Sun, 21 Jun 2026 17:42:44 -0400 Subject: [PATCH 14/22] facebook: update UI elements in `FacebookProgressComponent` --- .../components/FacebookProgressComponent.vue | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue index 415b540d..6ed1a3ec 100644 --- a/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue +++ b/src/renderer/src/views/facebook/components/FacebookProgressComponent.vue @@ -1,8 +1,8 @@