LINE BOT API を Hubot から使える hubot-line を作った


[hubot-line](https://github.com/umakoz/hubot-line)という [Hubot](https://hubot.github.com/) のアダプタを作りました。これを使うと、HubotというボットフレームワークからLINEへスタンプやイメージ、メッセージなどの送受信が簡単にできます。何らかの機能を持ったボットを提供したいときにメインロジックだけを開発するだけで済むので、楽ができますね。

しかし、マニュアルが長くなってしまったので、読むのも面倒だけど使ってみたい僕のような人向けに[サンプルボット](https://github.com/umakoz/hubot-line-example)も作りました。

さらに、こんなサンプルひとつ渡されても、コード読み解くのめんどいという僕のような人向けに、このサンプルボットを元に使い方を解説します。

なお、LINE BOT APIとHubot、Herokuの知識をある程度お持ちの方向けの解説になります。有用な記事がいくつもありますし、そちらを参考にしてください。そこまでは面倒すぎて面倒みきりません。ということで早速。

ボットのロジックは `./scripts/line.coffee` に全て書いてあります。  

## インポート

まず、先頭でスクリプトから使用するオブジェクトのインポートが必要です。

```coffeescript
{LineRawMessageListener, LineImageListener, LineVideoListener, LineAudioListener, LineLocationListener,
LineStickerListener, LineContactListener, LineRawOperationListener, LineFriendListener, LineBlockListener,
LineTextAction, LineImageAction, LineVideoAction, LineAudioAction, LineLocationAction, LineStickerAction
} = require 'hubot-line'
```

## メッセージを受け取る
### スタンプ

サンプルボットはスタンプを受け取ったとき、同じスタンプを返すという動作をします。次のコードがそれをしています。

```coffeescript
  robot.listeners.push new LineStickerListener robot, (() -> true), (res) ->
    res.emote new LineStickerAction res.message.STKID, res.message.STKPKGID
```

`LineStickerListener`を全スタンプを受け取るリスナーとして登録します。スタンプを受け取ったら、そのパラメーター`STKID`と`STKPKGID`から`LineStickerAction`を作って`emote`すると、同じスタンプを返信します。  
もし、受信するスタンプを限定したかったら、matcherの`(() -> true)`をカスタマイズしてください。`((msg) -> msg.STKID is 1)`みたいな感じです。

### イメージ

次に、サンプルボットはイメージを受け取ったとき、同じイメージを送り返すという動作をします。次のコードがそれをしています。

```coffeescript
  robot.listeners.push new LineImageListener robot, (() -> true), (res) ->
    res.message.content (content) ->
      robot.brain.set res.message.id, new Buffer(content, 'binary').toString('base64')
      originalContentUrl = "#{contentEndpoint}?id=#{res.message.id}"
      res.message.previewContent (previewContent) ->
        robot.brain.set "p#{res.message.id}", new Buffer(previewContent, 'binary').toString('base64')
        previewImageUrl = "#{contentEndpoint}?id=p#{res.message.id}"
        res.emote new LineImageAction originalContentUrl, previewImageUrl
```

スタンプと違うのは、イメージは受信したときにデータが含まれておらず、別にダウンロードしてくる必要があるということです。`res.message.content (content) ->`でイメージデータを、`res.message.previewContent (previewContent) ->`でプレビューデータをダウンロードできます。  
イメージを送信するときも同様で、APIでデータ自体は送信せず、`originalContentUrl`や`previewImageUrl`のようにURLを渡します。LINEプラットフォームはそのURLからデータをダウンロードするという方式になっています。
Herokuにはファイルを置けないため、サンプルではデータをBase64エンコードしてRedisに突っ込んでいます。そして以下のコードで、LINEプラットフォームにデータを提供しています。

```coffeescript
  robot.router.get "/download", (req, res) =>
    content = robot.brain.get req.query.id
    robot.brain.remove req.query.id
    res.set('Content-Type', 'binary')
    res.send new Buffer(content, 'base64')
```

Redisの容量制限もあるので、データをダウンロードさせたら、すぐにデータを消しています。それでも、キャパオーバーなど心配ですし、このような処理のままリリースしたりせず、適切なストレージを用意しましょう。これはサンプルだから許されるいい加減さです。  
また、プレビューをオウム返ししていますが、送信時の方がイメージサイズの上限が厳しいようで、ダウンロードに失敗しています。240x240にリサイズしましょう。これはサンプルだからry

### ビデオ

次に、サンプルボットはビデオを受け取ったとき、サンプルボットのリポジトリに含まれるビデオを送り返すという動作をします。次のコードがそれをしています。

```coffeescript
  robot.listeners.push new LineVideoListener robot, (() -> true), (res) ->
    res.message.content (content) ->
      originalContentUrl = "https://github.com/umakoz/hubot-line-example/raw/master/content/video.mp4"
      res.message.previewContent (previewContent) ->
        previewImageUrl = "https://github.com/umakoz/hubot-line-example/raw/master/content/video.jpg"
        res.emote new LineVideoAction originalContentUrl, previewImageUrl
```

やってることはほとんどイメージと同じですね。しかし何故かRedisに突っ込むパターンでは動作しなかったので、諦めました。

### 音声

サンプルボットは音声を受け取ったとき、サンプルボットのリポジトリに含まれる音声の先頭1秒を送り返すという動作をします。次のコードがそれをしています。

```coffeescript
  robot.listeners.push new LineAudioListener robot, (() -> true), (res) ->
    res.message.content (content) ->
      robot.brain.set res.message.id, new Buffer(content, 'binary').toString('base64')
      originalContentUrl = "#{contentEndpoint}?id=#{res.message.id}"
      res.emote new LineAudioAction originalContentUrl, 1000
```

先頭1秒になってるのは、音声データから秒数を取得するのが面倒だったためです。

### 位置情報

サンプルボットは位置情報を受け取ったとき、住所と緯度、経度を次の通り返信しています。

```coffeescript
  robot.listeners.push new LineLocationListener robot, (() -> true), (res) ->
    res.emote new LineLocationAction res.message.address, res.message.latitude, res.message.longitude
```

### 連絡先

サンプルボットは連絡先を受け取ったとき、以下の通り、MIDと名前を返信しています。テキストを返すときは`emote`ではなく`send`を使います。

```coffeescript
  robot.listeners.push new LineContactListener robot, (() -> true), (res) ->
    res.send "got a contact. mid: #{res.message.mid} displayName: #{res.message.displayName}"
```

## オペレーションを受け取る

### 友達登録

サンプルボットは新しい友達が増えた（もしくはブロック解除された）という通知を受け取ったとき、次の通りMIDを返信しています。

```coffeescript
  robot.listeners.push new LineFriendListener robot, (() -> true), (res) ->
    res.send "be a friend. mid: #{res.message.mid}"
```

### ブロック

サンプルボットはブロックされた（もしくは友達が減った）という通知を受け取ったとき、何もしません。ブロックされてるので、どうせ何も送れませんからね。

```coffeescript
  robot.listeners.push new LineBlockListener robot, (() -> true), (res) ->
```

テキストについては、次の通りHubotの通常のやり方で大丈夫です。ただ、サンプルボットは名無しで作ってますので、名前は入れなくて結構です。`badger`と送ると、`Badgers?...`と返してくれますね。

```coffeescript
  robot.respond /badger/i, (res) ->
    res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"
```

そのほかメッセージをまとめて受け取るための`LineRawMessageListener`と、オペレーションをまとめて受け取るための`LineRawOperationListener`が用意されていますので、使ってみてください。

一応、サンプルボットはHeroku へデプロイするという想定で書いていますが、サーバーと node.js、固定グローバルIP、Redisがあれば動作するはずです。

解説は以上です。

## あとがき

最後に、僕は基本iOSプログラマーなので、CoffeeScriptは全然詳しくありません。Hubotと他のアダプタのコードから学んだ知識しかないので、プルリクは大歓迎です。  
あと、英語を勉強しているので頑張ってドキュメント書いたけど、途中で諦めて残念な感じになっているので、そっちの指摘も大歓迎です。  
ということで、型安全な世界に戻ります。Swift書こう。