YouTube Liveのスパムを自動で非表示にするChrome拡張機能を作った話

## 動機
[YouTube Live Spam Killer - Chrome ウェブストア](https://chrome.google.com/webstore/detail/youtube-live-spam-killer/ldipjlelakcabalgnlkhbbdbabcihjbc)

突然ですが、自分はバーチャルYouTuber(VTuber)のライブストリーミングやそのアーカイブを複数窓垂れ流して作業するのが大好きです。
しかし、最近そのライブチャット欄に品のないユーザ名の出会い系スパムが多発しています。
これらのブロック作業で配信に集中できなかったりといった面倒が目立つようになりました。
せっかくの至福の時間を邪魔される、というのは人間誰しも嫌なものです。

人間様がクソプログラムに苛つかせられる必要はありません。
技術でなんとかしましょう。

YouTubeを利用している関係上、ブラウザ上でバックグラウンド動作してくれるタイプの技術を使うほうが見通しは良いです。
したがって今回、自分がメインで利用しているウェブブラウザであるGoogle Chrome上の拡張機能として実装することにしました。

なお、手元の事前調査などは使い慣れているPythonを利用しているため、Python(のREPLであるiPython)とJavaScriptのコードが混在する点のみご承知おきください。

## 実験
### 事前調査
これらのスパムは数種のパターンが存在しますが、大方以下のような特徴を持っています。

- ユーザ名の形式: `(英語圏の女性名) (スパムユーザのアカウントに誘導する文言)`
    - 例: `Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]`
    - 最近ではコロナウィルス周りの文言になったスパムも登場している
- 直前のチャット欄に現れた(人間の)コメントをコピーし、絵文字を追加したコメントを行う

上記の例で示したように、『スパムユーザのアカウントに誘導する文言』は単純なフィルタで取り除けないように、プレーンな英語ではなく記号類を用いています。

```python
In [1]: import unicodedata

In [2]: target = "Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]"

In [3]: for i, c in enumerate(target):
   ...:     print(i, '"{}"'.format(c), '{:04x}'.format(ord(c)),
   ...:           unicodedata.category(c), unicodedata.name(c))
   ...:
0 "L" 004c Lu LATIN CAPITAL LETTER L
1 "i" 0069 Ll LATIN SMALL LETTER I
2 "l" 006c Ll LATIN SMALL LETTER L
3 "a" 0061 Ll LATIN SMALL LETTER A
4 "h" 0068 Ll LATIN SMALL LETTER H
5 " " 0020 Zs SPACE
6 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H
7 "0" 0030 Nd DIGIT ZERO
8 "'" 0027 Po APOSTROPHE
9 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T
10 " " 0020 Zs SPACE
11 "ᴄ" 1d04 Ll LATIN LETTER SMALL CAPITAL C
12 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H
13 "ᴀ" 1d00 Ll LATIN LETTER SMALL CAPITAL A
14 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T
15 " " 0020 Zs SPACE
16 "ᴊ" 1d0a Ll LATIN LETTER SMALL CAPITAL J
17 "ᴏ" 1d0f Ll LATIN LETTER SMALL CAPITAL O
18 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
19 "ɴ" 0274 Ll LATIN LETTER SMALL CAPITAL N
20 " " 0020 Zs SPACE
21 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M
22 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E
23 " " 0020 Zs SPACE
24 "[" 005b Ps LEFT SQUARE BRACKET
25 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
26 " " 0020 Zs SPACE
27 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M
28 " " 0020 Zs SPACE
29 "ʟ" 029f Ll LATIN LETTER SMALL CAPITAL L
30 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I
31 "ᴠ" 1d20 Ll LATIN LETTER SMALL CAPITAL V
32 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E
33 "]" 005d Pe RIGHT SQUARE BRACKET
```
これはまだ素直な方ですが、ものによってはUnicodeの隅を突くような記号を突っ込んでくることもあります。
このような手口に対応する手法はいくつか考えられますが、この場合正規化を行い適当な英語表記に変換してやるのが一番楽でしょう。

### 正規化概要
具体的な手法としては、次に示すような連想配列を用いてユーザ名を逐一英語小文字に変換していく手法を採っています。

```javascript
const NORMALIZE_MAP = {
  a: ['ᴀ', 'ᗩ'],
  c: ['ᴄ', 'ᑕ'],
  e: ['3', 'ǝ', 'ᴇ', 'Ꭼ', 'є'],
  h: ['ʜ', 'н', 'ᕼ'],
  i: ['ɪ', 'ι', 'Ꭵ', 'í'],
  j: ['ᴊ'],
  k: ['ᴋ'],
  l: ['ʟ', 'Ꮮ', 'ᒪ'],
  m: ['ᴍ', 'ᗰ'],
  n: ['ɴ', 'ᑎ'],
  o: ['0', 'ᴏ'],
  p: ['ᴘ'],
  r: ['ʀ'],
  s: ['ꜱ'],
  t: ['т', 'ᴛ'],
  v: ['Ꮙ', 'ᴠ'],
} // 執筆時のバージョン

var normalizeWord = word => { // 単語単位で正規化を行う
  word = word.replace(/[!-/:-@\[-`{-~]/g, '') // 約物の除去
  Object.keys(NORMALIZE_MAP).forEach(key => {
    NORMALIZE_MAP[key].forEach(item => {
      word = word.replace(new RegExp(item, 'g'), key)
    })
  })
  return word.toLowerCase()
  // 英語圏以外の文字に作用し、連想配列をすり抜けてしまう事象が確認されたため、
  // 後付けでtoLowerCase()を作用させている
}
```

### スパム検知手法
さらに、予め用意しておいたブラックリスト単語に合致するかを調べます。

```javascript
const BLACKLIST_WORDS = ['chat', 'click', 'colona', 'here', 'hot', 'join', 'live', 'love',
                         'mask', 'me', 'photo', 'sex', 'tap'] // 執筆時のバージョン
