// ==UserScript== // @name discord黑白名单 // @namespace http://tampermonkey.net/ // @version 0.8.0 // @description 通过Dicord唯一的用户ID,可以跨群识别用户,并根据黑白名单给用户头像加标记。还可以根据与白名单名字的相似程度预警 // // @author devplugin@protonmail.com // @match https://discord.com/* // @grant GM_setValue // @grant GM_getValue // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.3/pako.min.js // @downloadURL https://update.greasyfork.icu/scripts/420853/discord%E9%BB%91%E7%99%BD%E5%90%8D%E5%8D%95.user.js // @updateURL https://update.greasyfork.icu/scripts/420853/discord%E9%BB%91%E7%99%BD%E5%90%8D%E5%8D%95.meta.js // ==/UserScript== (function () { 'use strict'; let config = { "alertTH": 0.7 }; /////////////////////////////////////// /// 基本操作 /////////////////////////////////////// // Array Remove - By John Resig (MIT Licensed) function array_remove_pos(array, from, to) { let rest = array.slice((to || from) + 1 || array.length); array.length = from < 0 ? array.length + from : from; return array.push.apply(array, rest); } function array_remove_value(array, value) { let pos = array.indexOf(value); while (pos >= 0) { array_remove_pos(array, pos); pos = array.indexOf(value); } } const log = { debug() { extLogger ? extLogger('debug', arguments) : console.debug.apply(console, arguments); }, info() { extLogger ? extLogger('info', arguments) : console.info.apply(console, arguments); }, verb() { extLogger ? extLogger('verb', arguments) : console.log.apply(console, arguments); }, warn() { extLogger ? extLogger('warn', arguments) : console.warn.apply(console, arguments); }, error() { extLogger ? extLogger('error', arguments) : console.error.apply(console, arguments); }, success() { extLogger ? extLogger('success', arguments) : console.info.apply(console, arguments); } }; const insertCss = (css) => { const style = document.createElement('style'); style.appendChild(document.createTextNode(css)); document.head.appendChild(style); return style; }; const createElm = (html) => { const temp = document.createElement('div'); temp.innerHTML = html; return temp.removeChild(temp.firstElementChild); }; //文本相似度计算 function similar(s, t, f) { if (!s || !t) { return 0; } var l = s.length > t.length ? s.length : t.length; var n = s.length; var m = t.length; var d = []; f = f || 3; var min = function (a, b, c) { return a < b ? (a < c ? a : c) : (b < c ? b : c); }; var i, j, si, tj, cost; if (n === 0) return m; if (m === 0) return n; for (i = 0; i <= n; i++) { d[i] = []; d[i][0] = i; } for (j = 0; j <= m; j++) { d[0][j] = j; } for (i = 1; i <= n; i++) { si = s.charAt(i - 1); for (j = 1; j <= m; j++) { tj = t.charAt(j - 1); if (si === tj) { cost = 0; } else { cost = 1; } d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); } } let res = (1 - d[n][m] / l); return res.toFixed(f); } let namecache = {}; //提取纯文本,去除字符串中的空格标点符号表情符号等 function cleanString(str) { if (str === undefined) return str; if (str in namecache) return namecache[str]; const regStr = /[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF][\u200D|\uFE0F]|[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF]|[0-9|*|#]\uFE0F\u20E3|[0-9|#]\u20E3|[\u203C-\u3299]\uFE0F\u200D|[\u203C-\u3299]\uFE0F|[\u2122-\u2B55]|\u303D|[\A9|\AE]\u3030|\uA9|\uAE|\u3030/ig; const regStr2 = /[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~¡¦«¸»¿‐—―‖‘’“”•…‹›、。〈〉《》「」『』【】〔〕〖〗〝〞︰﹐﹑﹔﹕﹖﹝﹞﹟﹠﹡﹢﹤﹦﹩﹪﹫!"'(),:;?¥\s\r\n]/g const regStr3 = /﹌﹏﹋´ˊˋ︳︴¯_ ̄˜﹨﹍﹉﹎﹊ˇ︵︶︷︸︹︿﹀︺︽︾ˉ﹁﹂﹃﹄︻︼/g; let simple_str = str.replace(regStr, "").replace(regStr2, "").replace(regStr3, ""); namecache[str] = simple_str; return simple_str; } /////////////////////////////////////// /// 数据操作 /////////////////////////////////////// let userdb = { "users": {}, "whitelist": [], "blacklist": [] }; let db_changed = 0; function usereq(u1, u2) { return u1["uid"] === u2["uid"] && u1["name"] === u2["name"] && u1["imgsrc"] === u2["imgsrc"]; } function adduser(user) { if (user["name"] === undefined || user["name"] === "") { return; } let uid = user["uid"]; if (!(uid in userdb)) { userdb["users"][uid] = [user]; db_changed += 1; } else { let flag = 1; for (let i = 0; i < userdb["users"][uid].length; i++) { if (usereq(user, userdb[uid][0])) { flag = 0; break; } } if (flag) { userdb["users"][uid].splice(0, 0, user); db_changed += 1; } } } function remove_user_from_whitelist(uid) { array_remove_value(userdb["whitelist"], uid); } function remove_user_from_blacklist(uid) { array_remove_value(userdb["blacklist"], uid); } function adduser_to_whitelist(uid) { if (userdb["whitelist"].indexOf(uid) < 0) { userdb["whitelist"].push(uid); } array_remove_value(userdb["blacklist"], uid); db_changed += 1; } function adduser_to_blacklist(uid) { if (userdb["blacklist"].indexOf(uid) < 0) { userdb["blacklist"].push(uid); } array_remove_value(userdb["whitelist"], uid); db_changed += 1; } function getUserCount() { return Object.keys(userdb["users"]).length; } function getUserName(uid) { return userdb["users"][uid][0]["name"]; } function userIsDangerous(uid) { if (!(uid in userdb["users"])) return; const cur_name = cleanString(getUserName(uid)); for (let i = 0; i < userdb["whitelist"].length; i++) { const w_uid = userdb["whitelist"][i]; if (!(w_uid in userdb["users"])) continue; const w_name = cleanString(getUserName(w_uid)); const sx = similar(cur_name, w_name); if (sx > config["alertTH"]) { return true; } } return false; } function getSimilarUserFromWhitelist(uid) { let result = []; const cur_name = cleanString(getUserName(uid)); for (let i = 0; i < userdb["whitelist"].length; i++) { const w_uid = userdb["whitelist"][i]; const w_name = cleanString(getUserName(w_uid)); const sx = similar(cur_name, w_name); if (sx > config["alertTH"]) { result.push([w_uid, sx]); } } return result; } /////////////////////////////////////// /// 数据导入导出 /////////////////////////////////////// function initUserDB() { userdb = { "users": {}, "whitelist": [], "blacklist": [] }; } function clearWhiteList() { userdb["whitelist"] = []; } function clearBlackList() { userdb["blacklist"] = []; } function saveUserDB() { //console.log(userdb); const cdd = pako.gzip(encodeURIComponent(JSON.stringify(userdb))); GM_setValue("lt_userdb_for_discord", cdd); db_changed = 0; console.log("[TM] user count=", getUserCount()); console.log("[TM] compressed length=", cdd.length); } function loadUserDB() { const cdd = GM_getValue("lt_userdb_for_discord"); if (!cdd) { return; } console.log(Object.keys(cdd).length); console.log("[TM] Load DB....... "); const data = pako.inflate(cdd); console.log("[TM] data length=", data.length); const strData = new TextDecoder("utf-8").decode(data); console.log("[TM] string length=", strData.length); userdb = JSON.parse(decodeURIComponent(strData)); db_changed = 0; console.log("[TM] user count=", getUserCount()); } function exportDBToFile() { // 创建a标签 var elementA = document.createElement('a'); //文件的名称为时间戳加文件名后缀 elementA.download = "db-" + new Date() + ".json"; elementA.style.display = 'none'; //生成一个blob二进制数据,内容为json数据 var blob = new Blob([JSON.stringify(userdb)]); //生成一个指向blob的URL地址,并赋值给a标签的href属性 elementA.href = URL.createObjectURL(blob); document.body.appendChild(elementA); elementA.click(); document.body.removeChild(elementA); } //从json文件导入数据库 function importDBFromFile(event) { const fileInputControl = event.srcElement; const filepath = fileInputControl.files[0]; if (filepath) { let reader = new FileReader(); reader.onload = function (evt) { const jsontext = evt.target.result; userdb = JSON.parse(jsontext); showWhiteBlackList(); }; reader.readAsText(filepath); } } /////////////////////////////////////// /// 界面交互 /////////////////////////////////////// let resource = { "whiteiconhtml": ``, "blackiconhtml": ``, "warningiconhtml": ``, "startbuttonhtml": `

