KoebutaSlayer

Adds a button that eliminates evil replys to tweet details.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или 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();

})();