Twitter DM Cleaner

One-click remove all the potential harassment spams in twitter's direct messages area.

// ==UserScript==
// @name         Twitter DM Cleaner
// @homepage     https://github.com/daymade/Twitter-DM-Cleaner
// @namespace    https://greasyfork.org/users/1121182
// @version      0.7.0
// @author       daymade
// @license      MIT
// @description  One-click remove all the potential harassment spams in twitter's direct messages area.
// @description:zh-CN 在Twitter私信中识别并高亮显示可能的骚扰信息,一键批量删除这些对话。
// @match        https://x.com/*
// @match        https://x.com/messages
// @match        https://x.com/messages/*
// @match        https://x.com/messages/requests
// @match        https://x.com/messages/requests/additional
// @run-at       document-end
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    let observer = null;

    // 添加 Tampermonkey 菜单选项
    GM_registerMenuCommand("批量删除私信", batchDeleteMessages);
    GM_registerMenuCommand("批量删除骚扰私信", batchDeleteHarassmentMessages);

    // 批量删除私信:选择删除私信的数量
    async function batchDeleteMessages() {
        if (!isMessagePage()) {
            alert("请在私信列表页面使用此功能(不是私信请求)");
            return;
        }

        const conversations = document.querySelectorAll('[data-testid="conversation"]');
        const conversationCount = conversations.length;
        const confirmed = confirm(`一共有 ${conversationCount} 条私信。请你选择要删除多少条,点击"确定"提供数量,点击"取消"不会执行任何操作`);

        if (confirmed) {
            const deleteCount = getDeleteCount();
            if (deleteCount !== null) {
                await bulkDeleteMessages(deleteCount);
            }
        }
    }

    // 批量删除私信
    async function bulkDeleteMessages(deleteCount) {
        const conversations = document.querySelectorAll('[data-testid="conversation"]');
        let deletedCount = 0;
        let totalCount = Math.min(deleteCount, conversations.length);

        const progressIndicator = createProgressIndicator(totalCount);
        document.body.appendChild(progressIndicator);

        for (const conversation of conversations) {
            if (deletedCount < deleteCount) {
                try {
                    await deleteConversation(conversation);
                    deletedCount++;
                    updateProgressIndicator(progressIndicator, deletedCount, totalCount);
                } catch (error) {
                    console.error('删除对话时出错:', error);
                }
            } else {
                break;
            }
        }

        // 确保进度条显示最终状态
        updateProgressIndicator(progressIndicator, deletedCount, totalCount);

        // 延迟移除进度条和显示结果,以便用户能看到最终进度
        setTimeout(() => {
            document.body.removeChild(progressIndicator);
            alert(`已成功删除 ${deletedCount} 条私信。`);
        }, 1000); // 延迟1秒
    }

    // 批量删除骚扰私信:选择删除数量
    async function batchDeleteHarassmentMessages() {
        if (!isMessageRequestsPage()) {
            alert("请在**私信请求**的列表页使用此功能,地址栏是 /messages/requests,不是**私信**列表页");
            return;
        }

        const highlightedConversations = document.querySelectorAll('[data-testid="conversation"][data-highlighted="true"]');
        const highlightedCount = highlightedConversations.length;
        const confirmed = confirm(`检测到 ${highlightedCount} 条已标记为骚扰的消息。是否批量删除?`);

        if (confirmed) {
            const deleteCount = getDeleteCount();
            if (deleteCount !== null) {
                await bulkDeleteHarassmentMessages(deleteCount);
            }
        }
    }

    async function bulkDeleteHarassmentMessages(deleteCount) {
        const conversations = document.querySelectorAll('[data-testid="conversation"][data-highlighted="true"]');
        let deletedCount = 0;
        let totalCount = Math.min(deleteCount, conversations.length);

        const progressIndicator = createProgressIndicator(totalCount);
        document.body.appendChild(progressIndicator);

        for (const conversation of conversations) {
            if (deletedCount < deleteCount) {
                try {
                    await deleteConversation(conversation);
                    deletedCount++;
                    updateProgressIndicator(progressIndicator, deletedCount, totalCount);
                } catch (error) {
                    console.error('删除骚扰消息时出错:', error);
                }
            } else {
                break;
            }
        }

        // 确保进度条显示最终状态
        updateProgressIndicator(progressIndicator, deletedCount, totalCount);

        // 延迟移除进度条和显示结果,以便用户能看到最终进度
        setTimeout(() => {
            document.body.removeChild(progressIndicator);
            alert(`已成功删除 ${deletedCount} 条骚扰消息。`);
        }, 1000); // 延迟1秒
    }

    // 判断是否为潜在骚扰消息
    function isPotentialHarassment(screenname) {
        if (isInWhitelist(screenname)) {
            return false;
        }

        // 移除 @ 符号
        screenname = screenname.replace('@', '');

        const totalLength = screenname.length;
        const digitCount = (screenname.match(/\d/g) || []).length;
        const letterCount = (screenname.match(/[a-zA-Z]/g) || []).length;
        const specialCharCount = totalLength - digitCount - letterCount;

        // 计算各种字符的比例
        const digitRatio = digitCount / totalLength;
        const letterRatio = letterCount / totalLength;
        const specialCharRatio = specialCharCount / totalLength;

        // 检查是否存在连续的数字
        const hasConsecutiveDigits = /\d{4,}/.test(screenname);

        // 检查是否存在年份样式的数字(如2020, 2021等)
        const hasYearLikeNumber = /(?:19|20)\d{2}/.test(screenname);

        // 检查是否存在过多的大写字母
        const uppercaseRatio = (screenname.match(/[A-Z]/g) || []).length / letterCount;

        // 评分系统
        let score = 0;

        if (digitRatio > 0.3) score += 2;
        if (specialCharRatio > 0.1) score += 1;
        if (hasConsecutiveDigits) score += 2;
        if (hasYearLikeNumber) score -= 1;
        if (uppercaseRatio > 0.5) score += 1;
        if (totalLength > 15) score += 1;

        // 如果用户名中包含常见的名字,减少分数
        const commonNames = ['peter', 'lin', 'andrew', 'adams', 'ollie', 'denise', 'nahum'];
        if (commonNames.some(name => screenname.toLowerCase().includes(name))) {
            score -= 1;
        }

        return score >= 3;
    }

    // 高亮潜在骚扰消息
    function highlightHarassmentMessages() {
        const conversations = document.querySelectorAll('[data-testid="conversation"]');

        conversations.forEach(conversation => {
            const textElements = conversation.querySelectorAll('div[dir="ltr"]');
            const messageElement = conversation.querySelector('span[data-testid="tweetText"]');

            if (textElements.length >= 3) {
                const name = textElements[0].textContent.trim();
                const screenName = textElements[2].textContent.trim().replace('@', '');
                const message = messageElement?.textContent.trim() || 'non-text-message';

                // 避免重复高亮
                if (conversation.dataset.highlighted) return;

                // 判断是否为潜在骚扰消息
                const isHarassment = isPotentialHarassment(screenName);
                console.log(`User: ${name}, Screen name: ${screenName}, Message: ${message}, IsHarassment: ${isHarassment}`);

                if (isHarassment) {
                    console.log(`Highlighting conversation for user ${name} because screenname "${screenName}" is all lowercase.`);
                    conversation.style.opacity = '0.2';
                    conversation.style.backgroundColor = '#f0f0f0';
                    conversation.dataset.highlighted = 'true'; // 标记为已高亮

                    // 添加白名单按钮
                    const whitelistButton = document.createElement('button');
                    whitelistButton.textContent = "添加到白名单";
                    whitelistButton.style.marginRight = "10px";
                    whitelistButton.style.width = "106px";
                    whitelistButton.onclick = () => {
                        addToWhitelist(screenName);
                        conversation.style.opacity = '1';
                        conversation.style.backgroundColor = '';
                        conversation.dataset.highlighted = '';
                    };
                    conversation.appendChild(whitelistButton);
                }
            } else {
                console.log("Skipping conversation due to insufficient text elements or missing message element.");
            }
        });
    }

    // 删除私信
    function deleteConversation(conversation) {
        return new Promise((resolve, reject) => {
            const TIMEOUT = 10000; // 增加超时时间到 10 秒
            let optionsButton;
            const isRequestPage = isMessageRequestsPage();

            if (isRequestPage) {
                optionsButton = conversation.querySelector('button[aria-label="Options menu"], button[aria-label="选项菜单"]');
            } else {
                optionsButton = conversation.querySelector('button[aria-label="More"], button[aria-label="更多"]');
            }

            if (!optionsButton) {
                return reject(new Error('未找到选项按钮'));
            }

            const cleanup = () => {
                if (deleteButtonObserver) deleteButtonObserver.disconnect();
                if (!isRequestPage && confirmButtonObserver) confirmButtonObserver.disconnect();
                clearTimeout(timeoutId);
            };

            const timeoutId = setTimeout(() => {
                cleanup();
                reject(new Error('操作超时'));
            }, TIMEOUT);

            let deleteButtonObserver;
            let confirmButtonObserver;

            const findAndClickDeleteButton = () => {
                const deleteButtons = Array.from(document.querySelectorAll('div[role="menuitem"]'));
                const deleteButton = deleteButtons.find(item =>
                                                        item.textContent.includes('Delete conversation') ||
                                                        item.textContent.includes('删除对话')
                                                       );

                if (deleteButton) {
                    console.log('找到删除按钮,尝试点击');
                    deleteButton.click();
                    if (isRequestPage) {
                        cleanup();
                        resolve();
                    } else {
                        observeConfirmButton();
                    }
                } else {
                    console.log('未找到删除按钮,继续观察');
                }
            };

            deleteButtonObserver = new MutationObserver((mutations, observer) => {
                findAndClickDeleteButton();
            });

            const observeConfirmButton = () => {
                confirmButtonObserver = new MutationObserver((mutations, observer) => {
                    const confirmButton = document.querySelector('[data-testid="confirmationSheetConfirm"]');
                    if (confirmButton) {
                        observer.disconnect();
                        setTimeout(() => {
                            try {
                                confirmButton.click();
                                cleanup();
                                resolve();
                            } catch (error) {
                                cleanup();
                                reject(new Error('点击确认按钮时出错'));
                            }
                        }, 100);
                    }
                });

                confirmButtonObserver.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            };

            try {
                console.log('点击选项按钮');
                optionsButton.click();
                setTimeout(() => {
                    deleteButtonObserver.observe(document.body, {
                        childList: true,
                        subtree: true
                    });
                    findAndClickDeleteButton(); // 立即尝试查找并点击删除按钮
                }, 500); // 给予一些时间让菜单打开
            } catch (error) {
                cleanup();
                reject(new Error('点击选项按钮时出错'));
            }
        });
    }

    // 白名单存储
    const WHITELIST_STORAGE_KEY = 'harassmentWhitelist';

    // 初始化或获取白名单
    function getWhitelist() {
        return GM_getValue(WHITELIST_STORAGE_KEY, []);
    }

    // 添加用户到白名单
    function addToWhitelist(screenname) {
        const whitelist = getWhitelist();
        if (!whitelist.includes(screenname)) {
            whitelist.push(screenname);
            GM_setValue(WHITELIST_STORAGE_KEY, whitelist);
        }
    }

    // 判断是否在白名单中
    function isInWhitelist(screenname) {
        const whitelist = getWhitelist();
        return whitelist.includes(screenname);
    }

    // 创建进度指示器
    function createProgressIndicator(total) {
        const indicator = document.createElement('div');
        indicator.style.position = 'fixed';
        indicator.style.top = '10px';
        indicator.style.right = '10px';
        indicator.style.padding = '10px';
        indicator.style.backgroundColor = 'rgba(29, 161, 242, 0.9)'; // Twitter 蓝色
        indicator.style.color = 'white';
        indicator.style.borderRadius = '5px';
        indicator.style.zIndex = '9999';
        indicator.style.fontWeight = 'bold';
        indicator.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        indicator.textContent = `进度: 0 / ${total}`;
        return indicator;
    }

    // 更新进度指示器
    function updateProgressIndicator(indicator, current, total) {
        indicator.textContent = `进度: ${current} / ${total}`;
        const percentage = (current / total) * 100;
        indicator.style.background = `linear-gradient(to right, rgba(29, 161, 242, 0.9) ${percentage}%, rgba(29, 161, 242, 0.5) ${percentage}%)`;
    }

    // 判断当前是否在私信列表页面
    function isMessagePage() {
        // 私信列表的 url 是 `/messages`, 点开某条私信的 url 是 `/messages/114514`
        return window.location.pathname.startsWith('/messages');
    }

    // 判断当前是否在私信请求列表的页面
    function isMessageRequestsPage() {
        // 私信请求列表的 url 是 `/messages/requests`
        // 点开更多可能包含冒犯的 url 是 `/messages/requests/additional`
        return window.location.pathname.endsWith('/messages/requests') || window.location.pathname.endsWith('/messages/requests/additional') ;
    }

    // 获取用户选择的删除数量
    function getDeleteCount() {
        const deleteChoice = prompt("选择要删除的消息数量:1, 10, 或 全部", "全部");
        if (deleteChoice === "1") return 1;
        if (deleteChoice === "10") return 10;
        if (deleteChoice.toLowerCase() === "全部") return Infinity;
        alert("无效的输入,操作取消。");
        return null;
    }

    // 监听页面变化,能高亮新收到的私信
    function observePageChanges() {
        if (isMessagePage() || isMessageRequestsPage()) {
            observer = new MutationObserver((mutations) => {
                for (let mutation of mutations) {
                    if (mutation.type === 'childList') {
                        highlightHarassmentMessages();
                    }
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }

    // 初始化
    function init() {
        if (isMessagePage() || isMessageRequestsPage()) {
            highlightHarassmentMessages();
            observePageChanges();
        }
    }

    // 处理页面切换
    function handlePageChange() {
        if (observer) {
            observer.disconnect();
            observer = null;
        }
        init();
    }

    // 监听页面变化
    window.addEventListener('popstate', handlePageChange);
    window.addEventListener('pushstate', handlePageChange);
    window.addEventListener('replacestate', handlePageChange);

    // 等待页面加载完成后执行
    window.addEventListener('load', () => {
        if (isMessagePage() || isMessageRequestsPage()) {
            init();
        }
    });
})();