`, "css": ` #ltmainui-btn{position: relative; height: 24px;width: auto;-webkit-box-flex: 0;-ms-flex: 0 0 auto;flex: 0 0 auto;margin: 0 8px;cursor:pointer; color: var(--interactive-normal);} #ltmainui{position:fixed;top:100px;right:10px;bottom:10px;width:780px;z-index:99;color:var(--text-normal);background-color:var(--background-secondary);box-shadow:var(--elevation-stroke),var(--elevation-high);border-radius:4px;display:none;flex-direction:column} #ltmainui a{color:#00b0f4} #ltmainui.redact .priv{display:none!important} #ltmainui:not(.redact) .mask{display:none!important} #ltmainui.redact [priv]{-webkit-text-security:disc!important} #ltmainui .toolbar span{margin-right:8px} #ltmainui button,#ltmainui .btn{color:#fff;background:#7289da;border:0;border-radius:4px;font-size:14px;line-height:100%;} #ltmainui .filePicker{height:100%;} #ltmainui button:disabled{display:none} #ltmainui input[type="text"],#ltmainui input[type="search"],#ltmainui input[type="password"],#ltmainui input[type="datetime-local"],#ltmainui input[type="number"]{background-color:#202225;color:#b9bbbe;border-radius:4px;border:0;padding:0 .5em;height:24px;width:144px;margin:2px} #ltmainui input#file {display:none} #ltmainui hr{border-color:rgba(255,255,255,0.1)} #ltmainui .header{padding:12px 16px;background-color:var(--background-tertiary);color:var(--text-muted)} #ltmainui .form{padding:8px;background:var(--background-secondary);box-shadow:0 1px 0 rgba(0,0,0,.2),0 1.5px 0 rgba(0,0,0,.05),0 2px 0 rgba(0,0,0,.05)} #ltmainui .logarea{overflow:auto;font-size:.75rem;font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;flex-grow:1;padding:10px} .ltmainui-icon{position: absolute;left: 42px;top: 0px;width: 1.25em;height: 1.25em;vertical-align: middle;overflow: hidden;z-index: 99;} #ltmainui .container{display: flex;} #ltmainui .rightbox {left: 200px;} #ltmainui .avatar {width: 32px;height: 32px;vertical-align: middle;fill: currentColor;overflow: hidden;} #ltmainui .proterties {display: inline-block;width: 202px;} #ltmainui .property-item {line-height: 34px;left: 9px;top: 0;text-align: initial;white-space: nowrap;right: 9px;height: 64px;width: 200px;background-color: #fff;border: 1px solid #c8c8c8;border-radius: 3px;color: #ccc;font-weight: 400;font-size: 10px;} #ltmainui table{border-spacing: 0; border-collapse: collapse; text-align: center; border: 3px solid purple; font-family: verdana,arial,sans-serif; font-size: 11px; color: #333333; border-width: 1px; border-color: #666666; border-collapse: collapse; height: 300px; width: 350px;} #ltmainui table tbody{display: block; width: 100%; height: 100%; overflow-y: scroll; } #ltmainui table td{ border-width: 1px; padding: 1px; border-style: solid; border-color: #666666; background-color: #ffffff; } #ltmainui .cellimg {width: 32px; height: 32px; border-radius: 50%; } `, "UIhtml": `
拉清单 - 记录Discord用户的唯一ID,跨群识别用户,与白名单相似的名字会被提示风险
导出数据

