转跳到内容

附件配额用完了怎么删除不需要的附件


只显示该作者

只有该作者的内容显示中。 返回到主题

推荐贴

发布于

电脑端,使用tampermonkey插件,试试看把这个脚本拷贝进去,如果是你有修改权限的帖子会删除附件,如果你没修改权限的话会报404,至少我实验是有效的

// ==UserScript==
// @name         SSTM 附件页删除按钮(区分主题/回复)
// @namespace    https://example.com/
// @version      0.2.0
// @author       sstm脚本组
// @description  在 https://sstm.moe/attachments/ 页面为每个附件块插入删除按钮,并根据来源链接自动判断编辑主题还是编辑回复
// @match        https://sstm.moe/attachments/*
// @match        https://sstm.moe/attachments/
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        itemSelector: 'div.ipsDataItem.ipsAttach',
        attachmentLinkSelector: 'a[href*="/applications/core/interface/file/attachment.php?id="]',
        sourceLinkSelector: '.ipsDataItem_generic.ipsDataItem_size9 a[href*="/topic/"]',
        buttonClass: 'sstm-delete-attachment-btn',
        confirmBeforeDelete: true,
        debug: true
    };

    function log(...args) {
        if (CONFIG.debug) {
            console.log('[SSTM Attachment Delete]', ...args);
        }
    }

    function absoluteUrl(url) {
        return new URL(url, location.origin).toString();
    }

    function getCsrfKey() {
        const fromMeta = document.querySelector('meta[name="csrfKey"]')?.content;
        if (fromMeta) return fromMeta;

        const fromWindow1 = window.ips?.getSetting?.('csrfKey');
        if (fromWindow1) return fromWindow1;

        const fromWindow2 = window.ipsSettings?.csrfKey;
        if (fromWindow2) return fromWindow2;

        const m = document.cookie.match(/(?:^|;\s*)csrfKey=([^;]+)/);
        if (m) return decodeURIComponent(m[1]);

        return null;
    }

    function extractAttachmentId(item) {
        const link = item.querySelector(CONFIG.attachmentLinkSelector);
        if (!link) return null;

        try {
            const url = new URL(link.href, location.origin);
            return url.searchParams.get('id');
        } catch (e) {
            return null;
        }
    }

    function extractFileName(item) {
        const el = item.querySelector('.ipsAttach_title a, .ipsDataItem_title a');
        return el ? el.textContent.trim() : '未知附件';
    }

    function extractSourceInfo(item) {
        const sourceLink = item.querySelector(CONFIG.sourceLinkSelector);
        if (!sourceLink) return null;

        const href = absoluteUrl(sourceLink.href);
        const url = new URL(href);

        const commentId = url.searchParams.get('comment');
        const doValue = url.searchParams.get('do');

        const isReply = doValue === 'findComment' && !!commentId;

        if (isReply) {
            url.searchParams.delete('do');
            url.searchParams.delete('comment');
            return {
                type: 'reply',
                sourceUrl: href,
                baseTopicUrl: url.toString(),
                commentId
            };
        }

        url.searchParams.delete('do');
        url.searchParams.delete('comment');
        return {
            type: 'topic',
            sourceUrl: href,
            baseTopicUrl: url.toString(),
            commentId: null
        };
    }

    function buildEditUrl(sourceInfo, csrfKey) {
        const url = new URL(sourceInfo.baseTopicUrl, location.origin);

        if (sourceInfo.type === 'reply') {
            url.searchParams.set('do', 'editComment');
            url.searchParams.set('comment', sourceInfo.commentId);
        } else {
            url.searchParams.set('do', 'edit');
        }

        if (csrfKey) {
            url.searchParams.set('csrfKey', csrfKey);
        }

        return url.toString();
    }

    async function fetchText(url, options = {}) {
        const resp = await fetch(url, {
            credentials: 'include',
            cache: 'no-cache',
            ...options
        });

        if (!resp.ok) {
            throw new Error(`请求失败 ${resp.status} ${resp.statusText}`);
        }

        return await resp.text();
    }

    function parseHTML(html) {
        return new DOMParser().parseFromString(html, 'text/html');
    }

    function extractPostKeyFromEditHtml(html) {
        const doc = new DOMParser().parseFromString(html, 'text/html');

        // 1. 优先从 IPS 编辑器容器读取
        const editorEl = doc.querySelector('[data-ipseditor-postkey]');
        if (editorEl) {
            const postKey = editorEl.getAttribute('data-ipseditor-postkey');
            if (postKey) return postKey;
        }

        // 2. 兜底:兼容可能存在的 input
        const input = doc.querySelector('input[name="postKey"]');
        if (input?.value) return input.value;

        // 3. 再兜底:正则扫原始 HTML
        let m = html.match(/data-ipseditor-postkey=["']([^"']+)["']/i);
        if (m) return m[1];

        m = html.match(/name=["']postKey["'][^>]*value=["']([^"']+)["']/i);
        if (m) return m[1];

        m = html.match(/["']postKey["']\s*:\s*["']([^"']+)["']/i);
        if (m) return m[1];

        return null;
    }

    async function deleteAttachment(editUrl, attachmentId, postKey, csrfKey) {
        const url = new URL(editUrl, location.origin);
        url.searchParams.set('postKey', postKey);
        url.searchParams.set('deleteFile', attachmentId);
        if (csrfKey) {
            url.searchParams.set('csrfKey', csrfKey);
        }

        log('删除请求 URL:', url.toString());

        const resp = await fetch(url.toString(), {
            method: 'GET',
            credentials: 'include',
            mode: 'cors',
            headers: {
                'accept': '*/*',
                'cache-control': 'no-cache',
                'pragma': 'no-cache',
                'x-requested-with': 'XMLHttpRequest'
            },
            referrer: editUrl
        });

        if (!resp.ok) {
            throw new Error(`删除请求失败 ${resp.status} ${resp.statusText}`);
        }

        return await resp.text();
    }

    function setBtnState(btn, state, text) {
        btn.disabled = state === 'loading';
        btn.textContent = text;

        if (state === 'loading') {
            btn.style.opacity = '0.7';
            btn.style.cursor = 'wait';
        } else {
            btn.style.opacity = '1';
            btn.style.cursor = 'pointer';
        }
    }

    function removeItem(item) {
        item.style.transition = 'opacity .25s ease, transform .25s ease';
        item.style.opacity = '0';
        item.style.transform = 'scale(0.98)';
        setTimeout(() => item.remove(), 260);
    }

    async function handleDelete(item, btn) {
        const attachmentId = extractAttachmentId(item);
        const fileName = extractFileName(item);
        const sourceInfo = extractSourceInfo(item);
        const csrfKey = getCsrfKey();

        if (!attachmentId) {
            alert('未找到附件 ID');
            return;
        }

        if (!sourceInfo) {
            alert('未找到该附件对应的主题/回复链接');
            return;
        }

        if (!csrfKey) {
            alert('未找到 csrfKey');
            return;
        }

        const targetText = sourceInfo.type === 'reply' ? `回复 #${sourceInfo.commentId}` : '主题首楼';

        if (CONFIG.confirmBeforeDelete) {
            const ok = confirm(
                `确定删除附件吗?\n\n文件名:${fileName}\n附件ID${attachmentId}\n挂载位置:${targetText}`
            );
            if (!ok) return;
        }

        try {
            setBtnState(btn, 'loading', '读取编辑页...');
            const editUrl = buildEditUrl(sourceInfo, csrfKey);

            log('编辑页 URL:', editUrl);
            log('来源类型:', sourceInfo.type, sourceInfo);

            const editHtml = await fetchText(editUrl, {
                headers: {
                    'accept': 'text/html,application/xhtml+xml'
                }
            });

            const postKey = extractPostKeyFromEditHtml(editHtml);
            if (!postKey) {
                throw new Error('未能从编辑页中解析出 postKey');
            }

            setBtnState(btn, 'loading', '提交删除...');
            const result = await deleteAttachment(editUrl, attachmentId, postKey, csrfKey);
            log('删除响应:', result);

            setBtnState(btn, 'success', '已删除');
            removeItem(item);
        } catch (err) {
            console.error(err);
            setBtnState(btn, 'error', '删除失败');
            alert(`删除失败:${err.message}`);
            setTimeout(() => {
                setBtnState(btn, 'idle', '删除附件');
            }, 1200);
        }
    }

    function injectButton(item) {
        if (item.querySelector(`.${CONFIG.buttonClass}`)) return;

        const attachmentId = extractAttachmentId(item);
        const sourceInfo = extractSourceInfo(item);
        if (!attachmentId || !sourceInfo) return;

        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = CONFIG.buttonClass;
        btn.textContent = '删除附件';

        btn.style.marginTop = '8px';
        btn.style.padding = '6px 10px';
        btn.style.border = '1px solid #cc3333';
        btn.style.borderRadius = '4px';
        btn.style.background = '#fff5f5';
        btn.style.color = '#b30000';
        btn.style.fontSize = '12px';
        btn.style.lineHeight = '1.2';
        btn.style.cursor = 'pointer';

        btn.addEventListener('mouseenter', () => {
            if (!btn.disabled) btn.style.background = '#ffeaea';
        });

        btn.addEventListener('mouseleave', () => {
            if (!btn.disabled) btn.style.background = '#fff5f5';
        });

        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            handleDelete(item, btn);
        });

        const target = item.querySelector('.ipsDataItem_main') || item;
        target.appendChild(btn);
    }

    function processAll() {
        document.querySelectorAll(CONFIG.itemSelector).forEach(injectButton);
    }

    function observe() {
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (!(node instanceof Element)) continue;

                    if (node.matches?.(CONFIG.itemSelector)) {
                        injectButton(node);
                    }

                    node.querySelectorAll?.(CONFIG.itemSelector).forEach(injectButton);
                }
            }
        });

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

    processAll();
    observe();
})();

然后之后如果想上传图片/视频的话,可以试试看这个https://sstm.moe/topic/378868-【油猴脚本】便捷文件上传自定义表情包/?do=findComment&comment=18887760

367ddd在文学领地阅读作品时遇到了穿着女仆装的文学少女,待她离开后找到了遗落的10节操

创建帐号或登入才能点评

您必须成为用户才能点评

创建帐号

在我们社区注册个新的帐号。非常简单!

注册新帐号

登入

已有帐号? 登入

现在登入
×
×
  • 新建...

重要消息

为使您更好地使用该站点,请仔细阅读以下内容: 使用条款