```

先頭の人名を除去した部分のユーザ名を正規化し、これらのブラックリスト単語が多くヒットすればスパムと見なします。

#### 旧検知手法
別の検出方法として、複数の言語圏の文字を利用していることを検出する手法も考えていました。
しかし、これはあまり一般性がないことが判明したためボツとしました。

## ソース
https://github.com/Le96/youtube-live-spam-killer

## 参考
参考にした記事一覧です。
JavaScriptはほぼ触った経験がなかったので基本的なことも紛れています。

### 言語仕様
[Array.filterが使えないNodeListに対してフィルターをかける方法 - Qiita](https://qiita.com/trewanek/items/c3d9c3455249fcefb028)
[JavaScript で forEach を使うのは最終手段 - Qiita](https://qiita.com/diescake/items/70d9b0cbd4e3d5cc6fce)
[Javascript 連想配列(オブジェクト)をforEachでループさせたい。 - かもメモ](https://chaika.hatenablog.com/entry/2016/06/21/085000)
[Javascript：任意の文字列を含むかどうかの確認方法 | WWWクリエイターズ](http://www-creators.com/archives/4537)
[JavaScriptにおける連想配列のforループ操作 - Qiita](https://qiita.com/wifeofvillon/items/15359535a834832e08ea)
[JSのArray.prototype.filter.callとArray.from().filterどっちが早いか - Qiita](https://qiita.com/apollo_program/items/60f987205f805d7beb8e)
[Unicode Property Escapesについての補説](http://www.akenotsuki.com/misc/srell/unicode_property_escapes.html)
[Unicodeの文字プロパティを指定した正規表現をみてみる（ECMAScript2018） - TES Blog](https://blog.tes.co.jp/entry/2018/06/29/145450)
[特定の文字列を全て置換する[Javascript] - Qiita](https://qiita.com/DecoratedKnight/items/103ab57431b6c448e535)

### (jQueryを利用しない)DOM操作
[【Javascript】setInterval()で要素が現れるまで待つ - Qiita](https://qiita.com/Rasukarusan/items/754de5d8943eea8cc4ce)
[iframeのonloadのタイミングでiframeのソースを確認する - みかづきブログ その3](http://kimizuka.hatenablog.com/entry/2015/09/16/000000)
[JavaScript - js読み込みの際はnullになるが、devツールのコンソールではnullにならない｜teratail](https://teratail.com/questions/116594)
[javascript - 読み込み完了の取得について - スタック・オーバーフロー](https://ja.stackoverflow.com/questions/27587/%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E5%AE%8C%E4%BA%86%E3%81%AE%E5%8F%96%E5%BE%97%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
[JavaScriptのMutationObserverでDOMの変化を監視する方法 - Qiita](https://qiita.com/munieru_jp/items/a6f1433652124a2165e4)
[ライブラリを使わない素のJavaScriptでDOM操作 - Qiita](https://qiita.com/kouh/items/dfc14d25ccb4e50afe89)
[要素の取得方法まとめ - Qiita](https://qiita.com/amamamaou/items/25e8b4e1b41c8d3211f4)

### Chrome拡張機能
[Chrome Extension を作って公開する - Qiita](https://qiita.com/sqrtxx/items/19fd2114430e9e1fb57f)
[Chrome 拡張機能のマニフェストファイルの書き方 - Qiita](https://qiita.com/mdstoy/items/9866544e37987337dc79)
[Chromeブラウザの拡張機能を作ってみたい初心者向けに開発方法を紹介！【サンプルあり】 - Qiita](https://qiita.com/guru_taka/items/37a90766f4f845e963e5)
[Chrome拡張　備忘録　１から１０まで - Qiita](https://qiita.com/Tachibana446/items/696bb93bd4a23525cbb0)
[Chrome拡張の開発方法まとめ　その1：概念編 - Qiita](https://qiita.com/k7a/items/26d7a22233ecdf48fed8)

### 類似の試み a.k.a.先行研究
[YouTube Live の絵文字スパムをブロックする - Qiita](https://qiita.com/compo031/items/61ce02704ee27abed8d1)
[YouTubeコピペスパムを抽出する（モデレータなら自動ブロック？） - Qiita](https://qiita.com/iroiro_bot/items/46f4d661813a15b15e6a)

### アイコン
[コメントアイコン7 | アイコン素材ダウンロードサイト「icooon-mono」 | 商用利用可能なアイコン素材が無料(フリー)ダウンロ ードできるサイト](https://icooon-mono.com/14448-%e3%82%b3%e3%83%a1%e3%83%b3%e3%83%88%e3%82%a2%e3%82%a4%e3%82%b3%e3%83%b37/)

## あとがき
本当はスパム報告・ブロックまで行いたかったのですが、手元のアイデアでは実装方法が浮かびませんでした。
ご教示いただければ非常に助かります。

また、想定の甘さや新種スパムの発生などに伴い、False Positive/False Negativeなど発生している場合もぜひご教示ください。