DiscordでMeCabで形態素解析した結果を返すだけのbotを作る

# 参考にした記事

* https://qiita.com/PinappleHunter/items/af4ccdbb04727437477f
* https://qiita.com/junpiiiiiiik/items/79c2219da6b5e2c06ed8

# 概要

掲題のとおりですが、

* Discord botを作る。
* 特定の文字列がメッセージに含まれたときに、MeCabで形態素解析した結果を返す。
* MeCabの辞書としてNEologdも使えるようにしておく。
* 処理言語はPythonを使う。
* heroku上で動作させる。
* herokuのContainer Registryを使ってDockerイメージをデプロイする。

```sh
root@4c506a68cd26:~# python --version
Python 3.7.7
root@4c506a68cd26:~# mecab --version
mecab of 0.996
```

# 準備

「参考にした記事」に倣うのみなので、割愛。

* https://qiita.com/junpiiiiiiik/items/79c2219da6b5e2c06ed8#%E8%A8%AD%E5%AE%9A
* https://qiita.com/PinappleHunter/items/af4ccdbb04727437477f#bot%E7%94%A8%E3%81%AE%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E6%89%8B%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B

１点わからなかった、というか勘違いしていたのは、ローカルやherokuにデプロイしたときにエンドポイントを指定する設定がどこかにあるものだと思っていろいろ調べてしまった。
どこにも書いていなかったが、なんとなく自分の中で結論が出たので忘れないように書いておくと、
ローカルだろうがインターネット上のサーバーで動作していようが、 `client.run(TOKEN)` が実行されて動作している間はDiscordサーバーに接続されて、オンラインと認識される。ということだと思う。
ソースコードを読んでないし確実な情報ではないが、多分そんなところだと思う。

# heroku周りの準備

CLIからherokuにログインする。

```sh
heroku login
```

ブラウザでherokuにログインする。

Heroku 上の Container Registry へログインする。

```sh
heroku container:login
```

DiscordのTOKENを設定する。

```sh
heroku config:set TOKEN=hogehoge
```

# Dockerの設定

```Dockerfile
FROM python:3.7-slim

ENV HOME=/app
WORKDIR /app

# NEologdのインストールに使う
RUN apt-get update && apt-get install -y \
  build-essential \
  curl \
  git \
  openssl \
  sudo \
  zip \
  file

# MeCabのインストール
RUN apt-get update && apt-get install -y \
  mecab \
  libmecab-dev \
  mecab-ipadic \
  mecab-ipadic-utf8

RUN cd /usr/share/mecab && \
    git clone https://github.com/neologd/mecab-ipadic-neologd.git && \
    cd mecab-ipadic-neologd/ && \
    ./bin/install-mecab-ipadic-neologd -n -a -y -p /usr/share/mecab/dic/mecab-ipadic-neologd/

COPY requirements.txt ./requirements.txt

RUN pip install -r requirements.txt

COPY . .

# discord_bot.py を実行してbotを起動する
CMD ["python", "discord_bot.py"]
```

# 使用ライブラリの設定

```requirements.txt
mecab-python3
discord.py
```

# botの実装

コマンドは使わず、全てのメッセージから、
* メッセージの文字列の先頭が "mecab " で開始されていたら
* スペース区切りで分割して
* 末尾の文字列を形態素解析対象にして
* 間の文字列はMeCabのオプションとして
* 形態素解析した結果をメッセージで送る

```discord_bot.py
import os
import MeCab
import discord

TOKEN = os.environ["TOKEN"]
PREFIX = "mecab "

client = discord.Client()

@client.event
async def on_ready():
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

@client.event
async def on_message(message):
    print("received message: " + str(message))
    if message.content.startswith(PREFIX):
        # 送り主がBotだった場合反応しない
        if client.user != message.author:
            print(message.content)

            splited_message = message.content.split() # スペースで分割する
            splited_message.pop(0) # 先頭は "mecab" なので不要
            content = splited_message.pop() # 末尾は解析対象の文字列として扱う
            option = ' '.join(splited_message) # "mecab " から "{対象文字列}" の間の文字列をスペースで連結する
            mecab = MeCab.Tagger(option)
            m = "```" + mecab.parse(content) + "```"
            print(m)

            # メッセージが送られてきたチャンネルへメッセージを送る
            await message.channel.send(m)

client.run(TOKEN)
```

# herokuにデプロイ

```sh
heroku container:push web --app discord-bot-sample-app
heroku container:release web --app discord-bot-sample-app
```

デプロイが成功したら、DiscordのdeveloperページのOAuth2のページの **SCOPES** にチェックボックスを入れて下部に表示されるURLにアクセスして、サーバーにbotを接続する。

