Created
August 25, 2026 14:39
-
-
Save raelga/18cce3b479be63ccdf55103fd68720ee to your computer and use it in GitHub Desktop.
Archive LinkedIn conversations whose latest activity is older than one month
This file contains hidden or 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 () => { | |
| const DELAY_MS = 1200; | |
| const ACTION_TIMEOUT_MS = 5000; | |
| const cutoff = new Date(); | |
| cutoff.setHours(0, 0, 0, 0); | |
| cutoff.setMonth(cutoff.getMonth() - 1); | |
| if ( | |
| !location.hostname.endsWith("linkedin.com") || | |
| !location.pathname.startsWith("/messaging") | |
| ) { | |
| throw new Error("Open https://www.linkedin.com/messaging/ first."); | |
| } | |
| if (!confirm( | |
| `Archive conversations last active before ${cutoff.toLocaleDateString()}?` | |
| )) return; | |
| const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); | |
| async function waitFor(predicate, timeoutMs) { | |
| const deadline = Date.now() + timeoutMs; | |
| while (Date.now() < deadline) { | |
| if (predicate()) return true; | |
| await sleep(100); | |
| } | |
| return false; | |
| } | |
| function isVisible(element) { | |
| if (!element) return false; | |
| const style = getComputedStyle(element); | |
| const rect = element.getBoundingClientRect(); | |
| return ( | |
| style.display !== "none" && | |
| style.visibility !== "hidden" && | |
| rect.width > 0 && | |
| rect.height > 0 | |
| ); | |
| } | |
| const list = document.querySelector( | |
| ".msg-conversations-container__conversations-list, " + | |
| '[class*="conversations-list"]' | |
| ); | |
| if (!list) { | |
| throw new Error("Conversation list not found. LinkedIn changed its UI."); | |
| } | |
| function findScroller(element) { | |
| for (let node = element; node; node = node.parentElement) { | |
| const style = getComputedStyle(node); | |
| if ( | |
| /(auto|scroll)/.test(style.overflowY) && | |
| node.scrollHeight > node.clientHeight | |
| ) return node; | |
| } | |
| throw new Error("Scrollable conversation container not found."); | |
| } | |
| function parseDate(value) { | |
| if (!value) return null; | |
| const text = value.trim(); | |
| const now = new Date(); | |
| if (/^today$/i.test(text)) return now; | |
| if (/^yesterday$/i.test(text)) { | |
| return new Date(now.getTime() - 86400000); | |
| } | |
| const relative = text.match(/^(\d+)\s*(min|m|h|d|w|mo|y)$/i); | |
| if (relative) { | |
| const amount = Number(relative[1]); | |
| const unit = relative[2].toLowerCase(); | |
| const milliseconds = { | |
| min: 60000, | |
| m: 60000, | |
| h: 3600000, | |
| d: 86400000, | |
| w: 604800000, | |
| mo: 2629800000, | |
| y: 31557600000 | |
| }[unit]; | |
| return new Date(now.getTime() - amount * milliseconds); | |
| } | |
| if (/^\d{4}-\d{2}-\d{2}/.test(text) || /\b\d{4}\b/.test(text)) { | |
| const parsed = new Date(text); | |
| if (!Number.isNaN(parsed.getTime())) return parsed; | |
| } | |
| const months = { | |
| jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, | |
| jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 | |
| }; | |
| const shortDate = text.match(/^([a-z]{3,9})\s+(\d{1,2})$/i); | |
| if (!shortDate) return null; | |
| const month = months[shortDate[1].slice(0, 3).toLowerCase()]; | |
| if (month === undefined) return null; | |
| const result = new Date(now.getFullYear(), month, Number(shortDate[2])); | |
| if (result > now) result.setFullYear(result.getFullYear() - 1); | |
| return result; | |
| } | |
| function rowDate(row) { | |
| const time = row.querySelector("time"); | |
| if (!time) return null; | |
| for (const value of [ | |
| time.getAttribute("datetime"), | |
| time.getAttribute("title"), | |
| time.getAttribute("aria-label"), | |
| time.textContent | |
| ]) { | |
| const parsed = parseDate(value); | |
| if (parsed) return parsed; | |
| } | |
| return null; | |
| } | |
| function rowName(row) { | |
| return row.querySelector( | |
| ".msg-conversation-listitem__participant-names, " + | |
| '[class*="participant-names"]' | |
| )?.textContent.trim() || "Unnamed conversation"; | |
| } | |
| function rowKey(row) { | |
| const date = row.querySelector("time")?.textContent.trim() || ""; | |
| const snippet = row.querySelector( | |
| ".msg-conversation-card__message-snippet" | |
| )?.textContent.trim() || ""; | |
| return `${rowName(row)}|${date}|${snippet}`; | |
| } | |
| function conversationRows() { | |
| const cards = [...list.querySelectorAll(".msg-conversation-card")]; | |
| if (cards.length) return cards.filter(row => row.querySelector("time")); | |
| return [...list.querySelectorAll("li")].filter(row => row.querySelector("time")); | |
| } | |
| function menuButton(row) { | |
| return row.querySelector( | |
| "button.msg-thread-actions__control, " + | |
| "button.msg-conversation-card__dropdown-trigger, " + | |
| "button.artdeco-dropdown__trigger" | |
| ) || [...row.querySelectorAll("button")].find(button => | |
| /open the options list/i.test(button.textContent) | |
| ); | |
| } | |
| function visibleArchiveAction() { | |
| const openDropdown = | |
| ":is(.msg-thread-actions__dropdown-options, " + | |
| ".msg-thread-actions__dropdown-options--inbox-shortcuts)" + | |
| ".artdeco-dropdown__content--is-open[aria-hidden='false']"; | |
| return [...document.querySelectorAll( | |
| `${openDropdown} ` + | |
| "[data-view-name='message-toolbar-dropdown-toggle-archive']" | |
| )].find(isVisible) || [...document.querySelectorAll( | |
| `${openDropdown} [role="button"], ` + | |
| `${openDropdown} button, ` + | |
| `${openDropdown} [role="menuitem"]` | |
| )].find(element => | |
| isVisible(element) && | |
| /^archive(?: conversation)?$/i.test(element.textContent.trim()) | |
| ); | |
| } | |
| const scroller = findScroller(list); | |
| const seen = new Set(); | |
| let archived = 0; | |
| let idleRounds = 0; | |
| console.log( | |
| "[Started]", | |
| `Archiving conversations older than ${cutoff.toLocaleDateString()}.` | |
| ); | |
| scroller.scrollTop = 0; | |
| await sleep(500); | |
| while (idleRounds < 6) { | |
| let discovered = false; | |
| let archivedOne = false; | |
| for (const row of conversationRows()) { | |
| const key = rowKey(row); | |
| if (seen.has(key)) continue; | |
| seen.add(key); | |
| discovered = true; | |
| const date = rowDate(row); | |
| if (!date) { | |
| console.warn("Could not parse conversation date:", rowName(row), row); | |
| continue; | |
| } | |
| console.log("[Scanned]", rowName(row), date.toLocaleDateString()); | |
| if (date >= cutoff) continue; | |
| const name = rowName(row); | |
| row.scrollIntoView({ block: "center" }); | |
| row.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); | |
| await sleep(150); | |
| const menu = menuButton(row); | |
| if (!menu) { | |
| throw new Error(`Menu button not found for "${name}".`); | |
| } | |
| menu.click(); | |
| const menuOpened = await waitFor( | |
| () => menu.getAttribute("aria-expanded") === "true" || | |
| Boolean(visibleArchiveAction()), | |
| 2000 | |
| ); | |
| if (!menuOpened) { | |
| throw new Error(`Options menu did not open for "${name}".`); | |
| } | |
| const archiveAction = visibleArchiveAction(); | |
| if (!archiveAction) { | |
| const visibleOptions = [...document.querySelectorAll( | |
| ".artdeco-dropdown__content" | |
| )] | |
| .filter(isVisible) | |
| .map(element => element.textContent.trim()) | |
| .filter(Boolean) | |
| .join(" | "); | |
| throw new Error( | |
| `Archive action not found for "${name}". Visible options: ${visibleOptions}` | |
| ); | |
| } | |
| console.log("[Archiving]", name, date.toLocaleDateString()); | |
| archiveAction.click(); | |
| const actionAccepted = await waitFor( | |
| () => | |
| !row.isConnected || | |
| !list.contains(row) || | |
| menu.getAttribute("aria-expanded") !== "true" || | |
| !isVisible(archiveAction), | |
| ACTION_TIMEOUT_MS | |
| ); | |
| if (!actionAccepted) { | |
| throw new Error( | |
| `LinkedIn did not accept the Archive click for "${name}".` | |
| ); | |
| } | |
| archived++; | |
| archivedOne = true; | |
| console.log("[Archive submitted]", name); | |
| await sleep(DELAY_MS); | |
| break; | |
| } | |
| if (archivedOne) { | |
| idleRounds = 0; | |
| continue; | |
| } | |
| const previousTop = scroller.scrollTop; | |
| scroller.scrollBy({ top: scroller.clientHeight * 0.8 }); | |
| await sleep(700); | |
| if (!discovered && scroller.scrollTop === previousTop) idleRounds++; | |
| else idleRounds = 0; | |
| } | |
| console.log(`Complete: ${archived} conversations submitted for archiving.`); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment