Slack bot に画像を表示してみた

# なんで作ろうと思った？
[前回作ったSlack bot](https://qiita.com/maiamea/items/70ebec2f6075a5a726c1) にセリフと一緒にスライムの画像も表示したいと思ったから。

# 使った技術は？
Node.js

# 前提
・Node.js インストール済
・Yarn インストール済
・Slack アカウント作成済

# どうやって作ったの？

## 一つの画像を表示する

### 1. images ディレクトリ作成
```
mkdir scripts/images
```

### 2. サンプル画像をダウンロードして保存する
[サンプル画像](https://1.bp.blogspot.com/-ZOg0qAG4ewU/Xub_uw6q0DI/AAAAAAABZio/MshyuVBpHUgaOKJtL47LmVkCf5Vge6MQQCNcBGAsYHQ/s1600/pose_pien_uruuru_woman.png)
`command` + `s`
ダウンロードフォルダにいったん保存する

### 3. ダウンロードした画像を images ディレクトリに移動させる

### 4. [参考にした記事](https://qiita.com/kijitora-neko/items/a976a08da080e2376858)の coffeescript のコードを JavaScript に変換する

変換前

```js:nekobot.coffee
module.exports = (robot) ->
    robot.hear /(猫)$/i, (msg) ->
      room = msg.envelope.room
      if room == "image"
        text = msg.match[1]
        msg.send  "#{text} の画像アップするよ"
        # 下記を追加
        filename = 'image/nya-o.png'
        channel = msg.message.rawMessage.channel
        exec "curl -F file=@#{filename} -F channels=#{channel} -F token=#{process.env.HUBOT_SLACK_TOKEN} https://slack.com/api/files.upload", (err, stdout, stderr) ->
```


変換後

```js:nekobot.js
module.exports = function(robot) {
  return robot.hear(/(猫)$/i, function(msg) {
    var channel, filename, room, text;
    room = msg.envelope.room;
    if (room === "image") {
      text = msg.match[1];
      msg.send(text + " の画像アップするよ");
      filename = 'image/nya-o.png';
      channel = msg.message.rawMessage.channel;
      return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function(err, stdout, stderr) {});
    }
  });
};

```

### 5. JavaScript に変換後のコードを既存のコードに合うように編集する

編集前

```js:hello.js
'use strict';
const data = require('./data.json');
const words = data.slimelife;
module.exports = (robot) => {
  robot.hear(/スライム/i, (msg) => {
    const user_id = msg.message.user.id;
    const word = words[Math.floor(Math.random() * words.length)];
    msg.send(`<@${user_id}> ${word}`);
  });
```

・`var` を `const` にする。
・`room`  は使用しないので消す。（チャンネルはすでにSlackの設定の時にひとつ指定しているから）
・`text` は使用しないので消す。（セリフのJSONファイルがあるから）

編集後

```js:hello.js
  'use strict';
  const data = require('./data.json');
  const words = data.slimelife;
  module.exports = (robot) => {
    robot.hear(/スライム/i, (msg) => {
      const user_id = msg.message.user.id;
      const word = words[Math.floor(Math.random() * words.length)];
+     msg.send(`<@${user_id}> ${word}`);
+     const filename = `images/pose_pien_uruuru_woman.png`;
+     const channel = msg.message.rawMessage.channel;
+     return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function(err, stdout, stderr) {
+       if (err) {
+         console.error(err);
+        }
+      });
    });
  };
```

実行すると以下のエラーメッセージがでた。

```
ERROR ReferenceError: exec is not defined`
```

[こちらの記事](https://www.wakuwakubank.com/posts/728-nodejs-child-process/)によると、`exec` の読み込みできてないのが原因だったので、以下のコードを1行目に追記した。

```js:hello.js
const { exec } = require('child_process');
```


実行すると今度は別のエラーメッセージがでた。

```
Failed to open/read local data from file/application
```
画像ファイルのパスが合ってないのが原因だった。

```
$ pwd
/Users/maiamea/git/maiamea/slimelife-bot
```
現在位置が slimelife-bot ディレクトリ。images ディレクトリは scripts ディレクトリ配下だから、slimelife-bot ディレクトリ直下ではないのでエラーになった。

ファイルパスを以下に修正

```js:hello.js
const filename = `scripts/images/pose_pien_uruuru_woman.png`;
```


### 6. ボットを Slack のアダプターなしで動かす

```
bin/hubot
```

実行すると以下のエラーメッセージがでた。

```
ERROR TypeError: Cannot read property 'channel' of undefined
```

＜検証１＞

・`bin/hubot`の時、`message`に何が入ってるのか。`channel`のオブジェクトである`rawMessage`に何が入ってるのか確認

```js:hello.js
'use strict';
const { exec } = require('child_process');
const data = require('./data.json');
const words = data.slimelife;
module.exports = (robot) => {
  robot.hear(/スライム/i, (msg) => {
    const user_id = msg.message.user.id;
    const word = words[Math.floor(Math.random() * words.length)];
    msg.send(`<@${user_id}> ${word}`);
    const filename = `scripts/images/pose_pien_uruuru_woman.png`;

    console.log('ーーーーーーーーーーー');
    console.log(msg.message);
    console.log('ーーーーーーーーーーー');
    console.log(msg.message.rawMessage);
    console.log('ーーーーーーーーーーー');

    const channel = msg.message.rawMessage.channel;
    return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function(err, stdout, stderr) {
      if (err) {
        console.error(err);
      }
    });
  });
};
```


```
bin/hubot

 TextMessage {
  user:
   User { id: '1', _getRobot: [Function], name: 'Shell', room: 'Shell' },
  done: false,
  room: 'Shell',
  text: 'スライム',
  id: 'messageId' }
ーーーーーーーーーーー
undefined
ーーーーーーーーーーー

ERROR TypeError: Cannot read property 'channel' of undefined
```

・`bin/hubot`で実行する場合、`message`には`TextMessage`が入ってる。
・`rawMessage`が`undefined`のため`channel`に何も入ってない状態になりエラーとなった。



＜検証２＞

・Slackに繋がないで実行した時と、繋げて実行した時とで何が違うか確認

・[参考にした記事](https://qiita.com/kijitora-neko/items/a976a08da080e2376858)で元々`const room = msg.envelope.room;`があったので、`room`のオブジェクトの`msg.envelope`に何が入っているのか確認

```js:hello.js
'use strict';
const { exec } = require('child_process');
const data = require('./data.json');
const words = data.slimelife;
module.exports = (robot) => {
  robot.hear(/スライム/i, (msg) => {
    const user_id = msg.message.user.id;
    const word = words[Math.floor(Math.random() * words.length)];
    msg.send(`<@${user_id}> ${word}`);
    const filename = `scripts/images/pose_pien_uruuru_woman.png`;

    console.log('ーーーーーーーーーーー');
    console.log(msg.message);
    console.log('ーーーーーーーーーーー');
    console.log(msg.message.rawMessage);
    console.log('ーーーーーーーーーーー');
    console.log(msg.envelope);
    console.log('ーーーーーーーーーーー');

    const channel = msg.message.rawMessage.channel;
    return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function(err, stdout, stderr) {
      if (err){
        console.error(err);
      }
    });
  });
};
```

【Slackに繋がないで実行した時】

```
bin/hubot

