HubotでRocket.Chat用Botを作る

Rocket.Chat用のHubot作成手順です。

**2018/06/14追記**
https://rocket.chat/docs/bots/ にはbotkitなどの記載もありますが、現在は開発中で未リリースです。
また、hubotも https://rocket.chat/docs/bots/running-a-hubot-bot/ にはボイラープレートからの作成なども記載されていますが、これも開発中です。
bot系は開発フレームワークなども含め、[強化検討](https://github.com/RocketChat/Rocket.Chat/projects/16)されているようです。 slack の Interactive messages のような機能も[検討](https://github.com/RocketChat/Rocket.Chat/issues/6786)されています。

**2018/07/26追記**

- hubot-rocketchat v1.0.12 に気づいたので追記。
- hubot-rocketchat v1.0.9 からenvelope, message, userのroomの値がIDから名称に変更されたのに気づいたので追記。
- `yo hubot` すると、hubot-rocketchat v2.0.0 が入ってしまう問題に気付いたので追記。

**2018/10/15追記**
ボイラープレートを使用するv3系については、[ボイラープレート](https://github.com/RocketChat/hubot-rocketchat-boilerplate)は未だ `NB: THIS IS A WORK IN PROGRESS` だし、[アダプタ](https://github.com/RocketChat/hubot-rocketchat/releases)もv2系リリースタグ無し（[なぜかnpmにはv2が上がっている](https://www.npmjs.com/package/hubot-rocketchat)）。またアダプタ内で使用されている @rocket.chat/sdk のバージョンが古い(v0.1.0)などにより、 still beta と判断。

- `yo hubot` すると、hubot v3.1.1 が入ってしまう問題に気付いたので追記。

**2018/12/14追記**
一昨日くらいにSnapでRocket.Chat 0.72.1 が降ってきたタイミングで、botが一部反応しなくなった。
`ROCKETCHAT_ROOM` でroomを指定していると反応しない。hear, respondなどの応答系が全滅。。
`LISTEN_ON_ALL_PUBLIC=true` 付けると反応する。が、、全roomで反応。
1つ前のバージョンの Rocket.Chat 0.71.2 を立てて正常に動作することを確認。
rocketchat_driver.coffee 内の setupReactiveMessageList メソッド内の changedMsg.args[1] が `{}` になっている所まで確認。
試しにhubot 3系で動作確認したが、同様に動作しない。
[このissueか？](https://github.com/RocketChat/Rocket.Chat/issues/12574)

**2018/12/16追記**
SnapでRocket.Chat 0.72.3 が降ってきて、正常に戻った。
`changedMsg.args[1]` に値が入ってくるように戻っていた。

## 対象
Rocket.Chat : 0.55.0 くらいで動作確認。
node : 6.10.x
開発言語 : JavaScript(≠CoffeeScript)

# Hubotの開発環境作成
node, npmをインストールします。
その後、Hubot開発に必要な `yo` , `generator-hubot` をインストールします。
参考：https://hubot.github.com/docs/

```sh
# 未インストール時のみ
npm install -g yo generator-hubot
```

## Hubot作成
### 雛型生成
yoで雛型生成します。

```sh
mkdir myhubot
cd myhubot
yo hubot
# 対話式で色々聞かれるが、adapterに rocketchat を指定すればとりあえずはOK
# hubot-rocketchatも一緒にインストールされる

# ******** 2018/10/15追記 ********
# yoがv3.1.1を入れてしまうので、ダウングレードする。
npm install hubot@2.19.0

# ******** 2018/07/26追記 ********
# yoがv2.0.0を入れてしまうので、ダウングレードする。
npm install hubot-rocketchat@1.0.12

```

2017/09/07 現在、ここでインストールされるhubotはnpmに登録されているv2.19.0です。
githubのv3.0.1を使用すると、正常に動作しないようです。

2018/10/15 現在、ここでインストールされるhubotはnpmに登録されているv3.1.1です。
v2系にダウングレードする必要があります。

`hubot-rocketchat` は Rocket.Chat用のHubot adapter で https://github.com/RocketChat/hubot-rocketchat で開発されています。
2018/05/01 v2.0.0 リリース ※**don't use this!!** [yoでの生成とhubot v2が非サポート](https://github.com/RocketChat/hubot-rocketchat#important)になったので使ってはいけない。
2017/10/10 [v1.0.12 リリース](https://www.npmjs.com/package/hubot-rocketchat/v/1.0.12) ※githubでタグ打ちされていないバージョン。 npm に登録されていた。
2017/08/08 [v1.0.11 リリース](https://github.com/RocketChat/hubot-rocketchat/releases/tag/v1.0.11)
2017/07/26 [v1.0.10 リリース](https://github.com/RocketChat/hubot-rocketchat/releases/tag/v1.0.10)
2017/07/14 [v1.0.9 リリース](https://github.com/RocketChat/hubot-rocketchat/releases/tag/v1.0.9) ※破壊的変更あり

### とりあえず動作確認
環境変数を設定し、Hubotを起動します。

```sh
# 環境変数設定
export ROCKETCHAT_URL=http://192.168.0.123:3000
export ROCKETCHAT_ROOM=general
export ROCKETCHAT_USER=chabot # botのユーザー
export ROCKETCHAT_PASSWORD=chabotpassword # botのパスワード
export ROCKETCHAT_AUTH=password

# adapterにrocketchatを指定して起動
bin/hubot -a rocketchat
```

以下を確認。

- Rocket.Chat の general にボットユーザーが参加されていることを確認。
- `chabot ping` と打ち、`PONG` と帰ってくることを確認。

Ctrl+Cで終了。
ここまでで、基本的な枠の作成は完了です。

### 不要なパッケージ削除
yoで生成した雛型には不要なパッケージが色々入っているので削除します。
私はデータ保持にはredisは使わず、代わりにnedbをよく使用している為、以下では`hubot-redis-brain`も消してます。

```sh
# 全無まとめて削除Ver.
npm uninstall hubot-diagnostics hubot-google-images hubot-google-translate hubot-help hubot-heroku-keepalive hubot-maps hubot-pugme hubot-redis-brain hubot-rules hubot-scripts hubot-shipit --save

# ping等
npm uninstall hubot-diagnostics --save

# リスの画像
npm uninstall hubot-shipit --save

# heroku用
npm uninstall heroku-keepalive --save

# map
npm uninstall hubot-maps --save

# help
npm uninstall hubot-help --save

# google-translate
npm uninstall hubot-google-translate --save

# google-images
npm uninstall hubot-google-images --save

# パグの画像
npm uninstall hubot-pugme --save

# ロボット3原則
npm uninstall hubot-rules --save

# DEPRECATED
npm uninstall hubot-scripts --save

# redisを使ってデータを保持したい場合使用。
# 私はnedb使ったりするので削除。
npm uninstall hubot-redis-brain --save
```

### 不要なファイル削除
以下のファイルを削除します。

- external-script.json
  外部botコードを使用しないので削除します。

- hubot-script.json
  hubot-scriptを使わないので削除します。

- bin/hubot.cmd
  本番でwindowsで動作させたい場合などは残します。
  windowsで開発する場合もGit for Windowsが入っていると思うのでbash.exeで問題なければ基本使わないハズ。

- scripts/example.coffee
  サンプルコードなので削除します。

### nodeのバージョン変更
package.jsonに記載されているenginesのバージョンが古いので動作させるnodeのバージョンに書き換えます。

```js
"engines": {
  "node": "6.10.x"
}
```

### 起動スクリプト編集
私は `bin/hubot` に環境変数などを以下のように書いて起動しています。
また、本番環境では Systemdに登録して起動しています。

```sh
#!/bin/sh

set -e

export ROCKETCHAT_URL=http://192.168.0.123:3000
export ROCKETCHAT_ROOM=wg-sandbox
export ROCKETCHAT_USER=chabot
export ROCKETCHAT_PASSWORD=chabotpassword
export ROCKETCHAT_AUTH=password

npm install
export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH"

exec node_modules/.bin/hubot --name "chabot" -a rocketchat
```

### メイン処理ファイル作成
`scripts` ディレクトリ以下にjsファイルを作成します。ファイル名は任意です。
私は `main.js` を作成しています。

```js
// Description:
//   先頭に"// Description:"コメントを書いておかないと起動時に警告(INFOだが)が出る。
module.exports = (robot) => {

    // ここに処理を記述する。
    robot.respond(/hoge/i, (res) => {
        res.send("hoge!!!");
    });

};
```

#### ※開発言語について
Hubotのデフォルト開発言語はCoffeeScriptですが（v3系([#1347](https://github.com/hubotio/hubot/pull/1347))からJavaScriptに移行してました）、scriptsフォルダにJavaScriptファイルを置いても実行されます。Rocket.ChatもCoffeeScriptからJavaScriptに移行していますし、私はJavaScriptで書いています。
参考：https://github.com/RocketChat/Rocket.Chat/issues/796

## メッセージ投稿処理について
### 基本応答
ユーザーの投稿に反応して応答を返す場合、`robot.hear` や `robot.respond` の応答関数の引数のメソッド `res.send` を使用します。
デフォルトでは新規投稿にしか反応しません。編集投稿に反応するようにするためには環境変数 `RESPOND_TO_EDITED=true` を設定する必要があります。
2017/09/07 現在 `hear` 内でメッセージ( `res.message` )が新規か編集かを判別する手段は無いようです。どうしても必要な場合、Rocket.Chat用のHubot adapterの `recketchat.coffee` にコードを1行追加して `hear` 内で判別可能なようにできます。


```js
// Description:
//   先頭に"// Description:"コメントを書いておかないと起動時に警告が出る。(INFOだが)
module.exports = (robot) => {

	robot.hear(/hoge/i,(res) => {
		res.send("hoge! 全ての投稿でhearが含まれる投稿に反応して発言する。");
	});

	robot.respond(/piyo/i,(res) => {
		res.send("piyo! botの名前＋piyoが含まれる投稿に反応して発言する。");
		// 注意：
		// ここでのbotの名前はbin/hubotの中に書かれている --name 【名前】でも反応する。
		// これはyo hubot時に質問されるbotname。
		// ボット名はアットマークなしで反応する。
	});

};
```

### 能動的に投稿
`cron` や `node-schedule` などを使用して、定時に投稿する場合などは、 `robot.send` を使用します。
2017/09/07 今更ながら `robot.messageRoom` を発見。こちらの方がシンプル。

```js
// チャンネル名を指定する必要があります。
robot.send({ room: "wg-hoge" }, "hogeeeee!");
// こっちのほうがシンプル。
robot.messageRoom("wg-hoge", "hogeeeee!");
```

### テキスト以外の情報を含むメッセージ投稿
テキスト以外の情報を含める場合、メッセージオブジェクトを作成して send に渡します。

```js
robot.hear(/hoge/ig, (res) => {

	// 文字列のみ
	res.send("hoge?!");

	// 絵文字もOK
	res.send("emoji! :ok:");

	// メッセージオブジェクトを作って送信
	const message = {
		msg: "message here!",
		attachments: [{
			"text": "text here! :tada:",
			"color": "#ff0000",
			"alias": "別名を表示させたいときに設定",
			"avatar": "投稿者アイコンにアバターを表示させたいときに設定。URLを指定する。",
			"emoji": "投稿者アイコンに絵文字を表示させたいときに設定。ex. :ok:",
			"fields": [{
				"short": true,
				"title": "ok (no title emoji :ok: )",
				"value": "[Link](https://google.com/) *bold* _italic_ "
			},{
				"short": true,
				"title": "ng",
				"value": "no emoji :tada: , \r\n %0a no newline, no `inline` "
			}]
		}]
	};
	res.send(message);
});
```

```js
// 自発的に投稿、またはチャンネルを指定して送る場合、robot.sendを使う。
robot.send({ room: "wg-hoge" }, "hogeeeee!");

// メッセージオブジェクトを作って送信
const message = {
	msg:"send to wg-hoge!",
	attachments: [{
		"text": ":ok: emoji OK!",
		"color": "#FF0000"
	}]
};
robot.send({ room: "wg-hoge" }, message);
// こっちのほうがシンプル。
robot.messageRoom("wg-hoge", message);

```

Hubotのsend時のメッセージオブジェクトのドキュメントは今のところ発見できず、[rest-api](https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage) や [realtime-api](https://rocket.chat/docs/developer-guides/realtime-api/the-message-object/) 見ながら探してます。
今のところ `msg` , `attachment` でほぼ事足りています。
`avatar` は[自分のQiitaのアイコンURL](https://qiita-image-store.s3.amazonaws.com/0/154744/profile-images/1485098384)でも表示されました。

## res(RocketChatResponseオブジェクト)の中身
`robot.hear`や`robot.respond`などのコールバック関数の引数の`res`について。
[Rocket.Chat Hubot adapter定義クラス](https://github.com/RocketChat/hubot-rocketchat/blob/master/src/rocketchat.coffee)（[Hubot定義クラス](https://github.com/hubotio/hubot/blob/master/src/response.js)を継承）。

### メソッド
|メソッド|実装箇所|
|----|----|
|sendDirect|RocketChatBotAdapterのsendDirect
|sendPrivate|RocketChatBotAdapterのsendDirect
|send|RocketChatBotAdapterのsend
|emote|RocketChatBotAdapterのemote
|reply|RocketChatBotAdapterのreply

その他topicやplayなどもあるがRocketChatBotAdapterで未実装のため使用不可。

### プロパティ
いろんなところにroomやmessage, userが入ってて紛らわしいですが、全て同じ情報が入っています。

**2018/07/26追記**
※1: roomについて、1.0.9からルームIDではなくルーム名が入るようになったようです。**（破壊的変更）**
また、1.0.10からダイレクトメッセージ、ライブチャットの場合はIDがセットされています。

|プロパティ|内容|
|----|----|
|robot|上位のrobotと同じ。|
|message|TextMessageオブジェクト|
|match|マッチ配列。String.match()の戻り値と同様（g付き時にinput, indexが無いとかﾅﾝﾄｶ）|
|envelope|エンベロープ。room, user, messageが入っています。|

### res.envelope
|プロパティ|内容|
|----|----|
|room|ルーム名 ※1|
|user|Userオブジェクト|
|message|TextMessageオブジェクト|

### TextMessageオブジェクト
[Hubot定義クラス](https://github.com/hubotio/hubot/blob/master/src/message.js)。
メッセージ内容が入っています。出現箇所は以下。
* res.message
* res.envelope.message

|プロパティ|内容|
|----|----|
|user|Userオブジェクト|
|text|投稿メッセージ内容|
|id|メッセージID（内部ID）|
|done|処理済みフラグ。これを `true` にすると後続の`hear`や`respond`が処理しない。listenは通った。|
|room|ルーム名 ※1|

### Userオブジェクト
[Hubot定義クラス](https://github.com/hubotio/hubot/blob/master/src/user.js)。 `id`, `name` 以外は自由っぽい。
投稿したユーザーの情報が入っています。出現箇所は以下。
* res.message.user
* res.envelope.user
* res.envelope.message.user

|プロパティ|内容|
|----|----|
|id|ユーザーID（内部ID）|
|name|ユーザー名（名前ではない）|
|alias|エイリアス（無い場合undefined）|
|room|ルーム名 ※1|
|roomID|ルームID（内部ID）※1.0.9で追加された。|
|roomType|ルームタイプ ※1.0.12で追加された。※2|

※2: ルームタイプ（他にもあるかも）

|値|内容|
|----|----|
|c|パブリックチャンネル|
|p|プライベートチャンネル|
|d|ダイレクトメッセージ|


## Hubotの運用環境
基本node, npmが入っていれば問題ないです。


## 環境変数について
~~https://rocket.chat/docs/administrator-guides/hubot#configuration-options~~ https://rocket.chat/docs/bots/configure-bot-environment/ に説明は書いてありますが、ハマったり、よくわからなかった設定値について動作確認した記録です。
**2018/06/14追記**
URLが変わってました。

### ROCKETCHAT_URL
`ROCKETCHAT_URL=http://192.168.0.123:3000/` のように最後に `/` を付けると正しく動作しません。
0.46.0以前くらいでは起動していた時期がありました。

### LISTEN_ON_ALL_PUBLIC
基本的にボットが参加しているチャンネルすべてでlistenしますが、
`LISTEN_ON_ALL_PUBLIC=true` を指定して起動した場合、ボットが参加していないチャンネルもlistenします。
応答した場合は、ボットは未参加のままメッセージが投稿されます。
この値が設定された場合、 `ROCKETCHAT_ROOM='' ` とするべき、と書いてあります、理由は不明。
hubot-rocketchat内部で `LISTEN_ON_ALL_PUBLIC=true` の場合、 `ROCKETCHAT_ROOM=''` が設定されるようになっていました。

### ROCKETCHAT_ROOM
`ROCKETCHAT_ROOM=random`
ボット起動時、ボットが指定チャンネルに参加していない場合、参加します。
ここに記載していないチャンネルでもユーザーがボットを参加させると listen するようです。

### RESPOND_TO_EDITED
ボットはデフォルトでは新規投稿時にしか反応しません。（default: false）
`RESPOND_TO_EDITED=true` とすることにより、編集時にも反応するようになります。

### PORT
Hubotのパラメーター。
HubotにはExpressが内蔵されていて、ポートを指定しない場合、デフォルトの8080が使用されます。
そのため、同一端末で2つのHubotを起動すると、後から起動したHubotの8080が使えず、起動時にエラーログが出力されます。
Expressの機能を使わない場合、hubot起動オプションで`--disable-httpd`または`-d`を指定します。