Skip to content

Instantly share code, notes, and snippets.

@qm3ster
Last active March 25, 2024 20:02
Show Gist options
  • Save qm3ster/435674e5ede13e9e4f2897eaa7860f4e to your computer and use it in GitHub Desktop.
Save qm3ster/435674e5ede13e9e4f2897eaa7860f4e to your computer and use it in GitHub Desktop.
Delete all members from a Facebook group
(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