ーーーーーーーーーーー
TextMessage {
  user:
   User { id: '1', _getRobot: [Function], name: 'Shell', room: 'Shell' },
  done: false,
  room: 'Shell',
  text: 'スライム',
  id: 'messageId' }
ーーーーーーーーーーー
undefined
ーーーーーーーーーーー
{ room: 'Shell',
  user:
   User { ... },
  message:
   TextMessage { ... } 
}
ーーーーーーーーーーー
ERROR TypeError: Cannot read property 'channel' of undefined
```

・`message`には`TextMessage`が入ってる。
・`rawMessage`は`undefined`
・`room`が`Shell`


【Slackに繋げて実行した時】

```
env HUBOT_SLACK_TOKEN=控えておいた文字列 bin/hubot --adapter slack

ーーーーーーーーーーー
SlackTextMessage {
  user:
   User { ... },
  text: 'スライム',
  rawMessage:
    { client_msg_id: 'ee2210fa-39ee-4ffc-bfdb-5337d946b1ce',
     ...
     channel: 'CHCUY01CK',
     ... 
    },
  _channel_id: 'CHCUY01CK',
  _robot_name: 'slimelife_bot',
  _robot_alias: false,
  rawText: 'スライム',
  mentions: [],
  done: false,
  room: 'CHCUY01CK',
  id: '1597567494.002100',
  constructor:
   { ... },
  buildText: [Function],
  replaceLinks: [Function],
  replaceUser: [Function],
  replaceConversation: [Function] }
