Hubotに疲れた人のPython製bot"Errbot"入門

botといえば[Hubot](https://hubot.github.com/)が有名ですが、個人的にはJavascriptがそんなに得意でもないし、hubot-slack moduleもv3→v4で大幅に仕様が変わって追従できなくなり、Slackとの連携では稀に接続が切れてBot自体黙り込んでしまうというつらい状況で、ほとほと疲れてしまいました。一方、Python製の[Errbot](http://errbot.io/en/latest/)が良い出来なので紹介します。

なお、既に先人の方が入門については記載されているので、「**HubotでできるアレはErrbotでできるのか**」という観点で紹介したいと思います(あとメリット/デメリットも紹介します)。

* [Errbot: Python製chatbotの基礎
 | Qiita](https://qiita.com/nanakenashi/items/6542250275dca8062015)

# Errbotを試しに導入する

これから紹介するErrbotとその機能については、以下にまとめたDockerコンテナで確認することができるようにしました。(プラグイン自体は、公式からほとんど引っ張ってきただけですが…)

https://github.com/tkit/errbot-plugin-example

上記READMEの通り、以下の通り実行すればTextモードで対話的に実行できます。

```bash:Errbotコンテナ起動
docker run -it --rm \
    --name err-text \
    -e BACKEND=Text \
    -e BOT_USERNAME="@testbot" \
    -e BOT_ADMINS="@tkit" \
    -e "TZ=Asia/Tokyo" \
    rroemhild/errbot
```

Slackで動かす場合は、 `BACKEND=Slack`とする必要があります。(Bot用のTOKENは別途取得してください)

```bash:Errbotコンテナ起動(Slack)
docker run --rm \
    --name err-slack \
    -e BACKEND=Slack \
    -e BOT_TOKEN=xoxb-...... \
    -e BOT_USERNAME="@testbot" \
    -e BOT_ADMINS="@tkit" \
    -e "TZ=Asia/Tokyo" \
    rroemhild/errbot
```

`BOT_USERNAME`と`BOT_ADMINS`は好きに書き換えてください。

起動後、以下のコマンドでプラグインをインストールしてください。

```:プラグインのインストール
!repos install https://github.com/tkit/errbot-plugin-example
```

# 機能について

## 基本的な命令

Errbotは、命令をmentionなどで送り込むのではなく、(デフォルトでは)`!`というPrefixを付与することによって発行します。

```:helloコマンド
[@tkit ➡ @testbot] >>> !hello
[@tkit ➡ @testbot] [␍]
Hello, world!
```

```:helpコマンドによるコマンドリファレンス
[@tkit ➡ @testbot] >>> !help
[@tkit ➡ @testbot] [␍]
All commands

 Backup
 Backup related commands.
• !backup - Backup everything.
 ChatRoom
 This is a basic implementation of a chatroom
• !room create - Create a chatroom.
• !room list - List chatrooms the bot has joined.
• !room destroy - Destroy a chatroom.
(snip)
```

## mentionに反応して返事を返す

さて本題です。Hubotでいう `robot.respond` 相当のことです。
`callback_mention`を使う方法と`@re_botcmd`デコレータを使う方法の2種類がありますが、後者の方が使いやすいでしょう。
(早速ですがTextモードでは動かないので、確認する場合はSlackなどを利用してください)

### `callback_mention`を使う

`callback_mention`関数を使うことで、非常に簡単に反応させることができます。

* [7. Mentions](http://errbot.io/en/latest/user_guide/plugin_development/mentions.html#mentions)

```python:callback_mentionの例
from errbot import BotPlugin, botcmd

class Example(BotPlugin):

    # like robot.respond
    def callback_mention(self, message, mentioned_people):
        for identifier in mentioned_people:
            self.send(message.frm, 'User %s has been mentioned' % identifier)

        if self.bot_identifier in mentioned_people:
            self.send(message.frm, 'Errbot has been mentioned !')
```

コードを見ると分かる通り、`callback_mention`は、別にBot向けのmentionに限らず **あらゆるmention** に反応します。そこから、Bot向けのmentionだけを条件で取ることができるので、そのときだけ反応させるようにすればよいわけです。

ちなみに、上記を動かすと、2つの条件がどちらも反応して無限ループします…。

<img width="422" alt="respond.png" src="https://qiita-image-store.s3.amazonaws.com/0/16458/f25090ac-e6a2-49ce-1f0e-594dcd67297b.png">


この方法がお勧めではないのは、簡単ではあるものの、あらゆる処理が`callback_mention`に集約されてしまうためです。見通しも悪くなります。この関数を使う場合は、Bot以外のmentionに反応させる場合などに限った方がよいでしょう。

### `@re_botcmd`デコレータを使う

一方、関数に`@re_botcmd`デコレータを利用するこちらの場合は、パターンを正規表現で指定でき、より`robot.respond`に近い形で利用できます。

* [4.4.1. With a bot prefix](http://errbot.io/en/latest/user_guide/plugin_development/botcommands.html#with-a-bot-prefix)

```python:@re_botcmdの例
from errbot import BotPlugin, botcmd, re_botcmd
import re 

class Example(BotPlugin):

    # like hubot.respond
    @re_botcmd(pattern=r"^(([Cc]an|[Mm]ay) I have a )?cookie please\?$")
    def hand_out_cookies(self, msg, match):
        yield "Here's a cookie for you, {}".format(msg.frm)
        yield "/me hands out a cookie."
```

しかし、これだけではmentionに反応しません。mentionによっても反応させるためには、config中にある `BOT_ALT_PREFIXES`を指定すれば思った通りにいきます。

* https://github.com/errbotio/errbot/blob/5.1/errbot/config-template.py#L228

今回のケースでは、 `BOT_ALT_PREFIXES=@testbot` とすれば、mentionでも反応するようになります。

```bash:Errbotコンテナ起動(Slack)
docker run --rm \
    --name err-slack \
    -e BACKEND=Slack \
    -e BOT_TOKEN=xoxb-...... \
    -e BOT_ALT_PREFIXES="@testbot" \  # この行を追加
    -e BOT_USERNAME="@testbot" \
    -e BOT_ADMINS="@tkit" \
    -e "TZ=Asia/Tokyo" \
    rroemhild/errbot
```

ただし、今回のExampleでは、これと`callback_mention`が競合するので、これもまた無限ループになります。

<img width="655" alt="hear.png" src="https://qiita-image-store.s3.amazonaws.com/0/16458/7f785e1f-b9ca-73ad-e531-644cd6b87e4c.png">


`callback_mention`と比べると、こちらのほうが起動時にオプションが必要な分、ちょっとだけ面倒ですが、一度設定してしまえば気にならないですし、`callback_mention`に全てまとめてしまうよりは後々やりやすくなるかと思います。

## 特定の発言に反応する

Hubotでいう `robot.hear` 相当のことです。
これも2種類の方法がありますが、後者のほうがよいでしょう。

### `callback_message`関数を使う

単純にやると、以下の方法で `callback_message` を使えば、全ての発言を拾うので、その中から発動する条件だけを拾って動かすことができます。

* [5.5. Trigger a callback with every message received](http://errbot.io/en/latest/user_guide/plugin_development/messaging.html#trigger-a-callback-with-every-message-received)

```python:callback_messageの例
from errbot import BotPlugin

class Example(BotPlugin):
    # like hubot.hear
    def callback_message(self, message):
        if message.body.find('cookie') != -1:
            self.send(
                message.frm,
                "What what somebody said cookie!?",
            )
```

これで問題はないのですが、全てのメッセージを拾うので、この `callback_message`が肥大化しますし、一見何をするのか分かりづらくなります。(Classごと分けてしまってもいいでしょうけれど)

### `@re_botcmd` デコレータ+`prefixed=False`指定を使う

mentionのときも紹介した`@re_botcmd`デコレータを使う方法もあります。デコレータのオプションで`prefixed=False`とすれば、プレフィックス(つまり`@testbot`相当のmention)なしで反応させることができます。

* [4.4.2. Without a bot prefix](http://errbot.io/en/latest/user_guide/plugin_development/botcommands.html#without-a-bot-prefix)

```python:@re_botcmd+prefixed=Falseの例
from errbot import BotPlugin
import re

class Example(BotPlugin):

    # like robot.hear
    @re_botcmd( pattern=r"(^| )cookies?( |$)", prefixed=False, flags=re.IGNORECASE)
    def listen_for_talk_of_cookies(self, msg, match):
        """Talk of cookies gives Errbot a craving..."""
        return "Somebody mentioned cookies? Om nom nom!"
```

<img width="446" alt="hear.png" src="https://qiita-image-store.s3.amazonaws.com/0/16458/802b97b3-40b3-5a6d-4a61-c31a9c4acd31.png">


## 発言を記憶する

Hubotでいう`robot.brain`相当のコマンドです。といっても簡単で、`self`(つまり自分自身)に保存させるだけです。

* [8. Persistence](http://errbot.io/en/latest/user_guide/plugin_development/persistence.html#persistence)

```python:@re_botcmd+prefixed=Falseの例
from errbot import BotPlugin

class Example(BotPlugin):

    # like robot.brain
    @botcmd
    def remember(self, msg, args):
        self['TODO'] = args

    @botcmd
    def recall(self, msg, args):
        return self['TODO']
```

Errbotが凄いのは、ちゃんと永続化してくれます。具体的には、`BOT_DATA_DIR`に定義している場所に保存しているようです。

https://github.com/errbotio/errbot/blob/5.1/errbot/config-template.py#L66

<img width="433" alt="brain.png" src="https://qiita-image-store.s3.amazonaws.com/0/16458/71523bc3-695e-82f6-4f22-c4d2f684343c.png">

他の保管先(手段)もPluginによって選べます。

## 定期的に実行する

Hubotでも標準ではできませんが、Errbotでも標準ではできません。
…が、attakeiさんのerrcronを使えば解決してしまいます！

https://github.com/attakei/errcron

# まとめ

上記の通り、Hubotでできる主だったことはほとんどErrbotでもできます…というか、大抵のBotなら基本機能としてこの辺りはできるのでしょうね。
加えて、今回は紹介しませんでしたがErrbotはそのBot単体でかなり完成度が高いので、「ただErrbotがあるだけ」でいろいろできるのが強みであるように思います。

## Errbotのいいところ

### 公式の情報が非常に充実している

もう公式だけ見ればいいんじゃないかというくらい公式の情報が充実しています。プラグインの開発や設定などは、とりあえず公式を一読すれば大まかにイメージがつきます。

### Botだけで運用が全てできる

例えばプラグインのインストール・アップデートや、Bot自体の再起動、ログ参照、プラグインの削除までできてしまいます。Hubotなら、別途CI/CD環境が必要になりそうなところですが、ErrbotではBotさえ立ち上げてしまえば、Botとの対話の中でほぼ全て完結してしまいます。プラグインで何かしらのバグがあると、ちゃんとそのプラグインだけを切り離してくれたりもします。

これらの重要度の高い操作は実行できるユーザを絞ることもでき、よく考えられているなぁと思います。

## Errbotのあまり嬉しくないところ

Errbotにも不満はあります。

### 名前がひどい

**Err**botという名前で9割損している気がするのですが、どうなんでしょうかね…。Google検索しても、大抵はerr + botっていう結果で他の文献がヒットしている気がします。なんでこんな名前なんだろう…。

### 公式以外に情報が少ない

文献は公式にかなり充実していますし、大抵それを見れば解決するのですが、すこし欲張ったことをすると途端に情報がなくなる気がします。(そもそも利用者が少ない？)
なぜか動かないことが多く、結局のところはソースを死ぬほど読んで解決することが多かったのですが、自分だけなのだろうか…。

### コードでBotの全てを定義できない

Hubotの場合は、Node.jsでHubotを作り、スクリプトをパスに配置して利用することになりますが、Errbotの場合はSlackなどのChatツールのなかで対話的にBotに命令を出し、Chatツールの中でプラグインをインストールしたり各種設定を行ったりするのが主として考えられているようです。

例えば、プラグインのインストールは `!repos install <path>`と命令を送り、Webserver機能を有効にするためには `!plugin config <jsonで書いたconfig>`を送り込む必要があります。

普通はそれでも問題ないのですが、これの問題点は**コードでBotを定義できない**ことにあります。つまり、一度別の環境でBotを使おうとした時に、それをそのまま持っていっただけだは設定は完了せず、Botを起動後に追加で設定をしないとセットアップが完了しないという問題があります。(回避策はあるにはあるようですが、まだそれほど追えていません)

これはメリットでもありデメリットでもあって、毎回コードを修正したりしなくとも、チャットツールとBotさえあればほとんど解決してしまうので、良し悪しあるとは思います。

### Chatツールとしての「共通」を目指している

例えばSlackには[Interactive Message](https://api.slack.com/interactive-messages)という機能がありますが、どうやら公式では対応予定がなさそうです。これは、Errbotが別にSlackだけのものではないからで、他のチャットツールを広くカバーする共通部分のみを対応するからのようです。

同様に、Slackのattachmentも、他のチャットツールとパラメータを同じにできる部分だけが使えるようになっていて、attachmentの幅広く豊かな表現を全て自由に使えるかというとそんなことはないです。これも、Backendのプラグインを改修すればいいのでしょうけれど、なかなかそこまでは…。

なので、チャットツールは特定のプロダクトのみで利用しようとしている場合(大抵そうだと思いますが)は、多少の作り込みが発生すると思ったほうがよさそうです。

# 最後に

いろいろ書きましたが、結局は一番使いやすい言語のBotを使えばいいんじゃないかなと思いました。Botが大流行している最近で、Hubotを必ずしも使わなければならないこともないかと思います。

そんな中で、Errbotは機能が充実していていい感じだったので、しばらく使ってみようかと思います。