// ==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, '゚');
}