Last active
March 25, 2024 20:02
-
-
Save qm3ster/435674e5ede13e9e4f2897eaa7860f4e to your computer and use it in GitHub Desktop.
Delete all members from a Facebook group
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(async function (...protectedMembers) { | |
const currentUserID = new URL('file://' + JSON.parse(document.getElementById("__eqmc").innerHTML).u).searchParams.get('__user'); | |
if (!protectedMembers || !Array.isArray(protectedMembers) || protectedMembers.length <= 0 || !protectedMembers.includes(currentUserID)) throw Error(`Add your ID (${currentUserID}) to protected members or you will delete yourself`) | |
async function navigateAndRun() { | |
try { | |
const [, name, rest] = /^\/groups\/([\w\d.]+)(\/.+)?$/.exec(document.location.pathname); | |
if (rest !== "/people/members") { | |
alert("Navigating to correct page, rerun the script there"); | |
document.location.pathname = `/groups/${name}/people/members`; | |
} | |
} | |
catch { | |
alert("Please navigate to a group page"); | |
} | |
await main(); | |
} | |
const sleep = (ms, arg) => new Promise(res => setTimeout(res, ms, arg)); | |
/** | |
* @param {DocumentFragment} element | |
* @param {string} selector | |
* @returns {Element} | |
*/ | |
function getExactlyOne(element, selector) { | |
const result = getUpToOne(element, selector); | |
if (!result) throw new Error(`Facebook has changed, there are no ${selector} on the ${element}, the script is outdated`); | |
return result; | |
} | |
/** | |
* @param {DocumentFragment} element | |
* @param {string} selector | |
* @returns {Element | undefined} | |
*/ | |
function getUpToOne(element, selector) { | |
const result = element.querySelectorAll(selector); | |
if (result.length > 1) { | |
console.error("selector", selector, "on element", element, "resulted in multiple matches", ...result, result); | |
throw new Error(`Facebook has changed, there is more than one ${selector} on the ${element}, the script is outdated`); | |
}; | |
return result[0]; | |
} | |
class TimeoutError extends Error { } | |
/** | |
* @param {DocumentFragment} element | |
* @param {string} selector | |
* @returns {Promise<Element>} | |
*/ | |
const waitFor = (element, selector, timeout = 8192) => | |
new Promise((resolve, reject) => { | |
const start = Date.now(); | |
const attempt = () => { | |
try { | |
const elm = getUpToOne(element, selector); | |
if (elm) resolve(elm) | |
else if (Date.now() - start > timeout) { | |
console.error("timed out waiting for", selector, "on", element); | |
reject(new TimeoutError(`timed out waiting for ${selector} on ${element}`)); | |
} | |
else requestIdleCallback(attempt); | |
} catch (err) { reject(new Error(err)); } | |
} | |
attempt() | |
}) | |
const USER_REGEX = /^\/groups\/(\d+)\/user\/(\d+)\//; | |
const HELP_REGEX = /^\/help\//; | |
const STORIES_REGEX = /^\/stories\//; | |
async function main() { | |
while (true) { | |
const list = getExactlyOne(document, '[role=list]'); | |
const children = list.children; | |
for (const item of children) { | |
const [avatar, nameLink] = item.querySelectorAll('a[href]'); | |
const hrefs = [avatar, nameLink].map(x => new URL(x.href)); | |
const userIDs = hrefs.map(x => USER_REGEX.exec(x.pathname)?.[2]); | |
const userID = userIDs[0] ?? userIDs[1]; | |
let fullName | |
let button | |
if ( | |
(userIDs[0] && userID === userIDs[1]) | |
|| (!userIDs[0] && userIDs[1] && STORIES_REGEX.test(hrefs[0].pathname)) | |
) { | |
fullName = nameLink.textContent; | |
button = getExactlyOne(item, '[role=button]'); | |
} | |
else if (userID && HELP_REGEX.test(hrefs[1].pathname)) { | |
// This is probably an "Unavailable" member. Attempting to ban them often fails. | |
const buttons = item.querySelectorAll('[role=button]'); | |
if (buttons.length !== 2) throw new Error(`Facebook has changed, there is not exactly two [role=button]s on a unavailable member, the script is outdated`); | |
fullName = buttons[0].textContent; | |
button = buttons[1]; | |
} else { | |
item.style.backgroundColor = 'red'; | |
console.error("Unknown type of member", item, "skipping"); | |
continue; | |
} | |
if (protectedMembers.includes(userID)) { | |
continue; | |
} | |
console.info("Banning user", userID, fullName); | |
const clickable = getExactlyOne(button, '[role=none]'); | |
clickable.click(); | |
const menu = await waitFor(document, '[class=""]>[role=menu]'); | |
const items = menu.querySelectorAll('[role=menuitem]'); | |
[...items].find(x => x.textContent.contains("Ban from group")).click(); | |
for (const dialog of document.querySelectorAll('[role=dialog]:not([aria-label]):has([role=button][aria-label="OK"])')) getExactlyOne(dialog, '[role=button][aria-label="OK"]').click() | |
const dialog = await waitFor(document, '[role=dialog]:not([aria-label]):has([aria-checked]):has([aria-label="Confirm"]),[role=dialog]:not([aria-label]):has([aria-checked]):has([aria-label="Next"])'); | |
(await waitFor(dialog, '[aria-checked=false][aria-label="Delete recent activity"]')).click(); | |
const futureAccountsCheckbox = getUpToOne(dialog, '[aria-checked=false][aria-label^="Ban "][aria-label$="\'s future accounts"]'); | |
futureAccountsCheckbox?.click(); | |
await waitFor(dialog, '[aria-checked=true][aria-label="Delete recent activity"]'); | |
futureAccountsCheckbox && await waitFor(dialog, '[aria-checked=true][aria-label^="Ban "][aria-label$="\'s future accounts"]'); | |
getUpToOne(dialog, '[role=button][aria-label="Next"]:not([aria-disabled=true])')?.click(); // when this is a member of other groups you manage | |
(await waitFor(dialog, '[role=button][aria-label="Confirm"]:not([aria-disabled=true])')).click(); | |
(async () => { | |
const now = Date.now(); | |
while (!item.textContent.contains("Banned") && Date.now() - now < 2048) await sleep(256); | |
item.remove(); | |
for (const dialog of document.querySelectorAll('[role=dialog]:not([aria-label]):has([role=button][aria-label="OK"])')) getExactlyOne(dialog, '[role=button][aria-label="OK"]').click() | |
})() | |
} | |
await sleep(32); | |
} | |
} | |
await navigateAndRun() | |
})('657792817', 'Your ID Here').catch(console.error) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment