Skip to content

Instantly share code, notes, and snippets.

@swayson
Created March 3, 2025 05:36
Show Gist options
  • Save swayson/6ac2100280bbdc8d1af7a3914bef8839 to your computer and use it in GitHub Desktop.
Save swayson/6ac2100280bbdc8d1af7a3914bef8839 to your computer and use it in GitHub Desktop.
Delete Teams Messages
/*
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