DoubanCommentDelete

Delete your comments in group posts

// ==UserScript==
// @name         DoubanCommentDelete
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Delete your comments in group posts
// @author       HouBo
// @match        *://www.douban.com/group/topic/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license MIT
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

;(async () => {
    'use strict';

    // —— 工具函数 ——
    function getCk() {
        const match = document.cookie.match(/(?:^|; )ck=([^;]+)/);
        return match ? match[1] : '';
    }

    function logToUI(text, type = 'info') {
        const colorMap = {
            info: '#1890ff',
            success: '#52c41a',
            warn: '#faad14',
            error: '#f5222d'
        };
        const log = document.createElement('div');
        log.textContent = text;
        Object.assign(log.style, {
            fontSize: '14px',
            padding: '4px 0',
            color: colorMap[type] || '#000'
        });
        logContainer.appendChild(log);
        logContainer.scrollTop = logContainer.scrollHeight;
    }

    function createButton(text, style, handler) {
        const btn = document.createElement('a');
        btn.href = 'javascript:void(0)';
        btn.textContent = text;
        Object.assign(btn.style, {
            padding: '6px 10px',
            margin: '0 4px',
            borderRadius: '4px',
            fontSize: '14px',
            textDecoration: 'none',
            cursor: 'pointer',
            ...style
        });
        btn.addEventListener('click', handler);
        return btn;
    }

    // —— 获取或输入用户 ID ——
    async function fetchOrPromptUserId() {
        let stored = await GM_getValue('userId', null);
        if (!stored) {
            const input = prompt('请输入你的用户 ID(可在个人主页 URL 中找到):');
            if (input && input.trim()) {
                await GM_setValue('userId', input.trim());
                return input.trim();
            }
            return null;
        }
        return stored;
    }

    const userId = await fetchOrPromptUserId();
    if (!userId) {
        alert('未输入用户 ID,脚本已停止。');
        return;
    }

    GM_registerMenuCommand('修改用户 ID', async () => {
        const newId = prompt('重新输入你的用户 ID:', userId || '');
        if (newId && newId.trim()) {
            await GM_setValue('userId', newId.trim());
            location.reload();
        }
    });

    // 显示当前 ID
    const idBox = document.createElement('div');
    idBox.textContent = `当前用户 ID: ${userId}`;
    Object.assign(idBox.style, {
        position: 'fixed',
        bottom: '10px',
        left: '10px',
        padding: '8px 12px',
        background: 'rgba(0,0,0,0.7)',
        color: '#fff',
        borderRadius: '6px',
        fontSize: '14px',
        zIndex: 9999,
        fontFamily: 'monospace'
    });
    document.body.appendChild(idBox);

    // —— 初始化变量 ——
    const tid = location.href.match(/topic\/(\d+)\//)?.[1];
    if (!tid) {
        console.error('无法提取话题 ID');
        return;
    }

    const ck = getCk();
    if (!ck) {
        alert('未获取到 ck(登录凭证),请重新登录豆瓣后重试。');
        return;
    }

    const topicAdminOpts = $('.topic-admin-opts')[0];
    if (!topicAdminOpts) {
        console.error('找不到操作区域');
        return;
    }

    // —— 创建控制面板 ——
    const controlPanel = document.createElement('div');
    controlPanel.innerHTML = `
        <div style="margin: 12px 0; padding: 12px; border: 1px solid #e8e8e8; border-radius: 8px; background: #fafafa; font-family: Arial, sans-serif;">
            <h3 style="margin: 0 0 10px; color: #333;">自动删除我的评论</h3>
            <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
                <label style="font-weight:bold; color:#333;">起始页:</label>
                <input type="number" id="page-start-input" value="1" min="1" style="width: 60px; padding: 4px; border: 1px solid #ccc; border-radius: 4px;">
                <div id="progress-bar" style="flex: 1; height: 6px; background: #eee; border-radius: 3px; overflow: hidden; display: none;">
                    <div id="progress-fill" style="height: 100%; width: 0%; background: #1890ff; transition: width 0.3s;"></div>
                </div>
            </div>
            <div id="button-container" style="margin-top: 10px;"></div>
            <div id="log-container" style="max-height: 200px; overflow-y: auto; margin-top: 10px; font-size: 14px; color: #555; line-height: 1.5;"></div>
        </div>
    `;
    topicAdminOpts.appendChild(controlPanel);

    const progressBar = document.getElementById('progress-bar');
    const progressFill = document.getElementById('progress-fill');
    const buttonContainer = document.getElementById('button-container');
    const logContainer = document.getElementById('log-container');

    let isRunning = false;
    let isPaused = false;
    let totalDeleted = 0;
    let currentPage = 1;
    let totalPages = null;

    // —— 控制按钮 ——
    const startBtn = createButton('▶️ 开始删除', { background: '#1890ff', color: 'white' }, startDeletion);
    const pauseBtn = createButton('⏸️ 暂停', { background: '#faad14', color: 'white', display: 'none' }, () => { isPaused = true; pauseBtn.style.display = 'none'; resumeBtn.style.display = 'inline-block'; logToUI('⏸️ 已暂停,点击“继续”恢复。', 'warn'); });
    const resumeBtn = createButton('▶️ 继续', { background: '#52c41a', color: 'white', display: 'none' }, () => { isPaused = false; resumeBtn.style.display = 'none'; pauseBtn.style.display = 'inline-block'; });
    const stopBtn = createButton('⏹️ 停止', { background: '#f5222d', color: 'white' }, () => {
        isRunning = false;
        isPaused = false;
        logToUI(`⏹️ 已停止。共删除 ${totalDeleted} 条评论。`, 'warn');
        updateButtons(false);
    });

    buttonContainer.appendChild(startBtn);
    buttonContainer.appendChild(pauseBtn);
    buttonContainer.appendChild(resumeBtn);
    buttonContainer.appendChild(stopBtn);

    function updateButtons(running) {
        startBtn.style.display = running ? 'none' : 'inline-block';
        pauseBtn.style.display = running && !isPaused ? 'inline-block' : 'none';
        resumeBtn.style.display = running && isPaused ? 'inline-block' : 'none';
        stopBtn.style.display = running ? 'inline-block' : 'none';
    }

    // —— 延迟函数 ——
    function randomDelay(min = 800, max = 1500) {
        return new Promise(resolve => setTimeout(resolve, Math.random() * (max - min) + min));
    }

    // —— 删除单条评论(带重试)——
    async function delComment(commentElement) {
        const cid = $(commentElement).data('cid');
        const content = $(commentElement).find('.markdown').text().trim().substring(0, 25);

        for (let i = 0; i < 3; i++) {
            try {
                await $.post(`/j/group/topic/${tid}/remove_comment`, { ck, cid });
                totalDeleted++;
                logToUI(`成功删除:"${content}"`, 'success');
                return true;
            } catch (err) {
                if (i === 2) {
                    logToUI(`删除失败(重试3次):"${content}"`, 'error');
                    return false;
                }
                await randomDelay(1000, 2000);
            }
        }
    }

    // —— 删除当前页所有自己的评论 ——
    async function delPageComment() {
        const comments = $('.topic-reply li');
        const myComments = Array.from(comments).filter(el => $(el).data('author-id') == userId);

        if (myComments.length === 0) {
            logToUI(`🔍 第 ${currentPage} 页:未找到属于你的评论。`);
            return;
        }

        logToUI(`🔍 第 ${currentPage} 页:发现 ${myComments.length} 条属于你的评论,开始删除...`);

        for (const comment of myComments) {
            if (!isRunning || isPaused) break;
            await delComment(comment);
            await randomDelay();
        }
    }

    // —— 跳转下一页 ——
    async function gotoNextPage() {
        const nextLink = $('a:contains("后页")').attr('href');
        if (!nextLink) return false;

        try {
            const data = await $.ajax({ url: nextLink, method: 'GET' });
            const $newDom = $('<div>').html(data);

            $('#comments').html($newDom.find('#comments').html());
            $('#popular-comments, h3:contains("最赞回应")').remove();
            $('.paginator').html($newDom.find('.paginator').html());

            currentPage++;
            updateProgress();
            return true;
        } catch (e) {
            logToUI('❌ 加载下一页失败', 'error');
            return false;
        }
    }

    // —— 更新进度条 ——
    function updateProgress() {
        const percent = totalPages ? Math.round((currentPage / totalPages) * 100) : 0;
        progressFill.style.width = `${Math.min(percent, 100)}%`;
    }

    // —— 主删除流程 ——
    async function startDeletion() {
        const startPageInput = document.getElementById('page-start-input').value;
        const startPage = Math.max(1, parseInt(startPageInput, 10) || 1);

        isRunning = true;
        isPaused = false;
        totalDeleted = 0;
        currentPage = startPage;
        updateButtons(true);
        logContainer.innerHTML = '';
        progressBar.style.display = 'block';
        progressFill.style.width = '0%';

        logToUI(`🚀 开始从第 ${startPage} 页删除评论...`, 'info');

        try {
            await goToPage(startPage);
            updateProgress();

            while (isRunning) {
                if (isPaused) {
                    await new Promise(resolve => {
                        const check = () => isPaused && isRunning ? setTimeout(check, 500) : resolve();
                        check();
                    });
                }

                await delPageComment();
                const hasNext = await gotoNextPage();
                if (!hasNext) {
                    logToUI('已到最后一页,删除完成!', 'success');
                    break;
                }
            }
        } catch (e) {
            logToUI(`❌ 发生错误: ${e.message}`, 'error');
        } finally {
            if (isRunning) {
                logToUI(`✅ 全部完成!共删除 ${totalDeleted} 条评论。`, 'success');
            }
            isRunning = false;
            updateButtons(false);
        }
    }

    // —— 跳转到指定页 ——
    function goToPage(pageNum) {
        if (pageNum === 1) {
            $('#popular-comments, h3:contains("最赞回应")').remove();
            return Promise.resolve();
        }

        const url = `/group/topic/${tid}/?start=${(pageNum - 1) * 100}`;
        return $.ajax({
            url,
            method: 'GET',
            success: data => {
                const $newDom = $('<div>').html(data);
                $('.topic-reply').html($newDom.find('.topic-reply').html());
                $('.paginator').html($newDom.find('.paginator').html());
                currentPage = pageNum;
                updateProgress();
            },
            error: () => logToUI(`❌ 无法加载第 ${pageNum} 页`, 'error')
        });
    }

    // —— 移除广告 ——
    $('#gdt-ad-container, #dale_group_topic_inner_middle').remove();

    logToUI('✅ 脚本已就绪,请设置起始页并点击“开始删除”。');

})();