import { $, $$, addStyle, DOMUtils, log, pops, utils } from "@/env"; import { PanelUISize } from "@components/setting/panel-ui-size"; import { VueUtils } from "@components/utils/VueUtils"; import { TiebaUniAppComponentDetectionRule, type TiebaUserLabelRule } from "./TiebaUniAppComponentDetectionRule"; import Qmsg from "qmsg"; import { TieBaApi } from "../api/TiebaApi"; import { TiebaHomeApi, type UserConcernInfo, type UserFollowInfo, type UserPostInfo } from "../api/TiebaHomeApi"; import { TiebaPCApi } from "../api/TiebaPCApi"; import { TiebaUrlHandler } from "../handler/TiebaUrlHandler"; import { TiebaNewPCApi } from "../api/TiebaNewPCApi"; /** 匹配信息 */ type MatchedInfo = { rule: TiebaUserLabelRule; /** 匹配的数据信息 */ matchedInfoList: { /** 符合规则的原因 */ reason: string; /** 符合规则的匹配的文本 */ reasonText: string; /** 符合规则的链接 */ reasonLink: string | null; reasonTime: number | string | null; }[]; }; export const TiebaUniAppComponentDetection = { $data: { /** 查询图标svg */ searchIcon: /*html*/ ` `, }, init() { log.info(`成分检测`); TiebaUniAppComponentDetectionRule.init(); addStyle(/*css*/ ` .composition-checkable{ padding: 4px 0px; } .composition-checkable, .composition-checked{ display: inline-flex; vertical-align: middle; flex-wrap: wrap; align-items: center; } /* 查询按钮 */ .composition-checkable .composition-badge-control { display: inline-flex; justify-content: center; align-items: center; width: fit-content; height: fit-content; background: #574AB830; border-radius: 8px; margin: 0 0 0 6px; font-family: PingFang SC, HarmonyOS_Regular, Helvetica Neue, Microsoft YaHei, sans-serif; } .composition-checkable .composition-name-control { color: #7367F0; padding: 2px 8px; font-size: 14px; display: flex; align-items: center; height: 20px; line-height: normal; } .composition-checkable .composition-name-control svg { vertical-align: middle; width: 16px; height: 16px; } /* ↑查询按钮 */ /* 标签按钮 */ .composition-checked .composition-badge { display: inline-flex; justify-content: center; align-items: center; width: fit-content; background: #574AB825; border-radius: 10px; margin: 0 6px 0 6px; font-family: PingFang SC, HarmonyOS_Regular, Helvetica Neue, Microsoft YaHei, sans-serif; font-weight: normal; cursor: pointer; } .composition-checked .composition-name { color: #574AB8; padding: 2px 8px; font-size: 14px; line-height: 14px; } .composition-checked .composition-icon { color: #574AB8 !important; background: transparent !important; border-radius: 50% !important; width: 20px !important; height: 20px !important; border: 2px solid #574AB880 !important; margin: -6px; display: flex !important; justify-content: center !important; align-items: center !important; font-size: 16px !important; } .composition-checked .composition-badge > *:first-child{ margin-left: 6px; } .composition-checked .composition-badge > *:last-child{ margin-right: 6px; } .composition-checked .composition-badge .composition-icon, .composition-checked .composition-badge .composition-name{ margin: 0; white-space: nowrap; } `); DOMUtils.onReady(() => { const setInjectAttr = ($el: Element) => { $el.setAttribute("data-is-inject-search-label", ""); }; const lockFn = new utils.LockFunction(async () => { // 发帖人 [$("uni-view.user-info .name-info:not([data-is-inject-search-label])")].forEach(($el) => { if (!$el) return; setInjectAttr($el); const { $container } = this.createSearchButton(() => { const $name = $el.querySelector(".head-name"); if (!$name) { throw new Error("获取name失败"); } const name = DOMUtils.text($name); const vueInst = VueUtils.getVue($el); if (!vueInst) { throw new TypeError("获取vue属性失败"); } const portrait = vueInst?.$parent?.config?.param?.portrait; if (typeof portrait !== "string") { throw new TypeError("获取portrait失败"); } return { portrait: portrait, name_show: name, }; }); DOMUtils.append($el, $container); }); // 评论区的 $$( ".pb-comment-item .player-info:not([data-is-inject-search-label]):has(.user-info-degrade)" ).forEach(($replyItem) => { setInjectAttr($replyItem); const { $container } = this.createSearchButton(() => { const $userInfoDegrade = $replyItem.querySelector(".user-info-degrade")!; const vueInst = VueUtils.getVue($userInfoDegrade); if (!vueInst) { throw new TypeError("获取vue属性失败"); } const nameShow: null | string = vueInst?.nameInfo?.text; if (nameShow == null) { throw new TypeError("获取nameShow失败"); } const sectionData = vueInst?.$parent?.sectionData; if (Array.isArray(sectionData)) { const findValue = sectionData.find((item) => item?.author?.name_show === nameShow); if (!findValue) { throw new TypeError("获取对应的用户信息失败"); } return { id: findValue?.author?.id, portrait: findValue?.author?.portrait, name_show: findValue?.author?.name_show, }; } else { const portrait = vueInst.$parent?.config?.param?.portrait; if (typeof portrait !== "string") { throw new TypeError("获取portrait失败"); } return { portrait, name_show: nameShow, }; } }); DOMUtils.append($replyItem, $container); }); // 楼中楼弹窗内的 $$( ".pb-comment-item-container .player-info:not([data-is-inject-search-label]):has(:not(.user-info-degrade))" ).forEach(($replyItem) => { setInjectAttr($replyItem); const { $container } = this.createSearchButton(() => { const $pbCommentItemContainer = $replyItem.closest(".pb-comment-item-container"); if (!$pbCommentItemContainer) { throw new TypeError("获取$pbCommentItemContainer失败"); } const vueInst = VueUtils.getVue($pbCommentItemContainer); if (!vueInst) { throw new TypeError("获取vue属性失败"); } const commentData = vueInst?.commentData; if (!commentData) { throw new TypeError("获取commentData失败"); } return { id: commentData?.author?.id, portrait: commentData?.author?.portrait, name_show: commentData?.author?.name_show, }; }); DOMUtils.append($replyItem, $container); }); }); utils.mutationObserver(document, { config: { subtree: true, childList: true, }, immediate: true, callback: () => { lockFn.run(); }, }); }); }, /** * 查询用户所有的信息 * * 即提取需要判断的信息 * @param userName */ async queryUserAllInfo(userInfo: { name_show: string; portrait: string; id: string | number }) { const userName = userInfo.name_show; // 获取关注的用户 const allFollowingData: UserFollowInfo[] = []; const followMaxPage = 12; let followPage = 1; while (true) { const followingData = await TiebaNewPCApi.followList_pc(userInfo.portrait, userInfo.id, followPage); if (!followingData) { break; } allFollowingData.push( ...followingData.follow_list.map((it) => { return { url: TiebaUrlHandler.getUserHome(it.portrait), userName: it.name_show || it.name, avatar: TiebaUrlHandler.getUserAvatar(it.portrait), portrait: it.portrait, }; }) ); if (!followingData.has_more) { break; } if (followPage >= followMaxPage) { break; } followPage++; await utils.sleep(150); } // 获取关注的吧 const allConcernData: UserConcernInfo[] = []; let concernPN = 1; let concernMaxCount = 10; let concernCount = 0; while (true) { const concernData = await TiebaHomeApi.getConcern(userName, concernPN); if (!concernData) { break; } allConcernData.push(...concernData.data); if (concernCount > concernMaxCount) { break; } if (!concernData.has_more) { break; } concernPN++; concernCount++; await utils.sleep(150); } // 发布的帖子 const allPostData: UserPostInfo[] = []; let postMaxPN = 10; let postPN = 1; while (true) { const postData = await TiebaHomeApi.getPost(userName, postPN); if (!postData) { break; } allPostData.push(...postData.data); if (postPN > postMaxPN) { break; } postPN++; await utils.sleep(150); } // 提取获取到的数据 const result = { /** 关注列表信息 */ following: allFollowingData, /** 关注的吧的信息 */ concernForum: allConcernData, /** 发帖/回帖信息 */ postInfo: allPostData, }; return result; }, /** * 创建查询按钮 * @param queryUserInfoFn 查询mid的函数 */ createSearchButton( queryUserInfoFn: () => { name_show: string; portrait: string; } ) { const $compositionCheckable = DOMUtils.createElement("div", { className: "composition-checkable", innerHTML: /*html*/ `
${this.$data.searchIcon}
`, }); const $badge = $compositionCheckable.querySelector(".composition-badge-control")!; const $compositionNameControl = $compositionCheckable.querySelector(".composition-name-control")!; DOMUtils.on( $badge, "click", async (event) => { DOMUtils.preventEvent(event); if ($compositionCheckable.hasAttribute("data-is-searching")) { log.error("正在搜索中,请稍后再试"); return; } $compositionCheckable.setAttribute("data-is-searching", ""); DOMUtils.html($compositionNameControl, "..."); try { const queryUserInfo = queryUserInfoFn(); const userInfo = { ...queryUserInfo, uid: "" as string | number, }; this.clearLabel($compositionCheckable); const homeSiderbarRight = await TiebaNewPCApi.homeSidebarRight(userInfo.portrait); if (homeSiderbarRight) { userInfo.name_show = homeSiderbarRight.user.name_show; userInfo.uid = homeSiderbarRight.user.id; } else { throw new TypeError("获取用户api信息中的uid失败"); } const userAllInfo = await this.queryUserAllInfo({ portrait: userInfo.portrait, name_show: userInfo.name_show, id: userInfo.uid, }); if (!userAllInfo) { throw new TypeError("获取用户所有信息失败"); } log.info(`当前检索出的用户所有信息:`, userAllInfo); this.handleShowLabel(userInfo, userAllInfo, $compositionCheckable); // 重置状态为搜索图标 DOMUtils.html($compositionNameControl, this.$data.searchIcon); } catch (error: any) { log.error(error); Qmsg.error(error.message, { timeout: 3500, }); DOMUtils.html($compositionNameControl, "重试"); } finally { $compositionCheckable.removeAttribute("data-is-searching"); } }, { capture: true } ); return { $container: $compositionCheckable, $compositionNameControl, }; }, /** * 创建标签 * @param data */ createLabel(data: MatchedInfo) { let $label = DOMUtils.createElement("div", { className: "composition-checked", innerHTML: /*html*/ `
`, }); let $badge = $label.querySelector(".composition-badge")!; if (data.rule.data.isShowDisplayName) { // 显示标签名 let $compositionName = DOMUtils.createElement("span", { className: "composition-name", innerHTML: data.rule.data.displayName, }); DOMUtils.append($badge, $compositionName); } if (data.rule.data.isShowDisplayIcon) { // 显示图标 let $compositionIcon: HTMLElement | null = null; if (data.rule.data.displayIcon.startsWith("http")) { // 网络图片 $compositionIcon = DOMUtils.createElement( "img", { className: "composition-icon", src: data.rule.data.displayIcon, }, { referrer: "no-referrer", referrerPolicy: "no-referrer", } ); } else { $compositionIcon = DOMUtils.createElement("span", { className: "composition-icon", innerHTML: data.rule.data.displayIcon, }); } DOMUtils.append($badge, $compositionIcon); } DOMUtils.on( $badge, "click", (event) => { DOMUtils.preventEvent(event); pops.alert({ title: { text: "识别信息", html: false, position: "center", }, content: { text: /*html*/ ` ${data.matchedInfoList .map((it) => { let $el = DOMUtils.createElement("div", { className: "reason-container", innerHTML: /*html*/ `
原因:${it.reason}
匹配:${ typeof it.reasonLink === "string" ? /*html*/ ` ${it.reasonText} ` : it.reasonText }
`, }); if (it.reasonTime != null) { let $reasonTime = DOMUtils.createElement("div", { className: "reason-text", innerHTML: /*html*/ ` 时间:${typeof it.reasonTime === "number" ? utils.formatTime(it.reasonTime) : it.reasonTime} `, }); DOMUtils.append($el, $reasonTime); } return $el.outerHTML; }) .join("\n")} `, html: true, }, btn: { ok: { enable: false }, }, mask: { enable: true, clickEvent: { toClose: true, }, }, width: PanelUISize.setting.width, height: PanelUISize.setting.height, style: /*css*/ ` .reason-container{ color: #7367F0; margin: 10px 10px; } `, }); }, { capture: true } ); return $label; }, /** * 清空标签 * @param $ele */ clearLabel($ele: HTMLElement) { $ele.querySelectorAll(".composition-checked").forEach((it) => it.remove()); }, /** * 处理并显示标签 * @param userId 用户id * @param data * @param $searchContainer */ handleShowLabel( userInfo: { uid: string | number; portrait: string; }, data: Exclude>, undefined>, $searchContainer: HTMLElement ) { if (TiebaUniAppComponentDetectionRule.$data.ruleData.length === 0) { Qmsg.warning("未配置规则,请在设置中进行添加"); return; } const userId = userInfo.uid.toString(); if (TiebaUniAppComponentDetectionRule.$data.whiteList.includes(userId)) { // 白名单用户 // 不处理 return; } /** 命中的规则 */ const matchedAllRule: MatchedInfo[] = []; /** * 添加命中的规则 * @param rule * @param matchedInfo */ const pushMatchedRule = (rule: MatchedInfo["rule"], matchedInfo: MatchedInfo["matchedInfoList"]["0"]) => { let findValue = matchedAllRule.find((it) => it.rule === rule); if (findValue) { findValue.matchedInfoList.push(matchedInfo); } else { matchedAllRule.push({ rule: rule, matchedInfoList: [matchedInfo], }); } }; TiebaUniAppComponentDetectionRule.$data.ruleData.forEach((ruleData) => { if ( Array.isArray(ruleData.data.blacklist) && ruleData.data.blacklist.find( (it) => it.toString() === userId || (typeof userInfo.portrait === "string" && userInfo.portrait.startsWith(it.toString())) ) ) { // 黑名单中存在符合的id pushMatchedRule(ruleData, { reason: "黑名单用户", reasonText: userId, reasonLink: TiebaUrlHandler.getUserHomeByUN(userId), reasonTime: null, }); return; } if (Array.isArray(ruleData.data.followings)) { let reason = "关注列表"; let reasonText = ""; let checkFlag = ruleData.data.followings.some((followPortrait) => { let __check__flag__ = data.following.some((followingData) => { return followingData.portrait === followPortrait.toString(); }); if (__check__flag__) { reasonText = followPortrait.toString(); } return __check__flag__; }); if (checkFlag) { // 关注列表中存在符合的portrait pushMatchedRule(ruleData, { reason: reason, reasonText: reasonText, reasonLink: TiebaUrlHandler.getUserHome(userInfo.portrait), reasonTime: null, }); } } if (Array.isArray(ruleData.data.followingForums)) { // 关注的吧 let reason = "关注的吧"; let reasonText = ""; let checkFlag = ruleData.data.followingForums.some((followingForumInfo) => { let __check__flag__ = data.concernForum.some((forumInfo) => { return forumInfo.forumName === followingForumInfo.toString(); }); if (__check__flag__) { reasonText = followingForumInfo.toString(); } return __check__flag__; }); if (checkFlag) { // 关注的吧中存在符合的吧名 pushMatchedRule(ruleData, { reason: reason, reasonText: reasonText, reasonLink: TiebaUrlHandler.getForum(reasonText), reasonTime: null, }); } } if (Array.isArray(ruleData.data.keywords)) { // 关键词 ruleData.data.keywords.forEach((keyword) => { // 比如说吧 // 目前暂时认为回复的帖子的内容不算 // 发布的帖子如果规则中设置了关注的吧的话,那么发帖在该吧就算是命中 for (let spaceIndex = 0; spaceIndex < data.postInfo.length; spaceIndex++) { const spaceData = data.postInfo[spaceIndex]; let reason = ""; let reasonText = keyword; let reasonLink = spaceData.url; let reasonTime = spaceData.time; if (spaceData.replyContent == null) { // 发的帖子 if (typeof spaceData.title === "string" && spaceData.title.match(keyword)) { reason = "发布的帖子的标题"; } else if (typeof spaceData.content === "string" && spaceData.content.match(keyword)) { reason = "发布的帖子的内容"; } else if ( typeof spaceData.forumName === "string" && Array.isArray(ruleData.data.followingForums) && ruleData.data.followingForums.findIndex((it) => it.toString() === spaceData.forumName.toString()) !== -1 ) { reason = "发布的帖子所在的吧"; reasonText = spaceData.forumName; } } else { // 评论 if (typeof spaceData.replyContent === "string" && spaceData.replyContent.match(keyword)) { // 转发的评论内容中存在关键词 reason = "评论的内容"; } } if (reason !== "") { pushMatchedRule(ruleData, { reason: reason, reasonText: reasonText, reasonLink: reasonLink, reasonTime: reasonTime, }); } } }); } }); // 按出现次数排序,出现次数最高的排在最前面 utils.sortListByProperty( matchedAllRule, (value) => { return value.matchedInfoList.length; }, true ); DOMUtils.append( $searchContainer, matchedAllRule.map((it) => { return this.createLabel(it); }) ); }, };