これでbotが動く(はず）。

# 使い方

`mecab こちらの公開リポジトリに動作環境を反映してあります。` とメッセージすると、

```
こちら    名詞,代名詞,一般,*,*,*,こちら,コチラ,コチラ
の    助詞,連体化,*,*,*,*,の,ノ,ノ
公開    名詞,サ変接続,*,*,*,*,公開,コウカイ,コーカイ
リポジトリ    名詞,一般,*,*,*,*,*
に    助詞,格助詞,一般,*,*,*,に,ニ,ニ
動作    名詞,サ変接続,*,*,*,*,動作,ドウサ,ドーサ
環境    名詞,一般,*,*,*,*,環境,カンキョウ,カンキョー
を    助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
反映    名詞,サ変接続,*,*,*,*,反映,ハンエイ,ハンエイ
し    動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て    助詞,接続助詞,*,*,*,*,て,テ,テ
あり    動詞,非自立,*,*,五段・ラ行,連用形,ある,アリ,アリ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。    記号,句点,*,*,*,*,。,。,。
EOS
```
こんな返事がかえってくる。

`mecab -d /usr/share/mecab/dic/mecab-ipadic-neologd/ まずは対象となる文字列を準備しましょう。` というふうに NEologd の辞書を指定すると、

```
こちら    名詞,代名詞,一般,*,*,*,こちら,コチラ,コチラ
の    助詞,連体化,*,*,*,*,の,ノ,ノ
公開    名詞,サ変接続,*,*,*,*,公開,コウカイ,コーカイ
リポジトリ    名詞,固有名詞,一般,*,*,*,リポジトリ,リポジトリ,リポジトリ
に    助詞,格助詞,一般,*,*,*,に,ニ,ニ
動作環境    名詞,固有名詞,一般,*,*,*,動作環境,ドウサカンキョウ,ドーサカンキョー
を    助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
反映    名詞,サ変接続,*,*,*,*,反映,ハンエイ,ハンエイ
し    動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て    助詞,接続助詞,*,*,*,*,て,テ,テ
あり    動詞,非自立,*,*,五段・ラ行,連用形,ある,アリ,アリ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。    記号,句点,*,*,*,*,。,。,。
EOS
```

`動作環境` のところだけが変わっているが、ちゃんとNEologdの辞書を使っているようだ。

# 問題点

この実装だと、botが接続しているサーバーのメッセージを全て受信して、`mecab ` の文字列が存在するかをチェックしてしまう(んだと思う。多分)。
ってことは大半の関係のないメッセージを処理するコストがかかってしまい無駄が多い。

コマンドを実装すれば、prefix文字列とコマンド文字列が存在したときだけ、メッセージを受信することができる(はず。多分）。

# コマンド版の実装

```discord_command_bot.py
import os
import MeCab
import discord
from discord.ext import commands

TOKEN = os.environ["TOKEN"]

bot = commands.Bot(command_prefix='!', description='Output to results of morphological analysis.')

@bot.event
async def on_ready():
    print('Logged in as')
    print(bot.user.name)
    print(bot.user.id)
    print('------')

@bot.command()
async def mecab(ctx, *args):
    print("received message: " + str(args))
    if bot.user != ctx.message.author:
        l = list(args)
        content = l.pop() # 末尾は解析対象の文字列として扱う
        print(content)

        option = ' '.join(l)
        mecab = MeCab.Tagger(option)
        m = "```" + mecab.parse(content) + "```"
        print(m)

        # メッセージが送られてきたチャンネルへメッセージを送ります
        await ctx.send(m)

bot.run(TOKEN)
```

非command版との違いは、

* `client (discord.Client)` が `bot (commands.Bot)` になっている。
* `mecab` メソッドでcommandを受信する。
* 受け取る引数はMessageではなく、`args` のタプルになる。

# Dockerfileを書き換えてデプロイ

コマンド版で実行するようにDockerfileを書き換える。

```Dockerfile
CMD ["python", "discord_commmand_bot.py"]
```

herokuにデプロイする。

```sh
heroku container:push web --app discord-bot-sample-app
heroku container:release web --app discord-bot-sample-app
```

これで `!mecab` コマンドが有効になったはず。

# 実験

`!mecab こちらの公開リポジトリに動作環境を反映してあります。`

```
こちら    名詞,代名詞,一般,*,*,*,こちら,コチラ,コチラ
の    助詞,連体化,*,*,*,*,の,ノ,ノ
公開    名詞,サ変接続,*,*,*,*,公開,コウカイ,コーカイ
リポジトリ    名詞,一般,*,*,*,*,*
に    助詞,格助詞,一般,*,*,*,に,ニ,ニ
動作    名詞,サ変接続,*,*,*,*,動作,ドウサ,ドーサ
環境    名詞,一般,*,*,*,*,環境,カンキョウ,カンキョー
を    助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
反映    名詞,サ変接続,*,*,*,*,反映,ハンエイ,ハンエイ
し    動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て    助詞,接続助詞,*,*,*,*,て,テ,テ
あり    動詞,非自立,*,*,五段・ラ行,連用形,ある,アリ,アリ
ます    助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。    記号,句点,*,*,*,*,。,。,。
EOS
```

`mecab こちらの公開リポジトリに動作環境を反映してあります。`

反応なし。

成功。

# 追記

## botがすぐオフラインになってしまう

デプロイして数分はbotとして機能しているが、すぐにオフラインになってしまう。
またデプロイしたりdynoをON/OFFすれば復帰するが、常時動かしておきたい。

https://teratail.com/questions/242975

調べてみると、デプロイ時に `web` としてcontainerをpushしていたのが良くなかったみたい。

```sh
heroku container:push web --app discord-bot-sample-app
heroku container:release web --app discord-bot-sample-app
```

workerとしてデプロイし直す。

```sh
heroku container:push worker --app discord-bot-sample-app
heroku container:release worker --app discord-bot-sample-app
```

確かにこれで止まらなくなった。