抽奖动态删除&取关

删除所有抽奖动态并自动取关

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         抽奖动态删除&取关
// @namespace    mscststs
// @version      0.22
// @description  删除所有抽奖动态并自动取关
// @author       mscststs
// @match        https://space.bilibili.com/*
// @match        http://space.bilibili.com/*
// @require https://greasyfork.org/scripts/38220-mscststs-tools/code/MSCSTSTS-TOOLS.js?version=713767
// @require      https://cdn.jsdelivr.net/npm/axios@1.7.3/dist/axios.min.js
// @icon         https://static.hdslb.com/images/favicon.ico
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const uid = window.location.pathname.split("/")[1];

    function getUserCSRF() {
        return document.cookie.split("; ").find(row => row.startsWith("bili_jct="))?.split("=")[1];
    }

    const csrfToken = getUserCSRF();

    class Api {
        constructor() { }

        async getFollowers() { // 获取粉丝列表
            return this.fetchJsonp(`https://api.bilibili.com/x/relation/followers?jsonp=jsonp&vmid=${window.BilibiliLive.UID}`);
        }

        async spaceHistory(offset = 0) { // 获取个人动态
            return this.retryOn429(() => this._api(
                `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history?visitor_uid=${uid}&host_uid=${uid}&offset_dynamic_id=${offset}`,
                {}, "get"
            ));
        }

        async removeDynamic(id) { // 删除动态
            return this._api(
                "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/rm_dynamic",
                { dynamic_id: id, csrf_token: csrfToken }
            );
        }

        async unfollowUser(id) { // 取关
            return this._api(
                "https://api.live.bilibili.com/relation/v1/Feed/SetUserFollow",
                {
                    uid: uid,
                    type: 0,
                    follow: id,
                    re_src: 18,
                    csrf_token: csrfToken,
                    csrf: csrfToken,
                    visit_id: "",
                }
            );
        }

        async _api(url, data, method = "post") { // 通用请求
            return axios({
                url,
                method,
                data: this.transformRequest(data),
                withCredentials: true,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }).then(res => res.data);
        }

        transformRequest(data) { // 转换请求参数
            return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
        }

        async fetchJsonp(url) { // jsonp请求
            return fetchJsonp(url).then(res => res.json());
        }

        async retryOn429(func, retries = 5, delay = 100) { // 出现429错误时冷却100ms重试,出现412错误时提示并退出
            while (retries > 0) {
                try {
                    return await func();
                } catch (err) {
                    if (err.response && err.response.status === 429) {
                        await this.sleep(delay);
                        retries--;
                    } else if (err.response && err.response.status === 412) {
                        alert('由于请求过于频繁,IP暂时被ban,请更换IP或稍后再试。');
                        throw new Error('IP blocked, please retry later.');
                    } else {
                        throw err;
                    }
                }
            }
            throw new Error('Too many retries, request failed.');
        }

        sleep(ms) { // 睡眠
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    }

    const api = new Api();
    const buttons = [".onlyDeleteAll", ".deleteAll", ".onlyDeleteRepost", ".deleteRepost", ".unfollowAll"];
    let logNode;

    async function init() {
        const shijiao = await mscststs.wait(".h-version-state", true, 100);
        if (!shijiao || shijiao.innerText != "我自己") {
            console.log('当前不是自己的个人动态');
            return;
        }

        await Promise.all([
            mscststs.wait("#page-dynamic"),
            mscststs.wait("#page-dynamic .col-2")
        ]);

        const node = createControlPanel();
        document.querySelector("#page-dynamic .col-2").append(node);
        logNode = document.querySelector(".msc_panel .log");

        setEventListeners();
    }

    function createControlPanel() {
        const node = document.createElement("div");
        node.className = "msc_panel";
        node.innerHTML = `
            <div class="inner">
                <button class="onlyDeleteAll">删除所有抽奖动态但是不取关</button><br>
                <button class="onlyDeleteRepost">删除所有转发动态但是不取关</button><br>
                <button class="deleteAll">删除所有抽奖动态并取关</button><br>
                <button class="deleteRepost">删除所有转发动态并取关</button><br>
                <button class="unfollowAll">取关所有</button>
                <div class="log"></div>
            </div>`;
        return node;
    }

    function setEventListeners() {
        document.querySelector(".onlyDeleteAll").addEventListener("click", handleDelete.bind(null, true, false));
        document.querySelector(".onlyDeleteRepost").addEventListener("click", handleDelete.bind(null, false, false));
        document.querySelector(".deleteAll").addEventListener("click", handleDelete.bind(null, true, true));
        document.querySelector(".deleteRepost").addEventListener("click", handleDelete.bind(null, false, true));
        document.querySelector(".unfollowAll").addEventListener("click", unfollowAll);
    }

    async function handleDelete(deleteLottery, unfollow) { // 参数含义:是否仅删除抽奖,是否取关
        disableAll();
        let deleteCount = 0; // 删除计数
        let unfollowCount = 0; // 取关计数
        let hasMore = true; // 是否还有更多动态
        let offset = 0; // 动态偏移量
        let unfollowList = {}; // 已取关列表

        while (hasMore) {
            const { data } = await api.spaceHistory(offset);
            hasMore = data.has_more;

            for (const card of data.cards) {
                offset = card.desc.dynamic_id_str;

                if (card.desc.orig_dy_id != 0) { // 如果是转发动态
                    try {
                        const content = JSON.parse(card.card);
                        const content2 = JSON.parse(content.origin_extend_json);

                        if (!deleteLottery || content2.lott) { // 如果“仅删除抽奖”为假,或判断为抽奖动态
                            const rm = await api.removeDynamic(card.desc.dynamic_id_str);
                            if (rm.code === 0) deleteCount++;
                            else throw new Error("删除出错");

                            if (unfollow && !unfollowList[content.origin_user.info.uid]) { // 如果“取关”为真,且未取关过
                                const uf = await api.unfollowUser(content.origin_user.info.uid);
                                if (uf.code === 0) {
                                    unfollowList[content.origin_user.info.uid] = 1;
                                    unfollowCount++;
                                } else throw new Error("取关出错");
                            }
                        }
                        await api.sleep(50);
                        log(`已删除 ${deleteCount} 条,取关 ${unfollowCount} 个`);
                    } catch (e) {
                        console.error(e);
                        break;
                    }
                }
            }
        }
        enableAll();
    }

    async function unfollowAll() {
        disableAll();
        const { data } = await api.getFollowers();
        let unfollowCount = 0;

        for (const follower of data.list) {
            try {
                const uf = await api.unfollowUser(follower.mid);
                if (uf.code === 0) {
                    unfollowCount++;
                } else {
                    throw new Error("取关出错");
                }
                await api.sleep(50);
                log(`已取关 ${unfollowCount} 个`);
            } catch (e) {
                console.error(e);
                break;
            }
        }
        enableAll();
    }

    function disableAll() {
        console.log('start');
        buttons.forEach(btn => document.querySelector(btn).disabled = true);
    }

    function enableAll() {
        console.log('done');
        buttons.forEach(btn => document.querySelector(btn).disabled = false);
    }

    function log(message) {
        logNode.innerText = message;
    }

    init();
})();