// ==UserScript== // @name Twitter(旧:𝕏)のインプレッション小遣い稼ぎ野郎どもをdisplay:none;するやつ // @name:ja Twitter(旧:𝕏)のインプレッション小遣い稼ぎ野郎どもをdisplay:none;するやつ // @name:en Hide the Twitter (formerly: 𝕏) impression-earning scammers with "display:none;" // @name:zh-TW 使用 "display:none;" 隱藏 Twitter(曾用名: 𝕏)的印象詐騙者。 // @name:zh-CN 使用 "display:none;" 隐藏 Twitter(曾用名: 𝕏)的印象收益骗子。 // @namespace https://snowshome.page.link/p // @version 1.4.9 // @description Twitterのインプレゾンビを非表示にするツールです。 // @description:ja Twitterのインプレゾンビを非表示にするツールです。 // @description:en This is a tool to hide spam on Twitter. // @description:zh-TW 這是一個可以隱藏 Twitter 垃圾訊息的工具。 // @description:zh-CN 这是一个可以隐藏 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 // @downloadURL none // ==/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: `設定項目をリセットします。 (ページがリロードされます) 実行すると設定は復元出来ません!!!`, 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 */ ` 変更の保存をした場合、ページを更新してください。`; let advanceDOM = document.createElement("details"); advanceDOM.innerHTML = /* html */ ` 高度な設定`; 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, "
"); 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, `非許可言語`); return; } } let ret = commentFilter(messageData); switch (ret[0]) { case -1: // 取得,判定済投稿 return; case 0: // 問題なし addDB(messageData); return; case 1: // フィルターに反応 hideComment(messageData, `フィルター検出`); return; case 2: // 絵文字のみ(スパム) hideComment(messageData, "絵文字のみ"); return; case 3: // コピペ hideComment(messageData, `文章の複製`); return case 4: // 異常なハッシュタグの使用 hideComment(messageData, `#多量使用`) 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 */ ` [${reason}] ${mesData.name} `; 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); } } })();