-
-
Save innateessence/43706a983b0f6cbf3b16058ada373baa to your computer and use it in GitHub Desktop.
| const SHORT_DELAY = 100; | |
| const MEDIUM_DELAY = 250; | |
| const LONG_DELAY = 500; | |
| let messageChatIdx = 0 | |
| let isRunning = false | |
| let exceptionsInARow = 0 | |
| const exceptionsInARowUntilNextChat = 40 | |
| async function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| async function __getLastMessageMoreIcon() { | |
| const elements = document.querySelectorAll('div[aria-label="More"]') | |
| const lastElement = elements[elements.length - 1] | |
| return lastElement | |
| } | |
| async function __unsendMessage() { | |
| const element = document.querySelector('div[aria-label="Remove message"]'); | |
| if (element) { | |
| await sleep(SHORT_DELAY); | |
| element.click(); | |
| await sleep(SHORT_DELAY); | |
| const removeButton = await __getRemoveButton(); | |
| await sleep(SHORT_DELAY); | |
| removeButton.click(); | |
| } | |
| } | |
| async function __findElementsByAriaLabel(label) { | |
| const elements = document.querySelectorAll(`[aria-label="${label}"]`); | |
| return Array.from(elements); | |
| } | |
| async function __findElementsByAriaLabelSubstring(substring) { | |
| const elements = document.querySelectorAll('[aria-label]'); | |
| const retval = Array.from(elements).filter(el => | |
| el.getAttribute('aria-label').includes(substring) | |
| ); | |
| return retval | |
| } | |
| async function __findElementByAriaLabel(label) { | |
| const elements = document.querySelectorAll(`[aria-label="${label}"]`); | |
| if (Array.isArray(elements) && elements.length > 0) { | |
| return elements[0] | |
| } | |
| return elements | |
| } | |
| async function __getRemoveButton() { | |
| const elems = Array.from(await __findElementsByAriaLabel("Remove")) | |
| if (elems.length === 0) { | |
| console.warn("No remove button found") | |
| return | |
| } | |
| for (let i = 0; i < elems.length; i++) { | |
| const elem = elems[i] | |
| if (elem.ariaDisabled === 'true') { | |
| continue | |
| } | |
| if (elem.textContent === "Remove") { | |
| return elem | |
| } | |
| } | |
| } | |
| async function __triggerMouseHover(element) { | |
| const mouseOverEvent = new MouseEvent('mouseover', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| }); | |
| element.dispatchEvent(mouseOverEvent); | |
| } | |
| async function __triggerMouseHoverExit(element) { | |
| const mouseOutEvent = new MouseEvent('mouseout', { | |
| bubbles: true, | |
| cancelable: true, | |
| view: window, | |
| }); | |
| element.dispatchEvent(mouseOutEvent); | |
| } | |
| async function __unsendLastMessage() { | |
| await sleep(SHORT_DELAY) | |
| const moreIcon = await __getLastMessageMoreIcon() | |
| moreIcon.click() | |
| await sleep(SHORT_DELAY) | |
| await __unsendMessage() | |
| await sleep(SHORT_DELAY) | |
| } | |
| async function deleteLastMessage() { | |
| await sleep(SHORT_DELAY) | |
| let elems = await getAllMessageElements() | |
| console.log(elems) | |
| let lastElement = elems[elems.length - 1] | |
| const lastElementText = lastElement?.textContent || "" | |
| console.log("lastElement: ", lastElement) | |
| const children = [] | |
| lastElement.childNodes.forEach((childNode) => { | |
| children.push(childNode) | |
| }) | |
| const targetElements = [lastElement].concat(...children) | |
| console.log("targetElements: ", targetElements) | |
| targetElements.forEach(async (elem) => { | |
| try { | |
| await __triggerMouseHover(elem) | |
| } catch { } | |
| }) | |
| await sleep(SHORT_DELAY) | |
| try { | |
| await __unsendLastMessage() | |
| } catch (e) { | |
| console.log("error", e) | |
| } | |
| await sleep(SHORT_DELAY) | |
| targetElements.forEach(async (elem) => { | |
| try { | |
| await __triggerMouseHoverExit(elem) | |
| } catch { } | |
| }) | |
| await sleep(MEDIUM_DELAY) | |
| elems = await getAllMessageElements() | |
| const newLastElement = elems[elems.length - 1] | |
| if (lastElement === newLastElement && lastElementText === newLastElement.textContent) { | |
| lastElement?.remove() | |
| } | |
| await sleep(SHORT_DELAY) | |
| } | |
| async function getAllMessageElements() { | |
| let conversationRoot = await __findElementsByAriaLabelSubstring("Conversation with") | |
| if (conversationRoot.length === 0) { | |
| console.warn("No conversation root found") | |
| return | |
| } | |
| conversationRoot = conversationRoot[0] | |
| const elems = conversationRoot.querySelectorAll('div[role="row"]') | |
| return Array.from(elems) | |
| } | |
| async function removeErrorMessages() { | |
| // get all DOM elements <div role="presentation"> | |
| const elems = Array.from(document.querySelectorAll('div[role="presentation"]')) | |
| elems.forEach(async (elem) => { | |
| if (elem && elem.textContent === 'Error displaying this message') { | |
| elem?.remove() | |
| await sleep(SHORT_DELAY) | |
| } | |
| }) | |
| } | |
| async function clickOnChat(chatElem) { | |
| const iterations = 5 | |
| for (let i = 0; i < iterations; i++) { | |
| chatElem = chatElem.childNodes[0] | |
| } | |
| chatElem.scrollIntoView() | |
| await sleep(SHORT_DELAY) | |
| chatElem.click() | |
| await sleep(LONG_DELAY) | |
| } | |
| async function getMessageChats() { | |
| let elem = await __findElementsByAriaLabel("Chats") | |
| const idx = elem.length - 1 | |
| elem = elem?.[idx] | |
| while (elem.childNodes.length == 2 || elem.childNodes.length == 1) { | |
| const idx = elem.childNodes.length - 1 | |
| const target = elem?.childNodes?.[idx] | |
| if (target) { | |
| elem = target | |
| } | |
| } | |
| return Array.from(elem.childNodes) | |
| } | |
| async function Main(loopForever = false) { | |
| isRunning = true | |
| console.log("Starting... to stop, issue `isRunning = false`") | |
| while (isRunning) { | |
| if (exceptionsInARow > exceptionsInARowUntilNextChat) { | |
| messageChatIdx++ | |
| try { | |
| // we probably unsent all the messages | |
| const chats = await getMessageChats() | |
| if (messageChatIdx >= chats.length) { | |
| console.log("Processed all chats!") | |
| isRunning = false | |
| return | |
| } | |
| await clickOnChat(chats[messageChatIdx]) | |
| exceptionsInARow = 0 | |
| } catch (e) { | |
| console.log(e) | |
| exceptionsInARow++ | |
| } | |
| } | |
| try { | |
| console.count("Deleting last message") | |
| await deleteLastMessage() | |
| exceptionsInARow = 0 | |
| } catch (e) { | |
| console.log(e) | |
| exceptionsInARow++ | |
| } | |
| await sleep(SHORT_DELAY) | |
| await removeErrorMessages() | |
| } | |
| if (loopForever) { | |
| await Restart(loopForever) | |
| } | |
| console.log("Done!") | |
| } | |
| async function Restart(loopForever = false) { | |
| // reset globals | |
| messageChatIdx = 0 | |
| isRunning = false | |
| exceptionsInARow = 0 | |
| const chats = await getMessageChats() | |
| await clickOnChat(chats[messageChatIdx]) // go back to first chat | |
| await Main(loopForever) // process it again | |
| } |
You may need to run this multiple times for two reasons:
-
Unsending and deleting messages can change the order of the chats. And if you read the code, you'll see I wrote this with the assumption that the index is a 1-to-1 match with the chat position. If I rewrite this to load all chats in the DOM and iterate backwards this problem should go away entirely. Running it multiple times also solves the issue, albeit, less elegantly.
-
Occasionally I find that the page just reloads itself for some unknown reason... So even if you tell it to loop infinitely, you may need to manually rerun this a few times to make sure it handles everything.
it stopped working due to changes in UI so i made a fix. hope its ok. newer version works as of 8/22/2025 (25.8.2025) on:
https://github.com/vandakdominik/Messenger-massage-unsender-removal/tree/main
No worries, feel free to do whatever you want with the code
Archive any chat's you wish to save before running this.
This will delete every message in every chat by individually unsending/removing every message.