ーーーーーーーーーーー
{ client_msg_id: 'ee2210fa-39ee-4ffc-bfdb-5337d946b1ce',
  ...
  channel: 'CHCUY01CK',
  ...
}
ーーーーーーーーーーー
{ room: 'CHCUY01CK',
  user:
   User { ... },
  message:
   SlackTextMessage { ... }
ーーーーーーーーーーー
```

・`message`には`SlackTextMessage`が入ってる。
・`rawMessage`はちゃんと存在していて、`channel`が入ってる。
・`room`が`channel`

Slack に繋げて実行すると、ちゃんと Slack の方でセリフと画像が返ってきた。
![スクリーンショット 2020-08-16 2.16.47.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384836/46ee96bc-9dd8-9ea4-05e7-1f8802943ef3.jpeg)



【解決策】

・`bin/hubot`でエラーにならずにセリフと同様に画像もちゃんと返ってくることを確認するために、`room`が`Shell`かどうかで場合分けする。`room`が`Shell`の時は`rawMessage`が`undefined`なので、`channel`の定義はせずそのまま結果を返す。
・ターミナルでは画像表示はできないので、画像のファイル名を返すことで確認する。

```js:hello.js
'use strict';
const { exec } = require('child_process');
const data = require('./data.json');
const words = data.slimelife;
module.exports = (robot) => {
  robot.hear(/スライム/i, (msg) => {
    const user_id = msg.message.user.id;
    const word = words[Math.floor(Math.random() * words.length)];
    msg.send(`<@${user_id}> ${word}`);
    const filename = `scripts/images/pose_pien_uruuru_woman.png`;

    console.log('ーーーーーーーーーーー');
    console.log(msg.message);
    console.log('ーーーーーーーーーーー');
    console.log(msg.message.rawMessage);
    console.log('ーーーーーーーーーーー');
    console.log(msg.envelope);
    console.log('ーーーーーーーーーーー');

    const room = msg.envelope.room;
    if (room === "Shell") {
      msg.send(`表示される画像ファイルは、pose_pien_uruuru_woman.png です。`);
    } else {
      const channel = msg.message.rawMessage.channel;
      return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function (err, stdout, stderr) {
        if (err) {
          console.error(err);
        }
      });
    }
  });
};
```

```
bin/hubot

ーーーーーーーーーーー
TextMessage {
  user:
   User { id: '1', _getRobot: [Function], name: 'Shell', room: 'Shell' },
  done: false,
  room: 'Shell',
  text: 'スライム',
  id: 'messageId' }
