Greasy Fork

Greasy Fork is available in English.

使用 "display:none;" 隐藏 Twitter(曾用名: 𝕏)的印象收益骗子。

这是一个可以隐藏 Twitter 垃圾信息的工具。

当前为 2024-01-09 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                Twitter(旧:𝕏)のインプレッション小遣い稼ぎ野郎どもをdisplay:none;するやつ
// @name:ja             Twitter(旧:𝕏)のインプレッション小遣い稼ぎ野郎どもをdisplay:none;するやつ
// @name:en             Hide the Twitter (formerly: 𝕏) impression-earning scammers with "display:none;"
// @name:zh-CN          使用 "display:none;" 隐藏 Twitter(曾用名: 𝕏)的印象收益骗子。
// @name:zh-TW          使用 "display:none;" 隱藏 Twitter(曾用名: 𝕏)的印象詐騙者。
// @namespace           https://snowshome.page.link/p
// @version             1.4.10
// @description         Twitterのインプレゾンビを非表示にするツールです。
// @description:ja      Twitterのインプレゾンビを非表示にするツールです。
// @description:en      This is a tool to hide spam on Twitter.
// @description:zh-CN   这是一个可以隐藏 Twitter 垃圾信息的工具。
// @description:zh-TW   這是一個可以隱藏 Twitter 垃圾訊息的工具。
// @author              tromtub(snows)
// @license             You can modify as long as you credit me
// @match               https://twitter.com/*
// @match               http://twitter.com/*
// @icon                data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB7ElEQVR4Ae1XMZLCMAwUdw0ldJQ8ATpKnkBJByUd8ALyA/gBdJTQUtHS8QT4AaRM5ctmThmfogQ75CYNmhGTbGJr45Vk0yAiQzXaF9VsHwIZAofDgYwxqo9GI/K16/X6cqyxvdVqmdvtZh6PhwmCIHXcw7vdrpFj8ny9XhsYxhe8lwWHw2EycLFYpNh0Ok2w8/nsFHy1WrkE1wnAN5tNMkGv10ux3W6XIab5fD5P3ovldCGrP2Ap4LiW8uRJAcIwe1wpArYU0FJimhQgxaQ9cqX4BZYCgSVmS8HBfRP1JQEsY1xKGSmAcTC+l0QrIWDraicVMBBA4O1265ScpQnAMbkMwphjub1HAI7EkxoDK7n0/gQQGATsCmDMo+z++Hf8E5CjPZ9PiqKIZrMZhWFIl8slxcbjMTWbTTqdTuRrXoz5i2WXRIL+WxWw2+Uml13rnJUT4K9E9nMFaF3SxiojoO1u2rJzl4z3/+oIcHBMLiUp2rDe3ozg+BIYtNee87KjGzLGndPx7JD/0K7xog2Gl30ymaSY1jm9CPhsrXnnBK1zOhHgCWWtF7l2TtA6p3S1E+73exoMBrRcLul4PJKL3e93arfbSUeMA1O/36eYPHU6nWQu7pyaqRlfZnezV05anhSN34va7PPXrHYCP+VaTG3LBV1KAAAAAElFTkSuQmCC
// @supportURL          https://github.com/hi2ma-bu4/X_impression_hide
// @grant               GM.addStyle
// @grant               GM_setValue
// @grant               GM_getValue
// @grant               GM_deleteValue
// @grant               GM.registerMenuCommand
// @run-at              document-idle
// @noframes
// ==/UserScript==


/*
Twitter(旧:𝕏)のインプレッション小遣い稼ぎ野郎どもをdisplay:none;するやつ

略して、

インプレゾンビをnoneするやつ

*/
/*
コピー・改変してもいいけど、
「tromtub(snows)」は変えないでね。

*/
/* todo
・検知率を上げる
    ・連投の検知
    ・あやしい日本語の検知(多分自分の実力じゃ無理)
    ・フィルターをもっと有能に
・誤検知を減らす(今はまだいい?)
・クイックミュートボタンを作成
・クイックブロックボタンを作成
・whitelist_filterの実装
    ・名前
    ・内容
・blacklist_filterの拡張
    ・名前
・blacklist_idを保存するかの設定
・他人の引用ツイートでの言語フィルターを作成
・menuのresize:both;を左下に
・menuをもっと見やすく(たすけて)
・gifをブロック
・正規表現などの最適化
・英語メニュー作成
・軽量化
・kiwi browserで動くようにする
*/


