Created
March 3, 2025 05:36
-
-
Save swayson/6ac2100280bbdc8d1af7a3914bef8839 to your computer and use it in GitHub Desktop.
Delete Teams Messages
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
/* | |
Delete Teams Messages v3.0 | |
Improved version based on dsci4's script (used claude 3.7 for rewriting) | |
Original: https://github.com/dsci4-hacks | |
This works on the current Teams Web Version as of March 2025. | |
Tested on Firefox and Chrome. | |
Open Microsoft Teams web version, select a conversation, | |
Open Developer tools (F12) and copy/paste this entire script. | |
USAGE: | |
1. Paste script in console and press Enter (this only sets up the functions) | |
2. Run window.TeamsMessageDeleter.start() to begin deletion | |
3. Use .pause(), .resume(), .status() and .stop() to control the script | |
*/ | |
(function () { | |
console.log("[Automation Script] Enhanced script loaded and ready."); | |
// Set up data structures but don't automatically start | |
const processedMessages = new Set(); // Tracks all processed message IDs | |
const queue = []; | |
const retryMap = new Map(); // Tracks retries for message processing | |
let isProcessing = false; | |
let isPaused = false; | |
let isSetup = false; | |
let lastScrollTime = 0; | |
let noNewMessagesCount = 0; | |
let observer = null; | |
// Configuration constants | |
const DELETE_INTERVAL_MS = 1000; // 2000 Delay between deletions | |
const MAX_RETRIES = 3; // Maximum retries for failed messages | |
const SCROLL_THROTTLE_MS = 1000; // 1000 Minimum time between scrolls | |
const BATCH_SIZE = 5; // Number of messages to process in one batch | |
const CLEANUP_INTERVAL_MS = 60000; // Interval for memory cleanup (1 minute) | |
const MAX_PROCESSED_MESSAGES = 500; // Maximum number of message IDs to keep in memory | |
const MAX_EMPTY_SCROLLS = 5; // Maximum scrolls with no new messages before stopping | |
// Add CSS for visual feedback | |
const addStyles = () => { | |
// Check if styles are already added | |
if (document.getElementById('teams-deleter-styles')) return; | |
const style = document.createElement('style'); | |
style.id = 'teams-deleter-styles'; | |
style.textContent = ` | |
.processing-message { outline: 3px solid blue !important; } | |
.deleted-message { outline: 3px solid green !important; } | |
.failed-message { outline: 3px solid red !important; } | |
.permanently-failed-message { outline: 3px dashed red !important; } | |
`; | |
document.head.appendChild(style); | |
}; | |
// Simulate mouse events | |
const simulateMouseEvent = (eventType, element) => { | |
const event = new MouseEvent(eventType, { | |
view: window, | |
bubbles: true, | |
cancelable: true, | |
buttons: 1, | |
}); | |
element.dispatchEvent(event); | |
}; | |
// Improved scroll management with throttling | |
const scrollToLoadMessages = () => { | |
const now = Date.now(); | |
if (now - lastScrollTime < SCROLL_THROTTLE_MS) { | |
return true; // Throttle scrolling | |
} | |
lastScrollTime = now; | |
const scrollContainer = document.querySelector("div.fui-Primitive.f1yrx710.f1l02sjl") || | |
document.querySelector(".ts-message-list-container"); | |
if (scrollContainer) { | |
// Check if we're already at the top | |
if (scrollContainer.scrollTop <= 10) { | |
console.log("[Automation Script] Reached the top of conversation history"); | |
return false; | |
} | |
scrollContainer.scrollBy({ top: -500, behavior: "auto" }); | |
console.log("[Automation Script] Scrolled up to load more messages"); | |
return true; | |
} | |
return false; | |
}; | |
// Scan and scroll logic | |
const scanAndScroll = () => { | |
const initialMessageCount = processedMessages.size; | |
scanMessages(); | |
if (queue.length === 0) { | |
// If scanning didn't find new messages | |
if (processedMessages.size === initialMessageCount) { | |
noNewMessagesCount++; | |
console.log(`[Automation Script] No new messages found (${noNewMessagesCount}/${MAX_EMPTY_SCROLLS})`); | |
if (noNewMessagesCount >= MAX_EMPTY_SCROLLS) { | |
console.log("[Automation Script] Reached message history limit or no more messages to process"); | |
return false; | |
} | |
} else { | |
// Reset counter if we found messages | |
noNewMessagesCount = 0; | |
} | |
return scrollToLoadMessages(); | |
} else { | |
// Reset counter if we have messages in queue | |
noNewMessagesCount = 0; | |
return true; | |
} | |
}; | |
// Process a single message (improved) | |
const processMessage = async (messageElement) => { | |
if (!messageElement) return; | |
const messageId = messageElement.getAttribute("data-mid"); | |
// Skip already-processed messages | |
if (processedMessages.has(messageId)) { | |
return; | |
} | |
try { | |
// Use class instead of style for visual feedback | |
messageElement.classList.add("processing-message"); | |
messageElement.classList.remove("failed-message", "deleted-message", "permanently-failed-message"); | |
// More efficient check for my messages | |
const isMine = messageElement.closest(".ChatMyMessage") !== null || | |
Array.from(messageElement.classList).some(cls => cls.includes("ChatMyMessage")); | |
if (!isMine) { | |
processedMessages.add(messageId); | |
messageElement.classList.remove("processing-message"); | |
return; | |
} | |
// Check if already deleted | |
const undoButton = messageElement.querySelector("[data-tid=message-undo-delete-btn]"); | |
if (undoButton) { | |
console.log(`[Automation Script] Message ID: ${messageId} already deleted (Undo button detected).`); | |
processedMessages.add(messageId); | |
messageElement.classList.remove("processing-message"); | |
return; | |
} | |
// Find message content for context menu | |
const messageContentElement = document.querySelector(`#content-${messageId}`) || | |
messageElement.querySelector("p") || | |
messageElement; | |
if (messageContentElement) { | |
simulateMouseEvent("contextmenu", messageContentElement); | |
const deleteOption = await new Promise((resolve, reject) => { | |
const interval = setInterval(() => { | |
const options = Array.from(document.querySelectorAll("div, span")); | |
const option = options.find(el => | |
el.textContent && el.textContent.trim().toLowerCase() === "delete" | |
); | |
if (option) { | |
clearInterval(interval); | |
resolve(option); | |
} | |
}, 100); | |
setTimeout(() => { | |
clearInterval(interval); | |
reject(new Error("Delete option not found.")); | |
}, 5000); | |
}); | |
simulateMouseEvent("click", deleteOption); | |
console.log(`[Automation Script] Deleted message ID: ${messageId}`); | |
processedMessages.add(messageId); | |
// Visual feedback for success | |
messageElement.classList.remove("processing-message"); | |
messageElement.classList.add("deleted-message"); | |
} | |
} catch (error) { | |
console.error(`[Automation Script] Error processing message ID: ${messageId}`, error); | |
// Retry logic | |
const retries = retryMap.get(messageId) || 0; | |
if (retries < MAX_RETRIES) { | |
console.log(`[Automation Script] Retrying message ID: ${messageId} (Attempt ${retries + 1}/${MAX_RETRIES})`); | |
retryMap.set(messageId, retries + 1); | |
queue.push(messageElement); | |
// Visual feedback for retry | |
messageElement.classList.remove("processing-message"); | |
messageElement.classList.add("failed-message"); | |
} else { | |
console.warn(`[Automation Script] Skipping message ID: ${messageId} after ${MAX_RETRIES} retries.`); | |
retryMap.delete(messageId); | |
processedMessages.add(messageId); | |
// Visual feedback for permanent failure | |
messageElement.classList.remove("processing-message", "failed-message"); | |
messageElement.classList.add("permanently-failed-message"); | |
} | |
} | |
await new Promise((resolve) => setTimeout(resolve, DELETE_INTERVAL_MS)); | |
}; | |
// Process messages in batches | |
const processBatch = async () => { | |
if (isProcessing || isPaused) return; | |
isProcessing = true; | |
try { | |
const batch = queue.splice(0, BATCH_SIZE); | |
if (batch.length === 0) { | |
if (scanAndScroll()) { | |
// If we successfully scanned or scrolled, continue | |
return; | |
} | |
return; | |
} | |
console.log(`[Automation Script] Processing batch of ${batch.length} messages`); | |
// Process batch sequentially | |
for (const message of batch) { | |
if (isPaused) break; | |
await processMessage(message); | |
} | |
} finally { | |
isProcessing = false; | |
// Schedule next batch if there are more messages and not paused | |
if (queue.length > 0 && !isPaused) { | |
setTimeout(processBatch, 500); | |
} else if (!isPaused) { | |
console.log("[Automation Script] Batch complete. Scanning for more..."); | |
scanAndScroll(); | |
if (queue.length > 0) { | |
setTimeout(processBatch, 500); | |
} | |
} | |
} | |
}; | |
// Efficient message scanning | |
const scanMessages = () => { | |
// Find the messages container (handling different possible selectors) | |
const messageContainer = document.querySelector("div.fui-Primitive.f1yrx710.f1l02sjl") || | |
document.querySelector(".ts-message-list-container"); | |
if (!messageContainer) return; | |
const messagesElements = messageContainer.querySelectorAll("div[data-mid]"); | |
if (!messagesElements.length) { | |
console.log("[Automation Script] No messages found with data-mid attribute"); | |
return; | |
} | |
const newMessages = Array.from(messagesElements) | |
.reverse() // Process oldest messages first (at the top) | |
.filter(msg => { | |
const messageId = msg.getAttribute("data-mid"); | |
return !queue.some(item => item.getAttribute && item.getAttribute("data-mid") === messageId) && | |
!processedMessages.has(messageId); | |
}); | |
if (newMessages.length > 0) { | |
console.log(`[Automation Script] Found ${newMessages.length} new messages.`); | |
queue.push(...newMessages); | |
} | |
}; | |
// Memory management | |
const setupMemoryManagement = () => { | |
// Clean up any existing interval | |
if (window.TeamsMessageDeleter && window.TeamsMessageDeleter.cleanupInterval) { | |
clearInterval(window.TeamsMessageDeleter.cleanupInterval); | |
} | |
const cleanupInterval = setInterval(() => { | |
if (processedMessages.size > MAX_PROCESSED_MESSAGES) { | |
console.log(`[Automation Script] Memory cleanup: Reducing processed messages from ${processedMessages.size} to ${MAX_PROCESSED_MESSAGES/2}`); | |
// Convert to array, keep only the most recent half | |
const messagesArray = Array.from(processedMessages); | |
const keepMessages = messagesArray.slice(messagesArray.length - (MAX_PROCESSED_MESSAGES/2)); | |
// Recreate the set with fewer items | |
processedMessages.clear(); | |
keepMessages.forEach(id => processedMessages.add(id)); | |
console.log(`[Automation Script] Memory cleanup complete. New size: ${processedMessages.size}`); | |
} | |
// Also clean up the retry map | |
if (retryMap.size > 0) { | |
console.log(`[Automation Script] Cleaning up retry map (${retryMap.size} entries)`); | |
retryMap.clear(); | |
} | |
}, CLEANUP_INTERVAL_MS); | |
if (window.TeamsMessageDeleter) { | |
window.TeamsMessageDeleter.cleanupInterval = cleanupInterval; | |
} | |
}; | |
// Setup the observer | |
const setupObserver = () => { | |
// Remove any existing observer | |
if (observer) { | |
observer.disconnect(); | |
} | |
observer = new MutationObserver(() => { | |
if (!isPaused && queue.length < BATCH_SIZE) { | |
scanMessages(); | |
if (!isProcessing) processBatch(); | |
} | |
}); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
return observer; | |
}; | |
// Start the script - now separated as its own function | |
const startScript = () => { | |
if (isSetup) { | |
console.log("[Automation Script] Already running. Use .stop() first if you want to restart."); | |
return; | |
} | |
console.log("[Automation Script] Starting message deletion..."); | |
isSetup = true; | |
isPaused = false; | |
noNewMessagesCount = 0; | |
// Reset counters if restarting | |
if (processedMessages.size > 0 || queue.length > 0) { | |
console.log("[Automation Script] Resetting previous state"); | |
} | |
addStyles(); | |
setupMemoryManagement(); | |
const newObserver = setupObserver(); | |
if (window.TeamsMessageDeleter) { | |
window.TeamsMessageDeleter.observer = newObserver; | |
} | |
scanMessages(); | |
processBatch(); | |
console.log(`[Automation Script] Started successfully. | |
- Use window.TeamsMessageDeleter.pause() to pause | |
- Use window.TeamsMessageDeleter.resume() to resume | |
- Use window.TeamsMessageDeleter.status() to check status | |
- Use window.TeamsMessageDeleter.stop() to stop completely`); | |
}; | |
// Reset everything for a clean restart | |
const resetScript = () => { | |
// Stop observer | |
if (observer) { | |
observer.disconnect(); | |
observer = null; | |
} | |
// Clear cleanup interval | |
if (window.TeamsMessageDeleter && window.TeamsMessageDeleter.cleanupInterval) { | |
clearInterval(window.TeamsMessageDeleter.cleanupInterval); | |
} | |
// Reset all state | |
queue.length = 0; | |
processedMessages.clear(); | |
retryMap.clear(); | |
isProcessing = false; | |
isPaused = true; | |
isSetup = false; | |
noNewMessagesCount = 0; | |
console.log("[Automation Script] Reset complete. Ready for a fresh start."); | |
}; | |
// Control methods exposed to global scope | |
window.TeamsMessageDeleter = { | |
start: () => { | |
startScript(); | |
}, | |
pause: () => { | |
isPaused = true; | |
console.log("[Automation Script] Paused. Call window.TeamsMessageDeleter.resume() to continue"); | |
}, | |
resume: () => { | |
if (!isSetup) { | |
console.log("[Automation Script] Script hasn't been started yet. Use .start() first."); | |
return; | |
} | |
if (isPaused) { | |
isPaused = false; | |
console.log("[Automation Script] Resumed"); | |
processBatch(); | |
} else { | |
console.log("[Automation Script] Already running"); | |
} | |
}, | |
status: () => { | |
console.log(`[Automation Script] Status: | |
- Running: ${isSetup ? (isPaused ? "Paused" : "Active") : "Not started"} | |
- Queue length: ${queue.length} | |
- Processed messages: ${processedMessages.size} | |
- Currently processing: ${isProcessing} | |
- Failed scroll attempts: ${noNewMessagesCount}/${MAX_EMPTY_SCROLLS}`); | |
return { | |
running: isSetup, | |
paused: isPaused, | |
queueLength: queue.length, | |
processedCount: processedMessages.size, | |
isProcessing: isProcessing, | |
scrollAttempts: noNewMessagesCount | |
}; | |
}, | |
stop: () => { | |
// Stop the script completely | |
if (observer) { | |
observer.disconnect(); | |
console.log("[Automation Script] Stopped and disconnected observer"); | |
} | |
if (window.TeamsMessageDeleter.cleanupInterval) { | |
clearInterval(window.TeamsMessageDeleter.cleanupInterval); | |
} | |
isPaused = true; | |
isSetup = false; | |
console.log("[Automation Script] Stopped completely"); | |
}, | |
reset: () => { | |
resetScript(); | |
console.log("[Automation Script] Reset complete. Use .start() to begin fresh."); | |
}, | |
// Add configuration option | |
configure: (options = {}) => { | |
// Allow configuration of parameters | |
if (options.deleteInterval !== undefined) DELETE_INTERVAL_MS = options.deleteInterval; | |
if (options.maxRetries !== undefined) MAX_RETRIES = options.maxRetries; | |
if (options.batchSize !== undefined) BATCH_SIZE = options.batchSize; | |
if (options.maxEmptyScrolls !== undefined) MAX_EMPTY_SCROLLS = options.maxEmptyScrolls; | |
console.log("[Automation Script] Configuration updated:", { | |
deleteInterval: DELETE_INTERVAL_MS, | |
maxRetries: MAX_RETRIES, | |
batchSize: BATCH_SIZE, | |
maxEmptyScrolls: MAX_EMPTY_SCROLLS | |
}); | |
} | |
}; | |
console.log(`[Automation Script] Ready to use. | |
- Type window.TeamsMessageDeleter.start() to begin deletion | |
- Use .pause(), .resume(), .status(), .stop() to control script | |
- Use .configure({...}) to adjust settings | |
- Use .reset() to clear all state and restart fresh`); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment