// ==UserScript== // @name Bilibili删除抽奖动态 // @namespace https://github.com/AliubYiero/TamperMonkeyScripts // @version 1.0.2 // @description Bilibili删除抽奖动态并取消关注对应UP主 // @author Yiero // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_cookie // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @connect api.bilibili.com // @crontab * * once * * // ==/UserScript== /* ==UserConfig== 配置项: unFollow: title: "取关默认分组中的抽奖用户" description: "开启" type: checkbox default: true ==/UserConfig== */ /** * 取关配置 */ class UnFollowStorage { static key = '配置项.unFollow'; static get() { return GM_getValue(this.key, true); } static set(unFollowStat) { GM_setValue(this.key, unFollowStat); } } /* * 通用API封装模块 * */ class CommonAPI { constructor() { } /** * 获取 cookie 内容 * @param {string} domain 域名 * @param {string} key cookie 键名 * @returns {Promise} cookie 值 */ getCookie = async (domain, key) => { return new Promise((resolve) => { // @ts-ignore 忽略未被查找到的全局函数 GM_cookie GM_cookie('list', { domain, }, (cookieList) => { const userIdCookie = cookieList.find(cookie => cookie.name === key); resolve(userIdCookie?.value || ''); }); }); }; /** * 网络请求 * @param config */ fetch(config) { let { url, param, method = 'GET', } = config; /* * 处理url参数 * */ const urlParam = new URLSearchParams(param).toString(); if (urlParam) { url = `${url}?${urlParam}`; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, onload(response) { try { const res = JSON.parse(response.response); // 每个请求延时500ms, 防止请求过于频繁被ban setTimeout(() => { resolve(res); }, 600); } catch (e) { reject(e); } }, }); }); } /** * 桌面通知 */ notify(title, text, clickCallback) { console.log('[notify] %s\n%s', title, text); GM_notification({ title: title, text: text, onclick() { clickCallback && clickCallback(); }, }); } } /* * 获取用户信息模块 * */ class UserInfoGetter extends CommonAPI { constructor() { super(); } /** * 获取csrf * */ getCsrf() { return this.getCookie('.bilibili.com', 'bili_jct'); } /** * 获取用户UID */ getUid() { return this.getCookie('.bilibili.com', 'DedeUserID'); } } /** * 获取动态列表模块 */ class DynamicGetter extends CommonAPI { dynamicList = []; constructor() { super(); } /** * 获取全部动态 */ async getAllDynamic(uid) { /* * 获取第一页动态 * */ let res; try { res = await this.api_GetDynamic(uid); } catch (e) { console.error('获取动态失败: ', e); return this.dynamicList; } /* * 如果内存中存在动态列表, 更新第一页的动态就返回, 不继续请求 * */ if (this.dynamicList.length) { // 获取最新的动态索引 const latestDynamicIndex = res.items.findIndex(item => item.id_str === this.dynamicList[0].id_str); if (latestDynamicIndex > 0) { this.dynamicList.splice(0, 0, ...res.items.slice(0, latestDynamicIndex)); } return this.dynamicList; } /* * 加载后续动态 * */ let offset; this.dynamicList.push(...res.items); while (res.has_more) { offset = res.offset; try { res = await this.api_GetDynamic(uid, offset); } catch (e) { console.error('获取动态失败: ', e); break; } this.dynamicList.push(...res.items); } return this.dynamicList; } /** * 获取全部转发动态 * */ async getAllForwardDynamic(uid) { const dynamicList = await this.getAllDynamic(uid); return dynamicList.filter(item => item.type === 'DYNAMIC_TYPE_FORWARD'); } /** * 获取全部抽奖动态 */ async getAllLotteryDynamic(uid) { const forwardDynamicList = await this.getAllForwardDynamic(uid); return forwardDynamicList.filter(item => { try { const richTextNodes = item.orig?.modules.module_dynamic.desc.rich_text_nodes; if (!richTextNodes) { return false; } return richTextNodes.find(node => node.type === 'RICH_TEXT_NODE_TYPE_LOTTERY'); } catch (e) { return false; } }); } /** * 获取当前页动态 */ async api_GetDynamic(uid, offset) { /* * 参数归一 * */ let urlParam = { host_mid: uid, }; urlParam = offset ? { offset, ...urlParam } : { ...urlParam }; /* * 请求动态 * */ const res = await this.fetch({ url: 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space', method: 'GET', param: urlParam, }); if (res.code !== 0) { throw new Error(`请求失败. \n[${res.code}] ${res.message}`); } return res.data; } } /* * 抽奖信息模块 * */ class LotteryInfoGetter extends CommonAPI { constructor() { super(); } /* * 根据传入的抽奖动态列表, 获取抽奖信息列表 * */ async getLotteryInfoList(dynamicList, csrf) { const lotteryInfoList = []; for (let dynamicInfo of dynamicList) { const lotteryInfo = await this.getLotteryInfo(dynamicInfo, csrf); if (!lotteryInfo) { continue; } lotteryInfoList.push(lotteryInfo); } return lotteryInfoList; } /** * @param dynamicId 动态id * @param csrf csrf, 个人身份码 */ async api_GetLotteryInfo(dynamicId, csrf) { const res = await this.fetch({ url: 'https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice', method: 'GET', param: { business_id: dynamicId, csrf: csrf, business_type: 1, }, }); if (res && res.code !== 0) { throw new Error(`获取抽奖信息失败: ${res.message}`); } return res.data; } /* * 根据抽奖动态, 获取抽奖信息 * */ async getLotteryInfo(dynamicItem, csrf) { if (dynamicItem.type !== 'DYNAMIC_TYPE_FORWARD') { return null; } const originDynamicInfo = dynamicItem.orig; if (!originDynamicInfo) { return null; } const lotteryNode = originDynamicInfo.modules.module_dynamic.desc.rich_text_nodes .find(node => node.type === 'RICH_TEXT_NODE_TYPE_LOTTERY'); if (!lotteryNode) { return null; } return await this.api_GetLotteryInfo(originDynamicInfo.id_str, csrf); } } /* * 删除动态模块 * */ class DeleteController extends CommonAPI { constructor() { super(); } /** * 删除动态 */ async deleteDynamicList(lotteryDynamicList, csrf) { const lotteryInfo = new LotteryInfoGetter(); const lotteryInfoList = await lotteryInfo.getLotteryInfoList(lotteryDynamicList, csrf); // TODO 数据不统一处理 if (lotteryDynamicList.length !== lotteryInfoList.length) { return; } while (lotteryDynamicList.length) { const dynamicItem = lotteryDynamicList.shift(); const lotteryInfo = lotteryInfoList.shift(); if (!dynamicItem || !lotteryInfo) { break; } /* * 判断当前是否已经超时 * */ // 如果不超时, 则退出 if (Date.now() < lotteryInfo.lottery_time * 1000) { continue; } // 删除动态 try { await this.api_DeleteDynamic(dynamicItem.id_str, csrf); } catch (e) { console.error(`删除动态失败: ${dynamicItem.id_str}`); } this.notify('已删除动态', `${dynamicItem.orig?.modules.module_author.name}: \n${dynamicItem.orig?.modules.module_dynamic.desc.text}`, () => { GM_openInTab(`https://t.bilibili.com/${dynamicItem.orig?.id_str}`); }); /* * 查询配置项, 判断是否需要取消关注 * */ if (!UnFollowStorage.get()) { continue; } /* * 判断当前UP主是否在默认分组中 * */ // 查询用户分组 const userGroup = await this.api_GetUserInGroup(lotteryInfo.sender_uid); // 如果不存在默认分组中 (默认分组: 已关注并且userGroup返回的对象没有返回值) const userInDefaultGroup = dynamicItem.orig?.modules.module_author.following && Object.entries(userGroup).length === 0; if (!userInDefaultGroup) { continue; } /* * 判断是否还存在该UP的抽奖动态 * */ const existLotteryWithSameUser = Boolean(lotteryInfoList.find(item => item.sender_uid === lotteryInfo.sender_uid)); if (existLotteryWithSameUser) { continue; } // 不存在, 则取消关注 await this.api_UnfollowUser(lotteryInfo.sender_uid, csrf); this.notify('取消关注', `已取消关注用户: ${dynamicItem.orig?.modules.module_author.name}`, () => { GM_openInTab(`https://space.bilibili.com/${lotteryInfo.sender_uid}`); }); } } /** * 删除动态 */ async api_DeleteDynamic(dynamicId, csrf) { const res = await this.fetch({ url: 'https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/rm_dynamic', method: 'POST', param: { dynamic_id: dynamicId, csrf_token: csrf, csrf, }, }); if (res && res.code !== 0) { throw new Error(`删除失败: ${res.message}`); } } /** * 取关用户 */ async api_UnfollowUser(uid, csrf) { const res = await this.fetch({ url: 'https://api.bilibili.com/x/relation/modify', method: 'POST', param: { fid: uid, act: 2, re_src: 11, csrf, }, }); if (res && res.code !== 0) { throw new Error(`取关失败: ${res.message}`); } } /** * 查询用户所在分组 */ async api_GetUserInGroup(uid) { const res = await this.fetch({ url: 'https://api.bilibili.com/x/relation/tag/user', method: 'GET', param: { fid: uid, }, }); if (res && res.code !== 0) { throw new Error(`获取分组失败: ${res.message}`); } return res.data; } } // @ts-ignore // noinspection JSAnnotator return new Promise(async (resolve, reject) => { // 获取用户信息 const userInfoGetter = new UserInfoGetter(); const csrf = await userInfoGetter.getCsrf(); const uid = await userInfoGetter.getUid(); // 获取所有用户抽奖动态 const dynamicGetter = new DynamicGetter(); const lotteryDynamicList = await dynamicGetter.getAllLotteryDynamic(uid); console.log(`抽奖动态总数: ${lotteryDynamicList.length}`, lotteryDynamicList); // 删除动态 const deleteController = new DeleteController(); await deleteController.deleteDynamicList(lotteryDynamicList, csrf); });