您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
申し訳ないが連投荒らしはNG
当前为
// ==UserScript== // @name ニコニコ静画、クッキー☆連投荒らしNG // @namespace http://tampermonkey.net/ // @version 0.2 // @description 申し訳ないが連投荒らしはNG // @author cbxm // @match https://seiga.nicovideo.jp/tag/%E3%82%AF%E3%83%83%E3%82%AD%E3%83%BC%E2%98%86* // @grant GM.xmlHttpRequest // @grant GM.getValue // @grant GM.setValue // @run-at document-start // ==/UserScript== (async () => { "use strict"; ; ; class Util { //xmlString=未探査部分 static XmlToObj(xmlString) { const F = (xmlString, obj) => { //タグを抜き出す let tagMatchs = null; while (true) { tagMatchs = xmlString.match(/<([^>]+)>/); //タグがないということはそれが値になる if (tagMatchs == null) { return xmlString; } if (tagMatchs[1][tagMatchs[1].length - 1] == "/") { xmlString = xmlString.replace(/<[^>]+>([^]*)/, "$1"); } else { break; } } const tag = tagMatchs[1]; //タグの内側とその先を抜き出す const matchChildlen = []; while (true) { const matchs = xmlString.match(new RegExp(`^[^<]*<${tag}>([^]+?)<\/${tag}>([^]*)`)); if (matchs == null) { break; } matchChildlen.push(matchs[1]); xmlString = matchs[2]; } //タグあったのにマッチしなかったおかしい if (matchChildlen.length == 0) { return obj; } //そのタグが一つしかないとき、オブジェクトになる if (matchChildlen.length == 1) { //子を探す obj[tag] = F(matchChildlen[0], {}); } //そのタグが複数あるとき、配列になる if (matchChildlen.length > 1) { obj = []; for (let i = 0; i < matchChildlen.length; i++) { //子を探す obj[i] = F(matchChildlen[i], {}); } } //兄弟を探す F(xmlString, obj); return obj; }; //初期化で<xml>を取り除く xmlString = xmlString.replace(/\s*<[^>]+>([^]+)/, "$1"); return F(xmlString, {}); } static HtmlToDocument(str) { const parser = new DOMParser(); return parser.parseFromString(str, "text/html"); } static HtmlToChildNodes(str) { return this.HtmlToDocument(str).body.childNodes; } static Wait(ms) { return new Promise(r => setTimeout(r, ms)); } } ; class Fetcher { static GMFetchText(url) { return new Promise(r => { GM.xmlHttpRequest({ url: url, method: "GET", onload: (response) => { r(response.responseText); } }); }); } static async FetchIllustDatas(ids) { if (ids.length == 0) { return { illusts: [], userIds: [] }; } const url = `http:\/\/seiga.nicovideo.jp/api/illust/info?id_list=${ids.join()}`; const res = await this.GMFetchText(url); const obj = Util.XmlToObj(res); const list = Array.isArray(obj.response.image_list) ? obj.response.image_list : [obj.response.image_list.image]; const illusts = []; for (let i = 0; i < list.length; i++) { illusts[i] = { id: list[i].id, created: new Date(list[i].created) }; } return { illusts: illusts, userIds: list.map(l => l.user_id) }; } static async FetchUserName(userId) { const url = "http://seiga.nicovideo.jp/api/user/info?id=" + userId; const json = Util.XmlToObj(await this.GMFetchText(url)); return json.response.user.nickname; } static async FetchUserId(illustId) { const url = "https://seiga.nicovideo.jp/api/illust/info?id=im" + illustId; const resultText = await this.GMFetchText(url); const json = Util.XmlToObj(resultText); return json.response.image.user_id; } } ; class Storage { constructor(storageName) { this.storageName = ""; this.storageName = storageName; } async GetStorageData(defaultValue = null) { const text = await GM.getValue(this.storageName, null); return text != null ? JSON.parse(decodeURIComponent(text)) : defaultValue; } async SetStorageData(data) { await GM.setValue(this.storageName, encodeURIComponent(JSON.stringify(data))); } } ; class Observer { static Wait(predicate, parent = document, option = null) { return new Promise(r => { if (option == null) { option = { childList: true, subtree: true }; } const mutationObserver = new MutationObserver((mrs) => { if (predicate(mrs)) { mutationObserver.disconnect(); r(mrs); return; } }); mutationObserver.observe(parent, option); }); } ; static WaitAddedNode(predicate, parent, option = null) { return new Promise(r => { if (option == null) { option = { childList: true, subtree: true }; } const mutationObserver = new MutationObserver((mrs) => { //console.log(document.head.innerHTML); //console.log(document.body.innerHTML); for (let node of mrs) { node.addedNodes.forEach(added => { //console.log(added); if (predicate(added)) { mutationObserver.disconnect(); r(added); return; } }); } }); mutationObserver.observe(parent, option); }); } ; static async DefinitelyGetElementById(id, parent = document, option = null) { const e = document.getElementById(id); if (e != null) { return e; } return this.WaitAddedNode(e => e.id != null && e.id == id, parent, option); } //getElementsByClassNameをつかうけど単体なので注意 static async DefinitelyGetElementByClassName(className, parent = document, option = null) { const e = document.getElementsByClassName(className)[0]; if (e != null) { return e; } return this.WaitAddedNode(e => e.className != null && e.className == className, parent, option); } //getElementsByTagNameをつかうけど単体なので注意 static async DefinitelyGetElementByTagName(tagName, parent = document, option = null) { tagName = tagName.toUpperCase(); const e = document.getElementsByTagName(tagName)[0]; if (e != null) { return e; } return this.WaitAddedNode(e => e.tagName != null && e.tagName == tagName, parent, option); } } ; class Main { constructor() { this.cache = []; this.storage = new Storage("NICONICO_RENTO_ARASI_NG_DATA_CACHE"); } async GetCache() { this.cache = await this.storage.GetStorageData([]); } async ResetCache() { this.cache = []; await this.SetCache(); } ResetStatus() { for (let c of this.cache) { if (c.status != "LNG" && c.status != "LOK") { c.status = "OK"; this.ChackArasi(c); } } } async SetCache() { await this.storage.SetStorageData(this.cache); } GetUser(id) { for (let c of this.cache) { for (let illust of c.illusts) { if (illust.id == id) { return c; } } } return undefined; } ChackArasi(user) { if (user.illusts.length == 0 || user.status == "LNG" || user.status == "LOK" || user.status == "NG") { return; } for (let illust of user.illusts) { if (typeof illust.created == "string") { illust.created = new Date(illust.created); } } //新しい順 const sorted = user.illusts.sort((a, b) => b.created.getTime() - a.created.getTime()); for (let i = 0; i < sorted.length; i++) { const currentDate = sorted[i].created; let j = i + 1; let count = 0; while (true) { if (j >= sorted.length || currentDate.getTime() - sorted[j].created.getTime() > 7200000) { break; } j++; count++; } if (count >= 3) { user.status = "NG"; return; } } } ; GetIllustIds() { const illustIdElements = document.getElementsByClassName("item_list")[0].getElementsByTagName("a"); const illustIds = []; for (let i = 0; i < illustIdElements.length; i++) { const idMatchs = illustIdElements[i].href.match(/im(\d+)/); if (idMatchs == null) { continue; } const id = idMatchs[1]; illustIds.push(id); } return illustIds; } async Main() { const pageCountText = (await Observer.DefinitelyGetElementByClassName("page_count")).textContent; if (pageCountText == null) { return; } const pageCountMatchs = pageCountText.match(/(\d+)〜(\d+)/); if (pageCountMatchs == null) { return; } const count = parseInt(pageCountMatchs[2]) - parseInt(pageCountMatchs[1]) + 1; await Observer.Wait(() => document.getElementsByClassName("list_item").length >= count); const removedNGs = []; const illustElements = Array.from(document.getElementsByClassName("list_item")); const illustIds = this.GetIllustIds(); const unknownIllustIds = []; const unknownElements = []; for (let i = 0; i < illustIds.length; i++) { const user = this.GetUser(illustIds[i]); if (user != undefined) { //既存のものはもう判定 if (user.status == "NG" || user.status == "LNG") { if (user.status == "NG") { removedNGs.push(illustElements[i]); } illustElements[i].remove(); } continue; } unknownIllustIds.push(illustIds[i]); unknownElements.push(illustElements[i]); } console.log("Fetch", unknownIllustIds); const r = await Fetcher.FetchIllustDatas(unknownIllustIds); const newIllusts = r.illusts; const newUserIds = r.userIds; const updateUsers = new Set(); //キャッシュに追加 for (let i = 0; i < newIllusts.length; i++) { const user = this.cache.find((c) => c.userId == newUserIds[i]); if (user != null) { user.illusts.push(newIllusts[i]); updateUsers.add(user); continue; } const newUser = { illusts: [newIllusts[i]], userId: newUserIds[i], status: "OK" }; this.cache.push(newUser); updateUsers.add(newUser); } //更新 for (let c of updateUsers.values()) { this.ChackArasi(c); } //消す for (let i = 0; i < newIllusts.length; i++) { const user = this.cache.find((c) => c.userId == newUserIds[i]); if (user && (user.status == "NG" || user.status == "LNG")) { if (user.status == "NG") { removedNGs.push(unknownElements[i]); } unknownElements[i].remove(); } } const illustListElements = document.getElementsByClassName("item_list")[0]; for (let removedNG of removedNGs) { removedNG.getElementsByTagName("img")[0].style.filter = "brightness(0.3)"; illustListElements.appendChild(removedNG); } } } ; const main = new Main(); await main.GetCache(); //main.ResetStatus(); // main.ResetCache(); await main.Main(); await main.SetCache(); })(); //キャッシュは最小限に(ユーザーID、isArasi、イラスト「ID、投稿時間」) ok //投稿時間は必要になったときにパース ok //多分荒らしじゃない="OK"、多分荒らし="NG"、絶対荒らしじゃない=”LOK”、絶対荒らし="LNG" ok //イラスト右下にホワイトリスト、ブラックリストボタンをつける //設定項目:「何時間」以内に「何回」投稿が「何回」あった // ホワイトリスト、ブラックリスト // キャッシュリセット // NGリセット //# sourceMappingURL=script.js.map