KoebutaSlayer

Adds a button that eliminates evil replys to tweet details.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           KoebutaSlayer
// @namespace      https://github.com/albno273/KoebutaSlayer
// @description    Adds a button that eliminates evil replys to tweet details.
// @description:ja ツイート詳細に声豚のリプライを抹殺するボタンを追加します。
// @include        https://twitter.com/*
// @version        2.1.1
// @require        https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_log
// @grant          GM_registerMenuCommand
// @license        MIT License
// ==/UserScript==

// 使用スクリプト: GM config
// https://openuserjs.org/src/libs/sizzle/GM_config.js
// 参考: Extract images for Twitter
// https://greasyfork.org/ja/scripts/15271-extract-images-for-twitter

'use strict';

// 除外リスト(初期設定)
const defaultWhitelistArray = [
  '0309akari',      '0812asumikana',  '38kMarie',        'AyakaOhashi',
  'Emiryun',        'Erietty_55',     'ErikoMatsui',     'HiRoMi_ig',
  'Lynn_0601_',     'Miho_Aaaa',      'OnoSaki1126',     'RiccaTachibana',
  'TomoyoKurosawa', 'Uesakasumire',   'Yaskiyo_manager', 'akekodao',
  'coloruri',       'eerie_eery',     'fuchigami_mai',   'han_meg_han',
  'ibuking_1114',   'kanekosanndesu', 'kurapimk',        'maaya_taso',
  'makomorino',     'marika_0222',    'mikakokomatsu',   'mikami_shiori',
  'nanjolno',       'nojomiy',        'numakura_manami', 'osorasan703',
  'ousakichiyo',    'reimatsuzaki',   'shimoda_asami',   'shiori_izawa',
  'suzaki_aya',     'takamori_723',   'tanezakiatsumi',  'yuichupunch',
  'yukari_tamura',  'yuuka_aisaka',   'yuumin_uchiyama'
];

// 初期設定
GM_config.init(
  {
    'id':    'KoebutaSlayerConfig',
    'title': 'KoebutaSlayer 設定',
    'fields':
    {
      'slayBehavior':
      {
        'label':   '抹殺時の挙動',
        'type':    'radio',
        'options': ['非表示にする', 'ニンジャスレイヤー風'],
        'default': '非表示にする'
      },
      'whitelist':
      {
        'label':   'ホワイトリスト(ID を改行で区切って入力してください)',
        'type':    'textarea',
        'default': `${defaultWhitelistArray.join('\n')}`
      }
    },
    'events':
    {
      'save': () => {
        // 単語構成文字と改行以外の文字があったら注意
        if(/[^0-9_a-zA-Z\n]/g.test(GM_config.fields['whitelist'].toValue()))
          alert('Caution!\nホワイトリストに半角英数字とアンダーバー、' +
                '改行以外の文字が入っていませんか?');
        alert('設定を保存しました。');
      },
      'reset': () => {
        alert('設定を初期化しました。');
      }
    }
  }
);

GM_registerMenuCommand("抹殺設定", function () { GM_config.open(); });

