您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
各種便利機能をオールインワンで搭載 これお前の仕事だぞネカピン
// ==UserScript== // @name あにまん民強化パッチ // @namespace http://tampermonkey.net/ // @version 1.1.4 // @description 各種便利機能をオールインワンで搭載 これお前の仕事だぞネカピン // @author 無能の司祭A 協力:トリ虐の人、寄生荒らし愚痴部屋民 // @match https://bbs.animanch.com/* // @require http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js // @require https://greasyfork.org/scripts/545958/code/%E3%81%82%E3%81%AB%E3%81%BE%E3%82%93%E5%BA%83%E5%91%8A%E5%AE%8C%E5%85%A8%E5%89%8A%E9%99%A4.user.js // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @run-at document-end // @license MIT // @grant none // ==/UserScript== // ******************************************************* // * 定数 // ******************************************************* // 内部データキー const KEY = { STATUS : "_Status", NG_WORDS : "_NgWords", COMMON_NG_WORDS : "CommonNgWords", DEL_TIME : "DeleteTime", }; // 要素のID const ELEMENT_ID = { TIME_SET_BTN : "TimeSetButton", NG_DEL_BTN : "NgDeleteButton", AUTO_DEL_BTN : "AutoDeleteButton", BULK_DEL_BTN : "BulkDeleteButton", }; // ******************************************************* // * メンバ変数 // ******************************************************* // スレ情報 var _threadInfo = { isAdmin : false, isArchives : false, boardNo : "", }; // 現在のレス数 var _currentResCount = 1; // レスリストの更新フラグ var _refreshFlg = false; // ******************************************************* // * ローカルストレージ参照関数 // ******************************************************* const StorageUtil = { // 値を保存 set(key, value) { try { const payload = JSON.stringify(value); localStorage.setItem(key, payload); } catch (e) { console.error(`StorageUtil.set error: ${e}`); } }, // 値を取得 ※キー未存在時は空文字を返却 get(key, defaultValue = "") { try { const item = localStorage.getItem(key); if (item === null) return defaultValue; return JSON.parse(item); } catch (e) { console.error(`StorageUtil.get error: ${e}`); return defaultValue; } }, // 値を削除 remove(key) { try { localStorage.removeItem(key); } catch (e) { console.error(`StorageUtil.remove error: ${e}`); } } }; // ******************************************************* // * 初期処理 // ******************************************************* (function Initialize() { // 表示画面がスレかで分岐 if (document.URL.includes("board")) { // スレッドの各種情報を取得 var firstResTxt = document.getElementById('res1').getElementsByClassName('badge btn')[0]; if (firstResTxt) { _threadInfo.isAdmin = firstResTxt.textContent == "スレ削除"; } _threadInfo.isArchives = $('p.openform.res').text().includes("過去ログ"); _threadInfo.boardNo = document.URL.split("/")[4]; // 現在のレス数を取得 _currentResCount = $('#reslist').find('[id]').filter(function() {return /^res\d+$/.test(this.id);}).length; // スレ主の場合 if (_threadInfo.isAdmin) { // ボタンの作成 CreateButtons(); } // NGワードレス除去 RemoveRes(); // URLリンク作成 replaceUrl(); // レスリストの変更を監視 const observer = new MutationObserver(function() { RemoveRes(); replaceUrl(); }); observer.observe(document.body, { childList: true, subtree: true}); } // 不要リンクのオーバーライド NekapinFuckingAsshole(); // レス入力補助 AssistResInput(); // かつて広告が無かった頃の、君の本当の居場所へ帰りなさい const btn = document.getElementById('fixbtn'); if (btn) { btn.style.setProperty('bottom', '20px', 'important'); } })(); // ******************************************************* // * レス監視処理 // ******************************************************* const realTimeResWatcher = setInterval(() => { if (!document.URL.includes("board")) { // スレ画面でない場合はポーリング終了 clearInterval(realTimeResWatcher); } else { // reslistを自動更新 $.get(location.href, function(html) { // 最新のレスリストのみを取得 const newResList = $(html).find('#reslist'); const newResCount = newResList.find('[id]').filter(function() {return /^res\d+$/.test(this.id);}).length; // レス件数増、更新フラグON、レス重複確認のいずれかで更新 if ((newResList.length && newResCount > _currentResCount) || _refreshFlg || CheckDuplicateRes()) { // レスリスト更新フラグを初期化 _refreshFlg = false; // レスカウントを更新 _currentResCount = newResCount; // レスリストを反映 $('#reslist').html(newResList.html()); // スレ主の場合のみレスの削除処理を実行 if (_threadInfo.isAdmin) { DeleteTargetRes(); } // NGワードレス除去 RemoveRes(); // URLリンク作成 replaceUrl(); } }) } }, 1000); // ******************************************************* // * レス削除処理関数 // ******************************************************* function DeleteTargetRes() { // ステータス情報を取得 const statusInfo = StorageUtil.get(_threadInfo.boardNo + "_" + KEY.STATUS).split("/"); // ステータス情報が空(実行無し)の場合は何もせず終了 if (!statusInfo) { return; } // 現在時刻を取得し、実行時間帯外の場合は処理を行わない const now = new Date(); const nowTime = Number(String(now.getHours()) + String(now.getMinutes()).padStart(2, '0')); if (!ValidateExecuteTime(nowTime)){ return; } // 削除済レスの削除 $("#resList").find(".resbody.disabled").parent().remove(); // NGワードを取得 const ngWordsStr = StorageUtil.get(KEY.COMMON_NG_WORDS) + "/" + StorageUtil.get(_threadInfo.boardNo + KEY.NG_WORDS); const ngWords = ngWordsStr.split("/").filter(item => item); // レス番号単位で処理 $("#resList").find(".resnumber").each(function(element) { // レス番とレスの時間を取得 const resNum = Number($(this).text()); const baseResTime = $(this).parent().find(".resposted").text(); const resTime = Number(baseResTime.slice(-8, -6) + baseResTime.slice(-5, -3)); // レス番とレス時間を判定 if (resNum > statusInfo[1] && ValidateExecuteTime(resTime)) { // モードを判定 if (statusInfo[0] == "無条件") { deleteRes(_threadInfo.boardNo + "-" + resNum); console.log("無条件削除:" + _threadInfo.boardNo + "-" + resNum); } else { // レス本文 const resText = EliminateEscapeRoute($(this).parent().parent().find(".resbody").text()); // NGワードに引っかかったものを削除 for (const word of ngWords) { const ngWord = EliminateEscapeRoute(word); if (ngWord && resText.includes(ngWord)) { deleteRes(_threadInfo.boardNo + "-" + resNum); console.log("NGワード削除:" + _threadInfo.boardNo + "-" + resNum); break; } } } } }); } // ******************************************************* // * 時間判定 // ******************************************************* function ValidateExecuteTime(checkTime) { // 削除時間を取得 var delTime = StorageUtil.get(KEY.DEL_TIME, "2330/600").split("/"); // 日を跨いでいるかで判定を変更 if (delTime[0] < delTime[1]) { return checkTime >= delTime[0] && checkTime <= delTime[1]; } else { return checkTime >= delTime[0] || checkTime <= delTime[1]; } } // ******************************************************* // * レス除去処理関数 // ******************************************************* function RemoveRes() { // 削除済レスの削除 $("#resList").find(".resbody.disabled").parent().remove(); // NGワードを取得 const ngWordsStr = StorageUtil.get(KEY.COMMON_NG_WORDS) + "/" + StorageUtil.get(_threadInfo.boardNo + KEY.NG_WORDS); const ngWords = ngWordsStr.split("/").filter(item => item); $("#resList").find(".resbody").each(function(element) { // レス本文 const resText = EliminateEscapeRoute($(this).text()); // NGワードに引っかかったものを削除 for (const word of ngWords) { // NGワード const ngWord = EliminateEscapeRoute(word); // 含んでいた場合は除去 if (ngWord && resText.includes(ngWord)) { if (_threadInfo.isAdmin) { // スレ主の場合はレスをグレーに $(this).css("color", "#808080"); } else { // 除去 $(this).parent().remove(); } break; } } }) } // ******************************************************* // * hなしurlリンク化 // ******************************************************* function replaceUrl() { // hなしurl文字列の正規表現 let regExp = new RegExp("([^h])(ttps*:\/\/[^ < ]*)", "ig"); // レスの中にあるhなしurl文字列に該当する部分をリンクに置換 $("#resList").each(function(index) { if ($(this).html().match(regExp)) { $(this).html($(this).html().replace(regExp, "$1<a href='h$2' target='_blank'>h$2</a>")); } }); } // ******************************************************* // * 不要リンクのオーバーライド // ******************************************************* function NekapinFuckingAsshole() { // テキストでフィルタして書き換え // 捏造印象操作まとめ $('ul.nav.navbar-nav a').filter(function() { return $(this).text().trim() === 'あにまんch'; }) .attr('href', 'https://animanman.github.io/') .text('検索'); // RSS $('ul.nav.navbar-nav a').filter(function() { return $(this).text().trim() === 'RSS'; }) .attr('href', '') .text('NGワード') .on('click', function(e) { // リンク先への遷移をキャンセル e.preventDefault(); // NGワードの設定 SetNgWord(); }); } // ******************************************************* // * NGワード設定関数 // ******************************************************* function SetNgWord() { // スレかそれ以外かでキーを変更 var keyVal = KEY.COMMON_NG_WORDS; if (_threadInfo.boardNo) { keyVal = _threadInfo.boardNo + KEY.NG_WORDS; } // 内部データから登録済NGワードを取得 const ngWords = StorageUtil.get(keyVal); // モード選択 var input = ""; if (window.confirm("NGワードの追加を行いますか?")) { // 追加モード input = prompt('追加したいNGワードを入力してください', ''); // キャンセルや無入力時はそのまま終了 if (input === null || input === "") { return; } // 既存チェック if (ngWords.includes(input)) { alert("登録済です"); return; } // cookieの値に追加 input = ngWords + "/" + input; } else { if (window.confirm("NGワードの編集を行いますか?")) { // 編集モード input = prompt('「/」を区切り文字とし、NGワードを編集してください', ngWords); // キャンセル時はそのまま終了 if (input === null) { return; } } } // 一度配列化して空の要素の除去後、再度文字列化 input = input.split("/").filter(item => item).join("/"); // 登録 StorageUtil.set(keyVal,input); // resListを更新 _refreshFlg = true; } // ******************************************************* // * レス入力補助 // ******************************************************* function AssistResInput() { const rpStr = 'megalodon'; const $ta = $('textarea[name="text"]'); $ta.on('input', function() { // キャレット保存 const start = this.selectionStart; const end = this.selectionEnd; // 検閲回避文字に置換 var before = $(this).val(); var after = before.replace("megalodon", rpStr); // 画像ファイル after = after.replace("https://bbs.animanch.com/arc/img", "https://bbs.animanch.com/img"); if (after !== before) { $(this).val(after); // キャレット復元 this.setSelectionRange(start, end); } }); } // ******************************************************* // * ボタン作成関数 // ******************************************************* function CreateButtons() { // ツイートボタンを削除 $("#threadTitle").find(".tweet").remove(); // 時間帯設定ボタンを作成 let timeSetBtn = ""; timeSetBtn = "<button id='" + ELEMENT_ID.TIME_SET_BTN + "'>🕒</button>"; $("#threadTitle").find(".shareBtns").append(timeSetBtn); //$("#"+ELEMENT_ID.TIME_SET_BTN).css("color", "#00f"); $("#"+ELEMENT_ID.TIME_SET_BTN).on('click', function() { ClickBtnExecute("時間帯"); }); // NGワード削除ボタンを作成 let ngDelBtn = ""; ngDelBtn = "<button id='" + ELEMENT_ID.NG_DEL_BTN + "'>NG</button>"; $("#threadTitle").find(".shareBtns").append(ngDelBtn); $("#"+ELEMENT_ID.NG_DEL_BTN).css("color", "#00f"); $("#"+ELEMENT_ID.NG_DEL_BTN).on('click', function() { ClickBtnExecute("NGワード"); }); // 無条件削除ボタンを作成 let autoDelBtn = ""; autoDelBtn = "<button id='" + ELEMENT_ID.AUTO_DEL_BTN + "'>無条件</button>"; $("#threadTitle").find(".shareBtns").append(autoDelBtn); $("#"+ELEMENT_ID.AUTO_DEL_BTN).css("color", "#00f"); $("#"+ELEMENT_ID.AUTO_DEL_BTN).on('click', function() { ClickBtnExecute("無条件"); }); // 一括削除ボタンを作成 let bulkDelBtn = ""; bulkDelBtn = "<button id='" + ELEMENT_ID.BULK_DEL_BTN + "'>一括</button>"; $("#threadTitle").find(".shareBtns").append(bulkDelBtn); $("#"+ELEMENT_ID.BULK_DEL_BTN).css("color", "#00f"); $("#"+ELEMENT_ID.BULK_DEL_BTN).on('click', function() { ClickBtnExecute("一括"); }); // 実行中の場合はボタンを赤に const statusInfo = StorageUtil.get(_threadInfo.boardNo + "_" + KEY.STATUS).split("/"); if (statusInfo) { switch (statusInfo[0]) { case "NGワード": $("#"+ELEMENT_ID.NG_DEL_BTN).css("color", "#f00"); break; case "無条件": $("#"+ELEMENT_ID.AUTO_DEL_BTN).css("color", "#f00"); break; } } } // ******************************************************* // * レス自動削除ボタン押下時処理関数 // ******************************************************* function ClickBtnExecute(btnType) { // スレのステータスを取得 const statusInfo = StorageUtil.get(_threadInfo.boardNo + "_" + KEY.STATUS); // 削除時間を取得 var delTime = StorageUtil.get(KEY.DEL_TIME, "2330/600").split("/"); // ボタン毎に処理を分岐 switch (btnType) { case "時間帯": // 開始時刻を入力 var startTime = prompt('開始時刻を入力 (例)23時30分 ⇒ 2330', delTime[0]); if (!startTime) { return;} // 終了時刻を入力 var endTime = prompt('終了時刻を入力 (例)6時00分 ⇒ 600', delTime[1]); if (!endTime) { return;} // 入力チェック var pattern = /^\d{1,4}$/; if (!pattern.test(startTime) || !pattern.test(endTime) || startTime > 2400 || endTime > 2400) { alert("入力値が不正です"); return; } StorageUtil.set(KEY.DEL_TIME, startTime + "/" + endTime); alert("削除対象時間帯が" + startTime + "~" + endTime + "に設定されました"); break; case "NGワード": case "無条件": if (statusInfo) { StorageUtil.remove(_threadInfo.boardNo + "_" + KEY.STATUS); $("#"+ELEMENT_ID.NG_DEL_BTN).css("color", "#00f"); $("#"+ELEMENT_ID.AUTO_DEL_BTN).css("color", "#00f"); } else { if (window.confirm(_currentResCount + "より後の時間帯が" + delTime[0] + "~" + delTime[1] + "のレスを" + btnType + "削除します")) { StorageUtil.set(_threadInfo.boardNo + "_" + KEY.STATUS, btnType + "/" + _currentResCount); if (btnType == "NGワード") { $("#"+ELEMENT_ID.NG_DEL_BTN).css("color", "#f00"); } else { $("#"+ELEMENT_ID.AUTO_DEL_BTN).css("color", "#f00"); } // レスリストを更新 _refreshFlg = true; } } break; case "一括": var startResNo = prompt('削除開始レス番号を入力', ''); if (!startResNo) { return;} // 入力チェック var num = Number(startResNo); if (Number.isInteger(num) && num >= 2 && num <= _currentResCount) { if (window.confirm(startResNo + "以降のレスを一括削除します")) { // レス番号単位で処理 $("#resList").find(".resnumber").each(function(element) { // レス番号 const resNum = Number($(this).text()); if (resNum >= num) { // レスを一括削除 console.log("一括削除:" + _threadInfo.boardNo + "-" + resNum); deleteRes(_threadInfo.boardNo + "-" + resNum); } }) // レスリストを更新 _refreshFlg = true; } } else { alert("入力値が不正です"); return; } break; } } // ******************************************************* // * レスの重複チェック // ******************************************************* function CheckDuplicateRes() { // 出現済みIDを保持する Set const seen = new Set(); // id="reslist" 要素配下で、id が "res" で始まる全要素を取得 const items = document.querySelectorAll('#reslist [id^="res"]'); for (const el of items) { const id = el.id; // すでに Set に含まれていれば重複 if (seen.has(id)) { return true; } // 初登場の id は Set に追加 seen.add(id); } // 最後まで重複がなければ false return false; } // ******************************************************* // * 全角文字 半角変換関数 // ******************************************************* function EliminateEscapeRoute(str) { // 戻り値 let rtnStr = str.replace(/\n/, ''); // 半角変換 rtnStr = hiraToKana(rtnStr); rtnStr = toHalfWidth(rtnStr); rtnStr = kanaFullToHalf(rtnStr); // 回避文字を削除 let escapeStrAry = [" ",",",".","_","/","|","'","~","^","`"]; for (const item of escapeStrAry) { rtnStr = rtnStr.replace(item,""); } return rtnStr; } // ******************************************************* // * ひらがな カナ変換関数 // ******************************************************* function hiraToKana(str) { return str.replace(/[\u3041-\u3096]/g, function(s){ return String.fromCharCode(s.charCodeAt(0) + 0x60); }); } // ******************************************************* // * 全角英数字 半角変換関数 // ******************************************************* function toHalfWidth(str) { str = str.replace(/[A-Za-z0-9]/g, function(s) { return String.fromCharCode(s.charCodeAt(0) - 0xFEE0); }); return str; } // ******************************************************* // * 全角カナ 半角変換関数 // ******************************************************* function kanaFullToHalf(str){ let kanaMap = { "ガ": "ガ", "ギ": "ギ", "グ": "グ", "ゲ": "ゲ", "ゴ": "ゴ", "ザ": "ザ", "ジ": "ジ", "ズ": "ズ", "ゼ": "ゼ", "ゾ": "ゾ", "ダ": "ダ", "ヂ": "ヂ", "ヅ": "ヅ", "デ": "デ", "ド": "ド", "バ": "バ", "ビ": "ビ", "ブ": "ブ", "ベ": "ベ", "ボ": "ボ", "パ": "パ", "ピ": "ピ", "プ": "プ", "ペ": "ペ", "ポ": "ポ", "ヴ": "ヴ", "ヷ": "ヷ", "ヺ": "ヺ", "ア": "ア", "イ": "イ", "ウ": "ウ", "エ": "エ", "オ": "オ", "カ": "カ", "キ": "キ", "ク": "ク", "ケ": "ケ", "コ": "コ", "サ": "サ", "シ": "シ", "ス": "ス", "セ": "セ", "ソ": "ソ", "タ": "タ", "チ": "チ", "ツ": "ツ", "テ": "テ", "ト": "ト", "ナ": "ナ", "ニ": "ニ", "ヌ": "ヌ", "ネ": "ネ", "ノ": "ノ", "ハ": "ハ", "ヒ": "ヒ", "フ": "フ", "ヘ": "ヘ", "ホ": "ホ", "マ": "マ", "ミ": "ミ", "ム": "ム", "メ": "メ", "モ": "モ", "ヤ": "ヤ", "ユ": "ユ", "ヨ": "ヨ", "ラ": "ラ", "リ": "リ", "ル": "ル", "レ": "レ", "ロ": "ロ", "ワ": "ワ", "ヲ": "ヲ", "ン": "ン", "ァ": "ァ", "ィ": "ィ", "ゥ": "ゥ", "ェ": "ェ", "ォ": "ォ", "ッ": "ッ", "ャ": "ャ", "ュ": "ュ", "ョ": "ョ", "。": "。", "、": "、", "ー": "ー", "「": "「", "」": "」", "・": "・", " ": " " }; let reg = new RegExp('(' + Object.keys(kanaMap).join('|') + ')', 'g'); return str.replace(reg, function(s){ return kanaMap[s]; }).replace(/゛/g, '゙').replace(/゜/g, '゚'); }