Slackの秩序を守れ！! ~ slack_emoji_auditor ~

## はじめに
こんにちは、
皆さん仕事であったりプライベートであったり、普段からSlackを利用していますか？
筆者もSlackを仕事で利用することが多いです。

そんなSlackユーザになると利用したくなるのが[カスタム絵文字](https://get.slack.help/hc/ja/articles/206870177-%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E7%B5%B5%E6%96%87%E5%AD%97%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)ですね。
筆者の会社でも以下のように、さまざまなカスタム絵文字が日常的に利用されています。

![スクリーンショット 2018-06-27 17.17.52.png](https://qiita-image-store.s3.amazonaws.com/0/218189/a9c0ae7a-9174-bc14-4b0f-e140d44e0bd3.png)

**しかし**、ときには心無いユーザによって不愉快なカスタム絵文字が作成されているかもしれません！
皆さんもそんな経験ありませんか？

そこで今回はそんな荒れたSlackカスタム絵文字社会の秩序をGoogle Cloud Vision APIを利用して正すことを考えました。

## Google Cloud Vision API

[Google Cloud Vision API](https://cloud.google.com/vision/)は、言わずと知れたGoogle CloudのAPIサービスになります。APIを利用することによって画像内のオブジェクトを検出したり、画像内のテキスト情報を抽出したりすることができます。

実際に[Quickstart](https://cloud.google.com/vision/docs/drag-and-drop)でサンプル画像に対してAPIを実行すると以下のように解析してくれます。画像内の犬を検出してくれているのがわかります。

![スクリーンショット 2018-06-27 17.40.51.png](https://qiita-image-store.s3.amazonaws.com/0/218189/a5467c4c-33b6-812c-ae01-a729d0304fe9.png)

さて、上記のようにVision APIでは画像に対していろいろな解析をすることができますが、その中でも**[セーフサーチ検出](https://cloud.google.com/vision/docs/detecting-safe-search)**という機能があります。セーフサーチ検出ではアダルトコンテンツや暴力的コンテンツなど、画像に含まれる不適切なコンテンツを検出してくれる機能となります。

実際にAPIで問い合わせを行うと以下のように、adult,violenceなどの観点に関して、対象の画像が不正コンテンツであるかの度合いを返します。

```
{
  "responses": [
    {
      "safeSearchAnnotation": {
        "adult": "VERY_UNLIKELY",
        "spoof": "VERY_LIKELY",
        "medical": "UNLIKELY",
        "violence": "UNLIKELY"
      }
    }
  ]
}
```

不正コンテンツの度合い(Likelihood)は以下の6段階に分類されます（以下[Vision API Document](https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate)参照）。

| レベル | 説明 |
|:-----------:|:-----------|
| UNKNOWN | Unknown likelihood. |
| VERY_UNLIKELY | It is very unlikely that the image belongs to the specified vertical. |
| UNLIKELY | It is unlikely that the image belongs to the specified vertical. |
| POSSIBLE | It is possible that the image belongs to the specified vertical. |
| LIKELY | It is likely that the image belongs to the specified vertical. |
| VERY_LIKELY | It is very likely that the image belongs to the specified vertical. |

今回は、このセーフサーチ検出の機能をカスタム絵文字に適用することによって、不正な絵文字を検出することを考えていきます。

## 事前準備

今回はPythonを利用して実装を行なっていきます。事前にSlack APIのトークン情報と、Vision APIのクライアントライブラリの取得を行います。

#### Slack APIのトークン

Slack APIトークンは以下を参考に生成しておきます。
[API トークンの生成と再生成](https://get.slack.help/hc/ja/articles/215770388-API-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%94%9F%E6%88%90%E3%81%A8%E5%86%8D%E7%94%9F%E6%88%90)

#### Vision APIクライアント

PythonのVision APIクライアントは以下のサイトの手順を参考にセットアップしておきます。
[Vision API Client Libraries](https://cloud.google.com/vision/docs/libraries)

## 実装

今回の実装は以下を参考に、Vision APIの問い合わせ機能を追加しながら実装しました。
https://github.com/jkloo/slack-emojis

以下が実装コード（https://github.com/fuku68/slack_emoji_auditor）。

```python
# coding:utf-8
import os
import argparse
import requests
import logging

from google.cloud import vision
from google.cloud.vision import types

logging.basicConfig(level=logging.INFO, format='%(message)s')

SLACK_URL = 'https://{slack}.slack.com/api/emoji.list?token={token}'
RESPONSE_TYPE = ('adult', 'spoof', 'medical', 'violence', 'racy')
LIKEHOOD_NAME = ('UNKNOWN', 'VERY_UNLIKELY', 'UNLIKELY', 'POSSIBLE','LIKELY', 'VERY_LIKELY')

def main(args):
  # formatting result
  result = {}
  for type in RESPONSE_TYPE:
    result[type] = {}
    for name in LIKEHOOD_NAME:
      result[type][name] = []
      result[type][name + '_COUNT'] = 0 

  res = requests.get(SLACK_URL.format(slack=args.slack, token=args.token))

  # Google Cloud Vision API Client
  client = vision.ImageAnnotatorClient()
  image = types.Image()

  if res.status_code == 200:
    emoji = res.json().get('emoji', {})
    for k, v in emoji.items():
      image.source.image_uri = v
      logging.info('requesting... safe_search emoji: ' + k)
      gooleo_res = client.safe_search_detection(image=image)

      safe = gooleo_res.safe_search_annotation
      result['adult'][LIKEHOOD_NAME[safe.adult]].append(k)
      result['adult'][LIKEHOOD_NAME[safe.adult] + "_COUNT"] += 1
      result['spoof'][LIKEHOOD_NAME[safe.spoof]].append(k)
      result['spoof'][LIKEHOOD_NAME[safe.spoof] + "_COUNT"] += 1
      result['medical'][LIKEHOOD_NAME[safe.medical]].append(k)
      result['medical'][LIKEHOOD_NAME[safe.medical] + "_COUNT"] += 1
      result['violence'][LIKEHOOD_NAME[safe.violence]].append(k)
      result['violence'][LIKEHOOD_NAME[safe.violence] + "_COUNT"] += 1
      result['racy'][LIKEHOOD_NAME[safe.racy]].append(k)
      result['racy'][LIKEHOOD_NAME[safe.racy] + "_COUNT"] += 1
      
    result_print(result)

  else:
    logging.warning("can't get emoji list")


def result_print(result):
  print('----------- result ---------------')
  for type in RESPONSE_TYPE:
    print(type + ':')
    print('  VERY_LIKELY: ' + unicode_array_to_str(result[type]['VERY_LIKELY']))
    print('       LIKELY: ' + unicode_array_to_str(result[type]['LIKELY']))
    print('     POSSIBLE: ' + unicode_array_to_str(result[type]['POSSIBLE']))
    print('')
    print('  statistics:')
    for name in LIKEHOOD_NAME: 
      print('    ' + '{0:>13}'.format(name) + ': ' + str( result[type][name + '_COUNT']))
    print('')

def unicode_array_to_str(array):
  str = ', '.join(array)
  return '[' + str + ']'

if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument('slack', help='name of your slack group.')
  parser.add_argument('token', help='auth token for slack.')
  args = parser.parse_args()
  main(args)
```

それぞれの観点に関してPOSSIBLE、LIKELY、VERY_LIKELYに対応する絵文字と、それぞれのLikelihoodの統計情報を出力するように実装しました。

コマンドラインから以下の記述で実行します。

```sh
python auditor.py <Slackワークスペース名> <Slack APIトークン>
```

出力結果は以下。

```
---------- result ---------------
adult:
  VERY_LIKELY: []
       LIKELY: []
     POSSIBLE: []

  statistics:
          UNKNOWN: 4
    VERY_UNLIKELY: 364
         UNLIKELY: 20
         POSSIBLE: 0
           LIKELY: 0
      VERY_LIKELY: 0

spoof:
  VERY_LIKELY: [trollface, idobata_02, dodo_02, miyata_02]
       LIKELY: [utsunomiya_01, yakyu_01, eyes2]
     POSSIBLE: [utsunomiya_02, panda, tagawa, gudetama_03, ryoukai, tennen, daijobu, misawa_04, misawa_05, siba01, taiko_01, kurara]

  statistics:
          UNKNOWN: 4
    VERY_UNLIKELY: 319
         UNLIKELY: 46
         POSSIBLE: 12
           LIKELY: 3
      VERY_LIKELY: 4

medical:
  VERY_LIKELY: []
       LIKELY: []
     POSSIBLE: [thumbsup_all, fu, ee, kesseki, maguro_04, metal, funa, wakaru, kupo, elixir]

  statistics:
          UNKNOWN: 4
    VERY_UNLIKELY: 331
         UNLIKELY: 43
         POSSIBLE: 10
           LIKELY: 0
      VERY_LIKELY: 0

violence:
  VERY_LIKELY: []
       LIKELY: []
     POSSIBLE: []

  statistics:
          UNKNOWN: 4
    VERY_UNLIKELY: 360
         UNLIKELY: 24
         POSSIBLE: 0
           LIKELY: 0
      VERY_LIKELY: 0

racy:
  VERY_LIKELY: [yamamoto_maika, yoga_2, ee, kusu, maguro_04, haggar2, kotowaru]
       LIKELY: [vega, fu, haggar, yuriko, dodo_08, piggy, ogawa, misawa_03, misawa_05, elixir, kurara]
     POSSIBLE: [negi, cody, 天馬くん, bowtie, miyata_01, bdash2, dodo_05, metal, nandato, arai_01, fs-e-hiroaki-idobata, kesu, penguins, funa, kupo, yareyare, glitch_crab, dead, misawa_04, masaru-san, char, conan, matsuda_01, dusty_stick, はると]

  statistics:
          UNKNOWN: 4
    VERY_UNLIKELY: 243
         UNLIKELY: 98
         POSSIBLE: 25
           LIKELY: 11
      VERY_LIKELY: 7
```

ヒットしたら面白そうな`adult`や`violence`では対応する絵文字がありませんでした...(期待はずれ)。

ちなみに`spoof`のVERY_LIKELYと認識されたのは以下の絵文字たち

![スクリーンショット 2018-06-27 18.39.15.png](https://qiita-image-store.s3.amazonaws.com/0/218189/31b6f6a1-818c-6394-99ee-e2556de30382.png)

なんか顔が多い...

また`racy`のVERY_LIKELYと認識されたのは以下の絵文字たち
![スクリーンショット 2018-06-27 18.40.16.png](https://qiita-image-store.s3.amazonaws.com/0/218189/0d64f949-f250-7238-4e19-af6bd92a7fdf.png)

こちらの基準はさっぱり...

## 最後に

今回はSlackのカスタム絵文字に対してVision APIを利用して、コンテンツとして不正なものが無いかのチェックを行いました。
皆さんも是非、自分のSlackのワークスペースで試してみてください。そして不正なカスタム絵文字の魔の手から、Slackの秩序を守りましょう！

...こうして業務時間を浪費することによって会社の秩序は乱れていくのであった。