(() => {

  const processedList      = new WeakMap(),
        processedTweetList = new WeakMap();

  let slayCount = 0, // ツイート抹殺数
      isSlayed  = false;

  // 抹殺ボタンを作る
  const createSlayer = (tweetList) => {
    slayCount = 0;
    const slayer = document.createElement('div');
    slayer.setAttribute('class', 'ProfileTweet-action ProfileTweet-action--Slay');
    slayer.innerHTML =
      '<button class="ProfileTweet-actionButton js-actionButton js-actionSlay" type="button">' +
      '<div class="IconContainer js-tooltip" title="抹殺">' +
      '<span class="Icon Icon--medium Icon--close"></span>' +
      '<span class="u-hiddenVisually">抹殺</span>' +
      '</div>' +
      '<span class="ProfileTweet-actionCount ">' +
      '<span class="ProfileTweet-actionCountForPresentation Slay-counter" aria-hidden="true"></span>' +
      '</span>' +
      '</button>';
    const icon    = slayer.getElementsByClassName('Icon')[0],
          counter = slayer.getElementsByClassName('Slay-counter')[0];
    slayer.addEventListener('mouseenter', () => {
      icon.style.color    = 'darkred';
      counter.style.color = 'darkred';
    });
    slayer.addEventListener('mouseleave', () => {
      if(!isSlayed) {
        icon.style.color    = '';
        counter.style.color = '';
      }
    });
    slayer.addEventListener('click', () => {
      slayTweet(tweetList);
      if(slayCount != 0) {
        isSlayed = true;
        // 抹殺したツイート数を更新
        setTimeout(() => {
          counter.textContent = slayCount;
        }, 500);
      }
    });
    return slayer;
  };

  // 抹殺ボタンを追加
  const addSlayer = () => {
    const tweetList = document.getElementsByClassName('ProfileTweet-actionList');
    for (let i = 0, len = tweetList.length; i < len; i++) {
      const tweet = tweetList[i];
      if(!processedList.has(tweet)) {
        // 画面遷移前のボタンが残った時に削除
        const oldSlayer = tweet.getElementsByClassName('ProfileTweet-action--Slay')[0];
        if(oldSlayer)
          oldSlayer.remove();
        // ホワイトリストに入ってるアカウントのツイートの詳細欄にのみボタンを登録
        // TODO: parentNode 連打やめたい
        const tweetGGparent = tweet.parentNode.parentNode.parentNode,
              wl            = GM_config.get('whitelist').split('\n'),
              id            = /@(\w+)/g.exec(tweetGGparent.innerText)[1];
        if(tweetGGparent.classList.contains('permalink-tweet-container')) {
          if(wl.indexOf(id) >= 0) {
            processedList.set(tweet, 1);
            tweet.appendChild(createSlayer(tweetList));
          }
        }
      }
    }
  };

  // ツイートを抹殺
  const slayTweet = (tweetList) => {
    const behavior = GM_config.get('slayBehavior'),
          wl       = GM_config.get('whitelist').split('\n');
    for (let i = 0, len = tweetList.length; i < len; i++) {
      // 画面遷移前のツイートも含まれるのでリプライのみ抽出
      // TODO: parentsNode 連打やめたい
      const tweets = tweetList[i].parentNode.parentNode.parentNode.parentNode
        .getElementsByClassName('permalink-descendant-tweet');
      if(tweets.length == 1) {
        const tweet    = tweets[0],
              from     = tweet.getAttribute('data-screen-name'),
              to       = tweet.getAttribute('data-mentions');
        if(!processedTweetList.has(tweet)) {
          // チェインはされてるけど @id が明記されていない場合
          if(wl.indexOf(from) == -1 && to == null)
            slay(behavior, tweet);
          else {
            const toArr = to.split(' ');
            toArr.forEach((value, index, array) => {
              if (wl.indexOf(from) == -1 && wl.indexOf(value) >= 0)
                slay(behavior, tweet);
            });
          }
        }
      }
    }
  };

  const slay = (behavior, tweet) => {
    if(behavior == '非表示にする')
       tweet.style.display = 'none';
    else if(behavior == 'ニンジャスレイヤー風') {
      tweet.getElementsByClassName('tweet-text')[0].textContent = 'アバーッ!';
      tweet.getElementsByClassName('fullname')[0].textContent   = 'Koebuta Slayer';
      tweet.getElementsByClassName('username')[0].innerHTML     = '@<b>koebutaslayer</b>';
      tweet.getElementsByClassName('js-action-profile-avatar')[0]
        .setAttribute('src', 'https://pbs.twimg.com/profile_images/876001170409439232/pHHfBGu3_400x400.jpg');
      const media = tweet.getElementsByClassName('AdaptiveMediaOuterContainer')[0];
      if(media)
        media.remove();
    }
    processedTweetList.set(tweet, 1);
    slayCount++;
  }

  // DOMの更新が入るたびにボタンを追加
  (() => {
    let DOMObserverTimer = false;
    const DOMObserverConfig = {
      attributes: true,
      childList: true,
      subtree: true
    };
    const DOMObserver = new MutationObserver(() => {
      if (DOMObserverTimer !== 'false') {
        clearTimeout(DOMObserverTimer);
      }
      DOMObserverTimer = setTimeout(() => {
        DOMObserver.disconnect();
        addSlayer();
        DOMObserver.observe(document.body, DOMObserverConfig);
      }, 100);
    });
    DOMObserver.observe(document.body, DOMObserverConfig);
  })();

  // 初回起動
  addSlayer();

})();