白名单
黑名单
        
Star this project on github.com/mplugin/discordwblist!\n\n Issues or help
` } function getServerNameFromUI() { const servername = $(".name-1jkAdW").text(); return servername; } function getUidFromImgsrc(imgsrc) { const items = imgsrc.split("/"); if (items.length === 6) { return items[4]; } return undefined; } function AddIconToUser(userimagenode, icontype) { let nextnode = userimagenode.next(); if (nextnode[0].className.animVal === "ltmainui-icon") { nextnode.remove(); } let html = ""; if (icontype === "white") { html = resource["whiteiconhtml"]; } else if (icontype === "black") { html = resource["blackiconhtml"]; } else if (icontype === 'warning') { html = resource["warningiconhtml"]; } const iconnode = $(html); let prenode = userimagenode.parent().prev(); if (prenode.length > 0) { iconnode.css("top", "20px"); } userimagenode.after(iconnode); return iconnode; } function showReason() { const uid = getUidFromImgsrc($(this).prev().attr("src")); const uname = getUserName(uid); const suserids = getSimilarUserFromWhitelist(uid); console.log(uid, uname, suserids); let msg = "[" + uname + "]与白名单中的下列名字相似:\n"; for (let i = 0; i < suserids.length; i++) { let name = getUserName(suserids[i][0]); const sx = suserids[i][1] msg += " " + name + " 相似度=" + sx + "\n"; } alert(msg); return; } function updateOneUserIconInMessage(userimagenode) { //console.log("[TM] [updateOneUserIconInMessage] ", userimagenode); const imgsrc = userimagenode.attr("src"); if (imgsrc === undefined) { return; } const uid = getUidFromImgsrc(imgsrc); if (uid === undefined) return; if (userdb["whitelist"].indexOf(uid) >= 0) { AddIconToUser(userimagenode, "white"); } else if (userdb["blacklist"].indexOf(uid) >= 0) { AddIconToUser(userimagenode, "black"); } else if (userIsDangerous(uid)) { let iconnode = AddIconToUser(userimagenode, "warning"); iconnode.click(showReason); } else { let nextnode = userimagenode.next(); if (nextnode[0].className.animVal === "ltmainui-icon") { nextnode.remove(); } } } function updateUsersIconInMessage() { $(".contents-2mQqc9").each(function (i, messagenode) { let imagenode = $(messagenode.childNodes[0]); updateOneUserIconInMessage(imagenode.first()); }); } function getUsersFromMemberList() { $(".member-3-YXUe").each(function (i, n) { const imgsrc = $("img", n).first().attr("src"); if (imgsrc === undefined) { return; } const uid = getUidFromImgsrc(imgsrc); if (uid === undefined) return; const name = $("span.roleColor-rz2vM0", n).first().text(); if (name === undefined || name === "") return; adduser({ "uid": uid, "name": name, "img": imgsrc }); }); } function getUsersFromMessageList() { $(".contents-2mQqc9").each(function (i, messagenode) { let imagenode = $(messagenode.childNodes[0]); const imgsrc = imagenode.attr("src"); if (imgsrc === undefined) return; const uid = getUidFromImgsrc(imgsrc); if (uid === undefined) return; const name = $(messagenode.childNodes[1].childNodes[0].childNodes[0]).text(); if (name === undefined || name === "") return; adduser({ "uid": uid, "name": name, "img": imgsrc }); }); } function appendToUserList(uid, iswhite) { const name = userdb["users"][uid][0]["name"]; const img = userdb["users"][uid][0]["img"]; let newRow = `${uid}${name}`; if (iswhite) { $("#whitelisttable tbody").append(newRow); } else { $("#blacklisttable tbody").append(newRow); } } function clearUserList(iswhite) { if (iswhite) { $("#whitelisttable tbody").empty(); } else { $("#blacklisttable tbody").empty(); } } function showUserList(iswhite) { clearUserList(iswhite); let userlist = null; if (iswhite) userlist = userdb["whitelist"]; else userlist = userdb["blacklist"]; for (let i = 0; i < userlist.length; i++) { const uid = userlist[i]; appendToUserList(uid, iswhite); } } function addToUserList(uid, iswhite) { if (uid === undefined || uid === null || uid === "") return; const remove_listname = (!iswhite) ? "whitelist" : "blacklist"; let should_delete = userdb[remove_listname].indexOf(uid) >= 0; if (iswhite) adduser_to_whitelist(uid); else adduser_to_blacklist(uid); appendToUserList(uid, iswhite); if (should_delete) { showUserList(!iswhite); } updateUsersIconInMessage(); } function showWhiteBlackList() { showUserList(true); showUserList(false); } function addUserToWhitelist(uid) { addToUserList(uid, true); } function addUserToBlacklist(uid) { addToUserList(uid, false); } /////////////////////////////////////// //支持拖动头像功能 /////////////////////////////////////// function mountDropFunc(popover) { const droppables = $('.droppable', popover); for (const droppable of droppables) { droppable.addEventListener('dragover', dragOver); droppable.addEventListener('dragleave', dragLeave); droppable.addEventListener('dragenter', dragEnter); droppable.addEventListener('drop', dragDrop); } function dragOver(e) { e.dataTransfer.dropEffect = 'copy'; e.preventDefault(); e.stopPropagation(); } function dragEnter(e) { e.preventDefault(); e.stopPropagation(); //this.className += ' drag-over'; } function dragLeave(e) { e.preventDefault(); e.stopPropagation(); //this.className = 'droppable'; } function dragDrop(e) { e.preventDefault(); e.stopPropagation(); //this.className = 'droppable'; console.log(this.nodeName, this.nodeType, this.nodeValue); const imgsrc = e.dataTransfer.getData('text'); const uid = getUidFromImgsrc(imgsrc); $("#discord_photo").attr("src", imgsrc); $("#discord_uid").attr("value", uid); $("#discord_uname").attr("value", getUserName(uid)); if (this.nodeName == "TABLE") { if (this.id == "whitelisttable") { if (userdb["whitelist"].indexOf(uid) < 0) addUserToWhitelist(uid); } else { if (userdb["blacklist"].indexOf(uid) < 0) addUserToBlacklist(uid); } } else if (this.id == "removefromuserlist") { if (uid === undefined || uid === null || uid === "") return; remove_user_from_whitelist(uid); remove_user_from_blacklist(uid); updateUsersIconInMessage(); showWhiteBlackList(); } } } /////////////////////////////////////// /// UI /////////////////////////////////////// function initUI() { //构建用户界面 // 添加CSS insertCss(resource["css"]); // 添加UI let popover = createElm(resource["UIhtml"]); document.body.appendChild(popover); window.DebugX = popover; // 添加启动按钮 let btn = createElm(resource["startbuttonhtml"]); btn.onclick = function togglePopover() { if (popover.style.display !== 'none') { popover.style.display = 'none'; btn.style.color = 'var(--interactive-normal)'; } else { popover.style.display = 'block'; btn.style.color = '#f04747'; } }; // 关联按钮事件 const $$ = s => popover.querySelector(s); function setupButtonFunc() { const exporttofileBtn = $$('#exporttofile'); const alertTHBtn = $$('input#alertTH'); const clearwhitelistBtn = $$('button#clearwhitelist'); const clearblacklistBtn = $$('button#clearblacklist'); clearwhitelistBtn.onclick = function () { console.log("[TM][clearwhitelistBtn.onclick]") clearWhiteList(); updateUsersIconInMessage(); saveUserDB(); showWhiteBlackList(); }; clearblacklistBtn.onclick = function () { clearBlackList(); updateUsersIconInMessage(); saveUserDB(); showWhiteBlackList(); }; exporttofileBtn.onclick = function () { exportDBToFile(); }; alertTHBtn.onchange = function () { config["alertTH"] = $("#alertTH").val() / 1000.0; } let fileInput = document.getElementById("dbfileInput"); fileInput.addEventListener('change', importDBFromFile, fileInput); } setupButtonFunc(); ////////////////////////// // 构建监视器 ////////////////////////// function mountBtn() { const toolbar = document.querySelector('[class^=toolbar]'); if (toolbar) toolbar.appendChild(btn); } function mountObserver() { if ($(".membersWrap-2h-GB4").length === 1) { console.log("[TM]", "Setup moniter for member list"); member_observer.disconnect(); member_observer.observe($(".membersWrap-2h-GB4")[0], { attributes: false, childList: true, subtree: true }); } if ($(".chat-3bRxxu").length === 1) {//scrollerInner-2YIMLh console.log("[TM]", "Setup moniter for message list"); message_observer.disconnect(); message_observer.observe($(".scrollerInner-2YIMLh")[0], { attributes: false, childList: true, subtree: false }); } } const member_observer = new MutationObserver(function (_mutationsList, _observer) { getUsersFromMemberList(); }); const message_observer = new MutationObserver(function (_mutationsList, _observer) { getUsersFromMessageList(); for (let idx = 0; idx < _mutationsList.length; idx += 1) { if (!("addedNodes" in _mutationsList[idx])) { continue; } for (let nidx = 0; nidx < _mutationsList[idx].addedNodes.length; nidx++) { let addednode = _mutationsList[idx].addedNodes[nidx]; let nodeid = $(addednode).attr("id"); if (nodeid === undefined) continue; if (nodeid.startsWith('chat-messages')) { let imgnode = $(".contents-2mQqc9 > img", addednode).first(); if (imgnode.length === 0) continue; if (imgnode.get(0).tagName.toUpperCase() === "IMG") { updateOneUserIconInMessage(imgnode); } } } } }); const observer = new MutationObserver(function (_mutationsList, _observer) { if (!document.body.contains(btn)) { mountBtn(); // re-mount the button to the toolbar mountObserver(); } }); observer.observe(document.body, { attributes: false, childList: true, subtree: true }); //添加监视器 mountBtn(); //TODO::滚动右侧列表 let isMemberListRolling = 0; function wheel() { console.log("[TM] wheel"); $(".content-3YMskv").animate({ scrollTop: "0" }); if (isMemberListRolling === 1) { setTimeout(wheel, 5000); } } function start_role(element) { isMemberListRolling = 1; setTimeout(wheel, 1000); } function stop_role() { isMemberListRolling = 0; } //加载数据库 loadUserDB(); ////////////////////////////////////////// // 启动定时存储数据任务 ////////////////////////////////////////// setInterval(function () { if (db_changed !== 0) { console.log("[TM] auto save db"); saveUserDB(); } }, 60000); setInterval(updateUsersIconInMessage, 10000); //安装拖放功能 mountDropFunc(popover); //创建黑白名单显示 showWhiteBlackList(); } initUI(); })();