あにまん民強化パッチ

各種便利機能をオールインワンで搭載 これお前の仕事だぞネカピン

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