ーーーーーーーーーーー
undefined
ーーーーーーーーーーー
{ room: 'Shell',
  user:
   User { ... },
  message:
   TextMessage { ... } 
}
ーーーーーーーーーーー
<@1> 一行読めた！やったぁ！！
表示される画像ファイルは、pose_pien_uruuru_woman.png です。
```

ターミナル上で、セリフと画像ファイル名どちらも返ってきた。


## 複数の画像をランダムに表示する

### 1. 画像をリサイズする
画像のファイルサイズが大きいとアップロードに時間がかかってしまう。
[こちらの記事](https://blog.ideamans.com/2018/07/mac-preview.html)を参考にしました。

### 2. 画像ファイルを scripts/images に移動

### 3. 画像ファイルのパスをまとめる json ファイルを作成

```
touch scripts/path.json
```

```json:scripts/path.json
{
  "imagePaths": [
    "IMG_8952.jpeg",
    "IMG_8953.jpeg",
    "IMG_8955.jpeg"
  ]
} 
```

### 4. 画像がランダム表示されるようにコードを追記した。

```js:hello.js
  'use strict';
  const { exec } = require('child_process');
  const data = require('./data.json');
  const words = data.slimelife;
+ const paths = require('./path.json');
+ const imagePaths = paths.imagePaths;
  module.exports = (robot) => {
    robot.hear(/スライム/i, (msg) => {
      const user_id = msg.message.user.id;
      const word = words[Math.floor(Math.random() * words.length)];
+     const imagePath = imagePaths[Math.floor(Math.random() * imagePaths.length)];  
      msg.send(`<@${user_id}> ${word}`);
+     const filename = `scripts/images/${imagePath}`;
      const room = msg.envelope.room;
      if (room === "Shell") {
+       msg.send(`表示される画像ファイルは、${filename}です。`);
      } else {
        const channel = msg.message.rawMessage.channel;
        return exec("curl -F file=@" + filename + " -F channels=" + channel + " -F token=" + process.env.HUBOT_SLACK_TOKEN + " https://slack.com/api/files.upload", function(err, stdout, stderr) {
          if (err) {
            console.error(err);
          }
        });
      }
    });
  };
```

### 5. 実装

【Slackに繋がないで実行した時】

```
bin/hubot

 ーーーーーーーーーーー
TextMessage {
  user:
   User { id: '1', _getRobot: [Function], name: 'Shell', room: 'Shell' },
  done: false,
  room: 'Shell',
  text: 'スライム',
  id: 'messageId' }
ーーーーーーーーーーー
undefined
ーーーーーーーーーーー
{ room: 'Shell',
  user:
   User { ... },
  message:
   TextMessage { ... } 
}
ーーーーーーーーーーー
<@1> 今回のダンジョンでなにをしたか思い出しなさい。まず美味しいカレーを作ったでしょ？お化けも倒して闇の試練も乗り越えてフェンリルとも友達になったでしょ。この経験が今後に活きない訳ないじゃん。近づいてるよなんでも技師
表示される画像ファイルは、scripts/images/IMG_8955.jpegです。
```


【Slackに繋げて実行した時】

```
env HUBOT_SLACK_TOKEN=控えておいた文字列 bin/hubot --adapter slack
```

![スクリーンショット 2020-08-15 10.25.06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384836/9274814e-ad54-808e-bbda-fa7f56179a6e.jpeg)

無事 Slack でランダムに可愛いスライムの画像を表示できた。

# 参考記事
・[Hubotからslackに画像を投稿してみる - Qiita](https://qiita.com/kijitora-neko/items/a976a08da080e2376858)
・[javascriptのcoffeescript、cssとscssの変換ならこのサービスで - Qiita](https://qiita.com/G-awa/items/57bceba48ef20626a604)
・[Node.js｜シェルコマンドを実行する方法(child_process) - わくわくBank](https://www.wakuwakubank.com/posts/728-nodejs-child-process/)
・[Macだけでできる！画像をリサイズして、ファイルサイズを軽量化しよう！[プレビュー.app編] - アイデアマンズブログ](https://blog.ideamans.com/2018/07/mac-preview.html)