(function () {
    'use strict';

    const DEBUG = false;

    // 初期値(定数)
    const VISIBLE_LOG = true;
    const ONESELF_RETWEET_BLOCK = true;
    const VERIFY_BLOCK = false;
    const VERIFY_ONRY_FILTER = false;

    const BLACK_TEXT_REG = `!# 行頭が"!#"だとコメント

!# プロフィールメッセージを異常に推してる人
(はじめまして|こんにち[はわ]).*?ぷろふ

!# chatGPTが時々やらかす濁点半濁点問題を流用
[\\u3099\\u309a]

!# chatGPTのエラーメッセージを取り敢えず対処
^申し訳ありません.*?(過激な表現や性的な内容|不適切なコンテンツや言葉).*?他の(質問や話題|トピックで質問)があれば.*?。$

!# タイ語のハッシュタグを含む場合
#[\\u0E00-\\u0F7F]+

!# アラビア語のみで構成
^[\\u0600-\\u07FF]+$
`;

    const ALLOW_LANG = "ja|en|qme|und";
    const MAX_SAVE_TEXT_SIZE = 80;
    const MIN_SAVE_TEXT_SIZE = 8;
    const MSG_RESEMBLANCE = 0.8;
    const MAX_SAVE_LOG_SIZE = 100;
    const MAX_HASHTAG_COUNT = 6;

    const PRO_NAME = "X_impression_hide";
    const BODY_OBS_TIMEOUT = 3000;
    const SETTING_SAVE_KEY = PRO_NAME + "_json";

    const PARENT_CLASS = PRO_NAME + "_parent";
    const CHECK_CLASS = PRO_NAME + "_check";
    const HIDE_CLASS = PRO_NAME + "_none";
    const LOG_CLASS = PRO_NAME + "_log";
    const EX_MENU_ID = PRO_NAME + "_menu";
    const EX_MENU_OPEN_CLASS = EX_MENU_ID + "_open";
    const EX_MENU_ITEM_BASE_ID = EX_MENU_ID + "_item_";

    const OBS_QUERY = "section > div > div:has(article)";
    const RE_QUERY = `div:has(div > div > article):not(.${CHECK_CLASS})`;
    const NAME_SPACE_QUERY = `[data-testid="User-Name"]`;
    const NAME_QUERY = `:not(span) > span > span`;
    const ID_QUERY = "div > span:not(:has(span))";
    const IMAGE_QUERY = "a img";

    const BASE_CSS = /* css */ `
#${EX_MENU_ID} {
    display: none;
    position: fixed;
    top: 0;
    right: 0;
    z-index: 2000;
}
/* 積み防止 */
#${EX_MENU_ID}.${EX_MENU_OPEN_CLASS} {
    display: block !important;
    visibility: visible !important;
}

#${EX_MENU_ID} > div {
    position: relative;
    overflow-y: scroll;
    overscroll-behavior: contain;
    width: 50vh;
    min-width: 200px;
    max-width: 90vw;
    height: 50vh;
    min-height: 200px;
    max-height: 90vh;
    resize: both;
    border: solid #000 2px;
    background: #fafafaee;
}

#${EX_MENU_ITEM_BASE_ID}__btns {
    position: sticky;
    right: 0;
    bottom: 0;
    text-align: right;
}
`;
    const CUSTOM_CSS = /* css */ `
/* ツイート非表示 */
.${HIDE_CLASS}:has(.${LOG_CLASS} input[type=checkbox]:not(:checked)) > div:not(.${LOG_CLASS}) {
    display: none;
}

/* 検出内容の表示設定 */
.${HIDE_CLASS} {
    background: #aaaa;
}

/* 以下非表示後の表示内容設定 */
.${LOG_CLASS} {
    display: flex;
    justify-content: space-between;
}

.${LOG_CLASS} input[type=checkbox] {
    display: none;
}
.${LOG_CLASS} label {
    cursor: pointer;
}
.${LOG_CLASS} label:hover {
    text-decoration: underline;
}

/* メニュー表示設定 */
#${EX_MENU_ID} textarea {
    width: 95%;
    resize: vertical;
    height: 8em;
    max-height: 25em;
    tab-size: 4;
    white-space: nowrap;
}
#${EX_MENU_ID} input[type=checkbox] + span::after {
    content: "無効";
}
#${EX_MENU_ID} input[type=checkbox]:checked + span::after {
    content: "有効";
}

#${EX_MENU_ID} details {
    margin-top: 1em;
}

.${EX_MENU_ITEM_BASE_ID}_name {
    font-size: 1.3em;
    margin-bottom: 3px;
    margin-left: 2px;
}
.${EX_MENU_ITEM_BASE_ID}_name + p {
    font-size: .8em;
    margin: 0 4px;
}
`;

    const SETTING_LIST = {
        visibleLog: {
            name: "非表示ログを表示",
            explanation: `非表示にしたログを画面から消します。
画面が平和になりますが、投稿を非表示にされた理由・元投稿が確認出来なくなります。`,
            data: VISIBLE_LOG,
            _data: VISIBLE_LOG,
            input: "checkbox",
        },
        blackTextReg: {
            name: "禁止する表現",
            explanation: `非表示にするテキストを指定します。
記述方法は正規表現(/の間部分)で記述します。
(半角カタカナ、カタカナはひらがなに自動変換されます)
(全角英数字は半角英数字に、改行文字は半角スペースに自動変換されます)`,
            data: BLACK_TEXT_REG,
            _data: BLACK_TEXT_REG,
            input: "textarea",
        },
        allowLang: {
            name: "許可する言語",
            explanation: `許可する言語を指定します。
記述方法は正規表現(/の間部分)で記述します。`,
            data: ALLOW_LANG,
            _data: ALLOW_LANG,
            input: "text",
        },
        oneselfRetweetBlock: {
            name: "自身の引用禁止",
            explanation: `自身を引用ツイートする投稿を非表示にします。`,
            data: ONESELF_RETWEET_BLOCK,
            _data: ONESELF_RETWEET_BLOCK,
            input: "checkbox",
        },
        verifyBlock: {
            name: "認証アカウント禁止",
            explanation: `認証済アカウントを無差別にブロックします。`,
            data: VERIFY_BLOCK,
            _data: VERIFY_BLOCK,
            input: "checkbox",
        },
        verifyOnryFilter: {
            name: "認証アカウントのみ判定",
            explanation: `認証済アカウントのみを検知の対象にします。
通常アカウントや認証マークの無いアカウントはブロックされなくなります。`,
            data: VERIFY_ONRY_FILTER,
            _data: VERIFY_ONRY_FILTER,
            input: "checkbox",
        },
        maxHashtagCount: {
            name: "ハッシュタグの上限数",
            explanation: `1つの投稿内でのハッシュタグの使用上限数を指定します。`,
            data: MAX_HASHTAG_COUNT,
            _data: MAX_HASHTAG_COUNT,
            input: "number",
            min: 1,
        },
        msgResemblance: {
            name: "文章類似度許可ライン",
            explanation: `コピペ文章かを判別する為の基準値を指定します。`,
            data: MSG_RESEMBLANCE,
            _data: MSG_RESEMBLANCE,
            input: "number",
            min: 0,
            max: 1,
            step: 0.01,
        },
        maxSaveTextSize: {
            name: "比較される最大テキストサイズ",
            explanation: `コピペ投稿の文章比較の最大文字数を指定します。
値を大きくするほど誤検知率は減り、検知率も減ります。
(投稿の文字数が最大値以下の場合、この値は使用されません)`,
            data: MAX_SAVE_TEXT_SIZE,
            _data: MAX_SAVE_TEXT_SIZE,
            input: "number",
            min: 0,
        },
        minSaveTextSize: {
            name: "一時保存・比較される最小テキストサイズ",
            explanation: `比較用文章の最小文字数を指定します。
値が大きくするほど誤検知率は減り、検知率も減ります。
([比較される最大テキストサイズ]より大きい場合、比較処理は実行されません)`,
            data: MIN_SAVE_TEXT_SIZE,
            _data: MIN_SAVE_TEXT_SIZE,
            input: "number",
            min: 0,
        },
        maxSaveLogSize: {
            name: "一時保存される投稿の最大数",
            explanation: `比較用文章の保持数を指定します。
値が小さいほど処理は軽くなりますが、検知率が減ります`,
            data: MAX_SAVE_LOG_SIZE,
            _data: MAX_SAVE_LOG_SIZE,
            input: "number",
            min: 1,
        },
        bodyObsTimeout: {
            name: "ページ更新検知用処理待機時間(ms)",
            explanation: `ページ更新を検知する際の検知の更新間隔を指定します。
値が大きいほど処理が軽くなりますが、非表示にする初速が落ちる可能性あります。`,
            data: BODY_OBS_TIMEOUT,
            _data: BODY_OBS_TIMEOUT,
            input: "number",
            min: 100,
            advanced: true,
        },
        customCss: {
            name: "ページ適用css設定",
            explanation: `ページへ適用するcssを指定します。`,
            data: CUSTOM_CSS,
            _data: CUSTOM_CSS,
            input: "textarea",
            advanced: true,
        },
        resetSetting: {
            name: "設定のリセット",
            explanation: `設定項目をリセットします。
(ページがリロードされます)
<span style="color: #f00">実行すると設定は復元出来ません!!!</span>`,
            value: "リセットする",
            input: "button",
            advanced: true,
        },
    };

    // グローバル変数
    let parentDOM = null;
    let parent_observer = null;
    let oldUrl = location.href;
    let parent_id = null;
    let exMenuDOM = null;

    const blacklist_reg = [];
    let allowLang_reg = /.*/;
    const msgDB = [];
    const msgDB_id = new Set();
    const blacklist_id = new Set();

    let levenshteinDistanceUseFlag = true;

    // ページ変更確認に使用
    let body_isReservation = false;
    let body_isWait = false;

    const kanaMap = {
        ガ: "ガ", ギ: "ギ", グ: "グ", ゲ: "ゲ", ゴ: "ゴ",
        ザ: "ザ", ジ: "ジ", ズ: "ズ", ゼ: "ゼ", ゾ: "ゾ",
        ダ: "ダ", ヂ: "ヂ", ヅ: "ヅ", デ: "デ", ド: "ド",
        バ: "バ", ビ: "ビ", ブ: "ブ", ベ: "ベ", ボ: "ボ",
        パ: "パ", ピ: "ピ", プ: "プ", ペ: "ペ", ポ: "ポ",
        ヴ: "ヴ", ヷ: "ヷ", ヺ: "ヺ",
        ア: "ア", イ: "イ", ウ: "ウ", エ: "エ", オ: "オ",
        カ: "カ", キ: "キ", ク: "ク", ケ: "ケ", コ: "コ",
        サ: "サ", シ: "シ", ス: "ス", セ: "セ", ソ: "ソ",
        タ: "タ", チ: "チ", ツ: "ツ", テ: "テ", ト: "ト",
        ナ: "ナ", ニ: "ニ", ヌ: "ヌ", ネ: "ネ", ノ: "ノ",
        ハ: "ハ", ヒ: "ヒ", フ: "フ", ヘ: "ヘ", ホ: "ホ",
        マ: "マ", ミ: "ミ", ム: "ム", メ: "メ", モ: "モ",
        ヤ: "ヤ", ユ: "ユ", ヨ: "ヨ",
        ラ: "ラ", リ: "リ", ル: "ル", レ: "レ", ロ: "ロ",
        ワ: "ワ", ヲ: "ヲ", ン: "ン",
        ァ: "ァ", ィ: "ィ", ゥ: "ゥ", ェ: "ェ", ォ: "ォ",
        ッ: "ッ", ャ: "ャ", ュ: "ュ", ョ: "ョ",
        "。": "。", "、": "、", ー: "ー",
        "「": "「", "」": "」", "・": "・",
    };
    const kanaReg = new RegExp("(" + Object.keys(kanaMap).join("|") + ")", "g");
    const spaceRegList = [
        /[  \t]/gu,
        /[\u00A0\u00AD\u034F\u061C]/gu,
        /[\u115F\u1160\u17B4\u17B5\u180E]/gu,
        // \u200Dが合成時に消失したため部分対処
        /[\u2000-\u200C\u200E-\u200F\u202F\u205F\u2060-\u2064\u206A-\u206F\u2800]/gu,
        /[\u3000\u3164]/gu,
        /[\uFEFF\uFFA0]/gu,
        /[\u{1D159}\u{1D173}-\u{1D17A}]/gu,
    ];
    const othToHiraRegList = [
        [kanaReg, (ch) => kanaMap[ch]],
        [/゙/g, "゛"],
        [/゚/g, "゜"],
        [/[ア-ヺ]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0x60)],
        [/[!-~]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xfee0)],
        [/[”“″‶〝‟]/gu, '"'],
        [/[’‘′´‛‵']/gu, "'"],
        [/¥/g, "\\"],
        [/〜/g, "~"],
    ];
    const CrLfReg = /[\r\n]/gu;
    const spaceReg = / /g;

    log("起動中...");

    init();

    const menu_command_id_1 = GM.registerMenuCommand("設定を開く", function (event) {
        menuOpen();
    }, {
        accessKey: "s",
        autoClose: true
    });

    function init() {
        // 親id取得
        setParentId();

        {
            // 設定呼び出し
            let saveData = GM_getValue(SETTING_SAVE_KEY, null);
            if (saveData != null) {
                log("設定読み込み...開始");
                let jsonData = null;
                try {
                    jsonData = JSON.parse(saveData);
                }
                catch (e) {
                    console.error(e);
                }
                if (jsonData != null) {
                    for (let key in SETTING_LIST) {
                        if (key in jsonData) {
                            SETTING_LIST[key].data = jsonData[key];
                        }
                    }
                    log("設定読み込み...完了");
                }
            }
        }

        {
            // フィルター正規表現設定
            let spText = SETTING_LIST.blackTextReg.data
                .replace(/\r\n/g, "\n")
                .replace(/\r/g, "\n")
                .split("\n");

            for (let row of spText) {
                if (row.trim().length && !row.startsWith("!#")) {
                    try {
                        blacklist_reg.push([new RegExp(reRegExpStr(row), "uim"), row]);
                    }
                    catch (e) {
                        console.error(`[${PRO_NAME}]`, e);
                        SETTING_LIST.blackTextReg.isError = true;
                    }
                }
            }

            // 投稿の言語を制限
            try {
                allowLang_reg = new RegExp(SETTING_LIST.allowLang.data.trim(), "i");
            }
            catch (e) {
                console.error(e);
                SETTING_LIST.allowLang.isError = true;
            }
        }

        // 画面移管時対応
        const body_observer = new MutationObserver(bodyChangeEvent);
        body_observer.observe(document.body, {
            subtree: true,
            childList: true,
        });

        // カスタムcss設定
        try {
            GM.addStyle(BASE_CSS);
            GM.addStyle(SETTING_LIST.customCss.data);
        }
        catch (e) {
            console.error(e);
            SETTING_LIST.customCss.isError = true;
        }

        // 文章類似比較を実行するか
        if (!SETTING_LIST.maxSaveTextSize.data
            || SETTING_LIST.maxSaveTextSize.data < SETTING_LIST.minSaveTextSize.data) {
            levenshteinDistanceUseFlag = false;
        }

        card_init();
    }

    function menu_init() {
        let w_exMenuDOM = document.createElement("div");
        w_exMenuDOM.innerHTML = /* html */ `
<small style="color:#d00">変更の保存をした場合、ページを更新してください。</small>`;
        let advanceDOM = document.createElement("details");
        advanceDOM.innerHTML = /* html */ `
<summary>高度な設定</summary>`;
        for (let key in SETTING_LIST) {
            let item = SETTING_LIST[key];
            // 入力欄作成
            let inputType = item?.input ?? ""
            let input_elem = document.createElement("input");
            input_elem.type = inputType;
            let add_elem = null;
            switch (inputType) {
                case "text":
                    input_elem.value = item.data;
                    break;
                case "number":
                    input_elem.value = item.data;
                    if (item?.min != null) {
                        input_elem.min = item.min;
                    }
                    if (item?.max != null) {
                        input_elem.max = item.max;
                    }
                    if (item?.step != null) {
                        input_elem.step = item.step;
                    }
                    break;
                case "checkbox":
                    input_elem.checked = item?.data ?? false;
                    add_elem = document.createElement("span");
                    break;
                case "radiobutton":
                    // 使ってない
                    break;
                case "button":
                    input_elem.value = item.value;
                    break;
                case "textarea":
                    input_elem = document.createElement("textarea");
                    input_elem.value = item.data;
                    break;
                default:
                    console.warn("対応していない形式", item);
                    continue;
            }
            input_elem.id = EX_MENU_ITEM_BASE_ID + key;

            // 項目を囲うdiv
            let div = document.createElement("div");
            // 名前
            if (item?.name) {
                let name_elem = document.createElement("p");
                name_elem.innerText = item.name;
                name_elem.classList.add(EX_MENU_ITEM_BASE_ID + "_name")
                div.appendChild(name_elem);
            }
            // 説明
            if (item?.explanation) {
                let ex_elem = document.createElement("p");
                ex_elem.innerHTML = item.explanation.replace(/\n/g, "<br/>");
                div.appendChild(ex_elem);
            }

            div.appendChild(input_elem);
            if (add_elem) {
                div.appendChild(add_elem);
            }
            if (item.advanced) {
                advanceDOM.appendChild(div);
            }
            else {
                w_exMenuDOM.appendChild(div);
            }
        }
        w_exMenuDOM.appendChild(advanceDOM);
        // 画面右下のボタン系
        {
            let div = document.createElement("div");
            div.id = EX_MENU_ITEM_BASE_ID + "__btns";
            let btn_elem = document.createElement("input");
            btn_elem.type = "button";
            btn_elem.value = "保存";
            btn_elem.id = EX_MENU_ITEM_BASE_ID + "__save";
            div.appendChild(btn_elem);
            btn_elem = document.createElement("input");
            btn_elem.type = "button";
            btn_elem.value = "閉じる";
            btn_elem.id = EX_MENU_ITEM_BASE_ID + "__close";
            div.appendChild(btn_elem);
            w_exMenuDOM.appendChild(div);
        }
        exMenuDOM = document.createElement("div");
        exMenuDOM.id = EX_MENU_ID;
        exMenuDOM.appendChild(w_exMenuDOM);
    }

    function card_init() {
        log("初期化中...")

        // 表示待機
        waitForKeyElements(OBS_QUERY, function () {
            // (投稿リストの)親を取得
            parentDOM = document.querySelector(OBS_QUERY);
            if (parentDOM == null) {
                log(`(${OBS_QUERY})が見つけれませんでした`)
                return;
            }
            parentDOM.classList.add(PARENT_CLASS);

            // DOM変更検知(イベント)
            parent_observer = new MutationObserver(records => {
                records.forEach(record => {
                    let addNodes = record.addedNodes;
                    if (addNodes.length) {
                        addNodes.forEach(addNode => {
                            cardCheck(addNode)
                        });
                    }
                });
            });
            parent_observer.observe(parentDOM, {
                childList: true,
                //subtree: true,
            });

            // 先頭部分が取得出来ていないので再実行
            parentDOM.querySelectorAll(RE_QUERY).forEach(elem => {
                cardCheck(elem)
            });
        });
    }

    // メッセージの親を取得
    function setParentId() {
        let url = oldUrl.replace(/https?:\/\/twitter.com/, "");
        if (url.startsWith("/")) {
            let uid = url.replace(/\?/, "").split("/")?.[1];
            if (uid && uid != "home" && uid != "search") {
                uid = "@" + uid;
                log(`親投稿者: ${uid}`);
                parent_id = uid;
                // 気分で消しとく
                blacklist_id.delete(uid);
            }
        }
    }

    // 画面移管対応
    function bodyChangeEvent() {
        // 更新過多で重くなるので同時実行禁止
        if (body_isWait) {
            body_isReservation = true;
            return;
        }
        body_isWait = true;
        // 反応しない場合用に一瞬待機
        setTimeout(function () {
            // URL変更時のみ
            if (oldUrl !== location.href) {
                oldUrl = location.href;
                setParentId();
                if (!document.getElementsByClassName(PARENT_CLASS)?.[0]) {
                    if (parent_observer) {
                        parent_observer.disconnect();
                        parent_observer = null;
                    }
                    card_init()
                }
            }
            body_isWait = false;
            // 一応再実行
            if (body_isReservation) {
                body_isReservation = false;
                bodyChangeEvent();
            }
        }, SETTING_LIST.bodyObsTimeout.data);
    }

    // 処理対象判定&処理実行
    function cardCheck(card_elem) {
        // 処理は1度のみ
        if (card_elem.classList.contains(CHECK_CLASS)) {
            return;
        }
        card_elem.classList.add(CHECK_CLASS)

        let messageData = {
            base_url: oldUrl,
            card: card_elem,
            verify: false,
            attach_img: false,
            reTweet: null,
            _nsOneLoadFlag: false,
        };

        // 処理対象か判定
        let article = card_elem?.firstChild?.firstChild?.firstChild;
        if (article?.tagName != "ARTICLE") {
            return;
        }

        // ユーザー名などの空間取得
        let nameSpace_div = article.querySelectorAll(NAME_SPACE_QUERY);
        nameSpace_div.forEach(div => {
            // 2回目以降はリツイート
            if (messageData._nsOneLoadFlag) {
                messageData.reTweet = {
                    verify: false,
                };
            }

            // ユーザー名(id)取得
            let name_span = div.querySelector(NAME_QUERY);
            if (messageData._nsOneLoadFlag) {
                messageData.reTweet.name = name_span?.innerText
            }
            else {
                messageData.name = name_span?.innerText;
            }

            // id取得(ついでに認証マーク判定)
            let id_span = div.querySelectorAll(ID_QUERY);
            id_span.forEach(span => {
                let fc = span?.firstChild;
                if (fc?.tagName == "svg") {
                    if (messageData._nsOneLoadFlag) {
                        messageData.reTweet.verify = true;
                    }
                    else {
                        messageData.verify = true;
                    }
                }
                else {
                    let tmp = span.innerText.trim();
                    if (tmp.startsWith("@")) {
                        if (messageData._nsOneLoadFlag) {
                            messageData.reTweet.id = tmp;
                        }
                        else {
                            messageData.id = tmp;
                        }
                    }
                }
            });

            messageData._nsOneLoadFlag = true;
        });

        // 投稿時刻
        let time_elem = article.querySelector("time");
        if (!time_elem) {
            return;
        }
        try {
            messageData.dateTime = new Date(time_elem.dateTime);
        }
        catch (e) {
            console.error(e);
            return;
        }
        if (messageData.dateTime.toString() == "Invalid Date") {
            log("日付変換失敗");
            return;
        }

        // 画像を添付しているか
        let attach_img = article.querySelectorAll(IMAGE_QUERY);
        if (attach_img) {
            for (let img of attach_img) {
                if (/^https?:\/\/pbs.twimg.com\/media\//.test(img.href)) {
                    messageData.attach_img = true;
                    break;
                }
            }
        }

        // メッセージ取得
        let text_divs = article.querySelectorAll("div[lang]");
        let text_div = text_divs?.[0];
        if (!text_div) {
            return;
        }

        let fullStr = "";
        let str = "";
        let emojiLst = [];
        let tmp;
        text_div.childNodes.forEach(elem => {
            switch (elem.tagName) {
                case "SPAN":
                    tmp = elem.innerText
                    str += tmp;
                    fullStr += tmp;
                    break;
                case "IMG":
                    tmp = elem.alt;
                    emojiLst.push(tmp);
                    fullStr += tmp;
                    break;
                default:
                    break;
            }
        });

        messageData.full = fullStr;
        messageData.str = str;
        messageData.emoji = emojiLst;
        messageData.cleanStr = othToHira(str).replace(CrLfReg, " ");
        messageData.str_len = messageData.cleanStr.length;

        //log(messageData);
        // 投稿主保護
        if (messageData.id == parent_id) {
            addDB(messageData);
            return;
        }
        if (SETTING_LIST.verifyOnryFilter.data && messageData.verify) {
            addDB(messageData);
            return;
        }
        // blacklist_id比較
        if (blacklist_id.has(messageData.id)) {
            hideComment(messageData, "他で検出済");
            return;
        }
        // 認証済アカウント強制ブロック
        if (SETTING_LIST.verifyBlock.data && messageData.verify) {
            hideComment(messageData, "認証垢");
            return;
        }
        // 投稿言語の制限
        for (let div of text_divs) {
            if (!allowLang_reg.test(div.lang)) {
                hideComment(messageData, `<span title="${div.lang}">非許可言語</span>`);
                return;
            }
        }

        let ret = commentFilter(messageData);
        switch (ret[0]) {
            case -1:
                // 取得,判定済投稿
                return;
            case 0:
                // 問題なし
                addDB(messageData);
                return;
            case 1:
                // フィルターに反応
                hideComment(messageData, `<span title="フィルター「/${ret[1]}/uim」">フィルター検出</span>`);
                return;
            case 2:
                // 絵文字のみ(スパム)
                hideComment(messageData, "絵文字のみ");
                return;
            case 3:
                // コピペ
                hideComment(messageData, `<span title="類似度:${(ret[1] * 10000 | 0) / 100}%">文章の複製</span>`);
                return
            case 4:
                // 異常なハッシュタグの使用
                hideComment(messageData, `<span title="使用回数: ${ret[1]}">#多量使用</span>`)
                return;
            case 5:
                // 自分自身の引用
                hideComment(messageData, "自身の引用");
                return;
        }
    }

    function commentFilter(mesData) {
        let message = mesData.cleanStr;
        if (!message.replace(spaceReg, "").length && !mesData.attach_img) {
            return [2];
        }

        // 引用リツイートしている場合
        if (mesData.reTweet) {
            // 自分自身の場合
            if (SETTING_LIST.oneselfRetweetBlock.data && mesData.reTweet.id == mesData.id) {
                return [5];
            }
        }

        // フィルターによる検出
        for (let reg of blacklist_reg) {
            if (reg[0].test(message)) {
                return [1, reg[1]];
            }
        }

        // 異常なハッシュタグの使用回数
        let hashtagCou = message.match(/#[^ ]+/g)?.length ?? 0;
        if (hashtagCou >= SETTING_LIST.maxHashtagCount.data) {
            return [4, hashtagCou];
        }

        // 短い文字列は比較しない(誤爆対処)
        if (levenshteinDistanceUseFlag
            && mesData.str_len >= SETTING_LIST.minSaveTextSize.data) {
            // コピぺチェック
            let msts = SETTING_LIST.maxSaveTextSize.data;
            let al = mesData.str_len;
            for (let md of msgDB) {
                let a = message;
                let b = md.cleanStr;
                let bl = md.str_len;
                let m = Math.min(al, bl, msts);
                if (m != al) {
                    a = a.substring(0, m);
                }
                if (m != bl) {
                    b = b.substring(0, m);
                }

                // 一度取得したツイートだった場合
                let am = mesData.dateTime.getTime();
                let bm = md.dateTime.getTime();
                if (am == bm && mesData.id == md.id && mesData.cleanStr == md.cleanStr) {
                    return [-1];
                }

                let ld = levenshteinDistance(a, b);
                if (ld >= SETTING_LIST.msgResemblance.data) {
                    if (am > bm) {
                        return [3, ld];
                    }
                    else {
                        blacklist_id.add(md.id);
                        break;
                    }
                }
            }
        }
        else {
            // 比較が行われない場合の代替処理
            for (let md of msgDB) {
                let am = mesData.dateTime.getTime();
                let bm = md.dateTime.getTime();
                if (am == bm && mesData.id == md.id && mesData.cleanStr == md.cleanStr) {
                    return [-1];
                }
            }
        }


        return [0];
    }

    function addDB(mesData) {
        // 短いと誤爆するため
        if (mesData.str_len < SETTING_LIST.minSaveTextSize.data) {
            return;
        }
        msgDB_id.add(mesData.id);
        if (msgDB.length > SETTING_LIST.maxSaveLogSize.data) {
            msgDB.shift();
        }
        msgDB.push(mesData);
        log(msgDB.length);
    }

    function hideComment(mesData, reason, ch = true) {
        blacklist_id.add(mesData.id);

        mesData.card.classList.add(HIDE_CLASS);

        if (SETTING_LIST.visibleLog.data) {
            let div = document.createElement("div");
            div.classList.add(LOG_CLASS);
            div.innerHTML = /* html */ `
<span>[${reason}] <a href="/${mesData.id}" title="${mesData.id}">${mesData.name}</a></span>

<label><input type="checkbox">元Tweetを見る</label>
`;
            mesData.card.prepend(div);
        }
        // 無駄な比較をしないように
        if (ch) {
            dbCommentBlock(mesData.id);
        }
    }

    // 後からblacklist_idに登録された場合、
    function dbCommentBlock(id) {
        if (msgDB_id.has(id)) {
            for (let i = msgDB.length - 1; i >= 0; i--) {
                let mData = msgDB[i];
                if (mData?.id == id) {
                    msgDB.splice(i, 1);
                    if (mData.base_url == oldUrl) {
                        hideComment(mData, `再帰的検出`, false);
                    }
                }
            }
            msgDB_id.delete(id);
        }
    }


    // メニューを開く
    function menuOpen() {
        log("メニュー表示...開始");
        if (!exMenuDOM) {
            menu_init();
        }

        // DOM 取得
        let menu_elem = document.getElementById(EX_MENU_ID);
        if (!menu_elem) {
            // なければ複製して追加
            menu_elem = exMenuDOM.cloneNode(true);
            document.body.appendChild(menu_elem);
            document.getElementById(EX_MENU_ITEM_BASE_ID + "__save").addEventListener("click", menuSave);
            document.getElementById(EX_MENU_ITEM_BASE_ID + "__close").addEventListener("click", menuClose);

            document.getElementById(EX_MENU_ITEM_BASE_ID + "customCss").addEventListener("keydown", OnTabKey);
            document.getElementById(EX_MENU_ITEM_BASE_ID + "resetSetting").addEventListener("click", menuReset);
        }
        menu_elem.classList.add(EX_MENU_OPEN_CLASS);
        log("メニュー表示...完了");
    }

    // メニューを閉じる
    function menuClose() {
        log("メニュー非表示");
        let menu_elem = document.getElementById(EX_MENU_ID);
        if (menu_elem) {
            menu_elem.classList.remove(EX_MENU_OPEN_CLASS);
        }
    }

    // データ保存
    function menuSave() {
        log("設定保存...開始");
        for (let key in SETTING_LIST) {
            let item = SETTING_LIST[key];

            let elem = document.getElementById(EX_MENU_ITEM_BASE_ID + key);
            if (elem) {
                let data = null;
                switch (item.input) {
                    case "text":
                    case "textarea":
                        data = elem.value;
                        break;
                    case "number":
                        data = parseFloat(elem.value);
                        if (item?.min != null && item.min > data) {
                            data = item.min;
                        }
                        if (item?.max != null && item.max < data) {
                            data = item.max;
                        }
                        break;
                    case "checkbox":
                        data = elem.checked;
                        break;
                    case "radiobutton":
                        // 使ってない
                        break;
                    default:
                        continue;
                }
                if (data == null) {
                    continue;
                }
                item.data = data;
            }
        }
        let dic = {};
        for (let key in SETTING_LIST) {
            let d = SETTING_LIST[key]?.data;
            let _d = SETTING_LIST[key]?._data;
            if (d != null && d != _d) {
                dic[key] = d;
            }
        }
        try {
            GM_setValue(SETTING_SAVE_KEY, JSON.stringify(dic));
        }
        catch (e) {
            console.error(e);
        }
        log("設定保存...完了");
        menuClose();
    }

    function menuReset() {
        if (confirm("本当にリセットを実行しますか?")) {
            log("リセット処理実行");
            GM_deleteValue(SETTING_SAVE_KEY);
            location.reload();
        }
    }

    //####################################################################################################

    // DOMが設置されるまで待機
    function waitForKeyElements(
        selectorTxt, //クエリセレクター
        actionFunction, //実行関数
        bWaitOnce = true, //要素が見つかっても検索を続ける
        iframeName = null //iframeの中の要素の場合はiframeのidを書く
    ) {
        var targetNodes, btargetsFound;
        var iframeDocument = document;
        if (iframeName !== null) {
            let iframeElem = document.getElementById(iframeName);

            if (!iframeElem) {
                doRetry();
                return;
            }
            iframeDocument = iframeElem.contentDocument || iframeElem.contentWindow.document;
        }
        targetNodes = iframeDocument.querySelectorAll(selectorTxt);

        if (targetNodes && targetNodes.length > 0) {
            btargetsFound = true;
            targetNodes.forEach(function (element) {
                var alreadyFound = element.dataset.found == 'alreadyFound' ? 'alreadyFound' : false;

                if (!alreadyFound) {
                    var cancelFound;
                    if (iframeName !== null) {
                        cancelFound = actionFunction(element, iframeDocument);
                    }
                    else {
                        cancelFound = actionFunction(element);
                    }
                    if (cancelFound) {
                        btargetsFound = false;
                    }
                    else {
                        element.dataset.found = 'alreadyFound';
                    }
                }
            });
        }
        else {
            btargetsFound = false;
        }

        if (btargetsFound && bWaitOnce) {
            //終了
        }
        else {
            doRetry();
        }

        function doRetry() {
            setTimeout(function () {
                waitForKeyElements(selectorTxt,
                    actionFunction,
                    bWaitOnce,
                    iframeName
                );
            }, 300);
        }
    }

    // 不明な空白を半角スペースに
    function uspTosp(str) {
        str = str.toString()
        for (let reg of spaceRegList) {
            str = str.replace(reg, " ");
        }
        return str;
    }

    //全ての文字を共通化
    function othToHira(str) {
        str = uspTosp(str);
        for (let regs of othToHiraRegList) {
            str = str.replace(...regs);
        }
        return str.toLowerCase();
    }

    // 困った時のレーベンシュタイン距離
    function levenshteinDistance(str1, str2) {
        let r,
            c,
            cost,
            lr = str1.length,
            lc = str2.length,
            d = [];

        for (r = 0; r <= lr; r++) {
            d[r] = [r];
        }
        for (c = 0; c <= lc; c++) {
            d[0][c] = c;
        }
        for (r = 1; r <= lr; r++) {
            for (c = 1; c <= lc; c++) {
                cost = str1.charCodeAt(r - 1) == str2.charCodeAt(c - 1) ? 0 : 1;
                d[r][c] = Math.min(d[r - 1][c] + 1, d[r][c - 1] + 1, d[r - 1][c - 1] + cost);
            }
        }
        return 1 - d[lr][lc] / Math.max(lr, lc);
    }

    // unicodeを復元
    function reRegExpStr(str) {
        return uspTosp(str)
            .replace(/\\x([0-9a-fA-F]{2})|\\u([0-9a-fA-F]{4})|\\u\{([0-9a-fA-F]{1,6})\}/g, function (f, a, b, c) {
                let str = a ?? b ?? c ?? null;
                if (str == null) {
                    return f;
                }
                return String.fromCodePoint(parseInt(str, 16));
            });
    }

    // tabをtextareaで入力可能に
    function OnTabKey(e) {
        if (e.keyCode != 9) {
            return;
        }
        e.preventDefault();

        let obj = e.target;

        // 現在のカーソルの位置と、カーソルの左右の文字列を取得
        var cursorPosition = obj.selectionStart;
        var cursorLeft = obj.value.substr(0, cursorPosition);
        var cursorRight = obj.value.substr(cursorPosition, obj.value.length);

        obj.value = cursorLeft + "\t" + cursorRight;

        // カーソルの位置を入力したタブの後ろにする
        obj.selectionEnd = cursorPosition + 1;
    }

    // ログを判別しやすく
    function log(str) {
        if (DEBUG) {
            console.log(`[${PRO_NAME}]`, str);
        }
    }
})();