Slack Botから駅すぱあとWebAPIを顔文字アイコンで対話的に利用してみる話。

[ヴァル研究所 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/val)、8日目は駅すぱあとWeb APIをSlack Botから使ってみる例を紹介してみようと思います。

## Slack Botから駅すぱあとfor web URL生成機能を利用する

[5日目の記事](http://qiita.com/valsitoh/items/fa6d18adfcdc145394a1)でRubyから駅すぱあとWebAPIを使ってみる例を紹介しました。Rubyには[slack-api](http://www.rubydoc.info/gems/slack-api/)という、[Slack API](https://api.slack.com)を利用できるライブラリが存在しています。
このライブラリを使って、Slack Botから駅すぱあとWeb APIを使ってみます。

### 準備

まずはgemの準備です。以下のようにして必要なライブラリをインストールします。

```
$ cat <<_EOF > Gemfile
source 'https://rubygems.org'
gem 'slack-api'
_EOF
$ bundle install --path vender/bundle
```

ソースコードは以下のようになります。[5日目の記事](http://qiita.com/valsitoh/items/fa6d18adfcdc145394a1)で解説したWebAPIの呼び出しを、Slackへの投稿に絡めて実行する形になっています。
Slackに投稿された内容(文字列)は`data["text"]`で取得できます。今回は「roote:東京から高円寺」のような投稿から駅名(東京、高円寺)を抜き出して[駅すぱあと for web URL生成](http://docs.ekispert.com/v1/le/api/search/course/light.html)のWrbAPIを呼び出しています。

```ruby
#!/usr/bin/env ruby
# coding: utf-8

require 'net/http'
require 'uri'
require 'erb'
include ERB::Util
require 'json'

require 'slack'

Slack.configure {|config|
  config.token = ENV['SLACK_TOKEN']
}
client = Slack.realtime

key = ENV['EKISPERT_ACCESS_KEY']
stinfo_cache = {}
icons = [ 
  'one', 'two',   'three', 'four', 'five',
  'six', 'seven', 'eight', 'nine', 'keycap_ten'
]

# URLリクエストを投げる
def request(url_str)
  url = URI.parse(url_str)
  req = Net::HTTP::Get.new(url.request_uri)
  
  if ENV['http_proxy'] != nil
    ENV['http_proxy'] =~ /http:\/\/(.*?):(.*?)\//
    proxy = Net::HTTP::Proxy($1, $2)
    res = proxy.start(url.host, url.port) {|http|
      http.request(req)
    }
  else
    Net::HTTP.start(url.host, url.port) {|http|
      http.request(req)
    }
  end
end

start_time = Time.now.to_i

client.on :message do |data|
  post_time = data["ts"].sub(/\..*$/, "").to_i
  next if post_time < start_time

  msg = data["text"]

  if msg =~ /^roote:(.*?)から(.*?)$/
    from_stname = $1 
    to_stname   = $2

    begin
      # 出発駅の駅コードを得る
      url_str   = "http://api.ekispert.jp/v1/json/station/light?key=#{key}&name=#{url_encode(from_stname)}"
      from_code = JSON.parse(request(url_str).body)["ResultSet"]["Point"][0]["Station"]["code"]

      # 到着駅の駅コードを得る
      url_str = "http://api.ekispert.jp/v1/json/station/light?key=#{key}&name=#{url_encode(to_stname)}"
      to_code = JSON.parse(request(url_str).body)["ResultSet"]["Point"][0]["Station"]["code"]

      # 探索結果を駅すぱあと for webのURLとして取得する
      url_str = "http://api.ekispert.jp/v1/json/search/course/light?key=#{key}&from=#{from_code}&to=#{to_code}"
      result_url = JSON.parse(request(url_str).body)["ResultSet"]["ResourceURI"]

      result_str = '```' << "\n"
      result_str = result_str << result_url << "\n"
      result_str = result_str << '```'

      param = {
        token:   ENV['SLACK_TOKEN'],
        channel: data["channel"],
        text:    result_str,
        username: "経路探索(#{from_stname}→#{to_stname})",
        icon_url: 'https://ekiworld.net/ekiworld/info/images/machinami.png'
      }
    rescue Exception => e
      param = {
        token:   ENV['SLACK_TOKEN'],
        channel: data["channel"],
        text:    "(>_<)\n" << "#{url_str}",
        username: "ERROR",
        icon_url: 'https://ekiworld.net/ekiworld/info/images/machinami.png'
      }
    end
    Slack.chat_postMessage(param)

  end

end
STDERR.puts("ok.")

client.start
```

スクリプトを実行してみます。

```
$ bundle exec ruby EkiRouteBot.rb 
ok.
```

この状態でSlackから「rooute:東京から高円寺」と投稿してみます。

![img900.png](https://qiita-image-store.s3.amazonaws.com/0/152217/a9482eef-f2cd-1527-74d3-0e25aa7ef28a.png)

Slack Botが[駅すぱあと for WEB](https://roote.ekispert.net)への探索結果URLをレスポンスとして返してくれました。

![img901.png](https://qiita-image-store.s3.amazonaws.com/0/152217/e593ab2f-2221-1527-3d3c-cfe14ea097d4.png)

URLをクリックすると、探索結果のページが開きます。

![img902.png](https://qiita-image-store.s3.amazonaws.com/0/152217/646c4794-279e-7705-00c0-1d88921992db.png)

## Slack Botとアイコンで対話的処理してみる

無事にSlack Botから駅すぱあとWebAPIを利用できたのですが、文字列入力→探索結果表示なので、もう一歩すすんだSlack Botの活用を考えてみたいものです。
そこで応用例として、Slack Botの応答に対して、ユーザーが顔文字アイコンでリアクションすることで、Botに対話的な処理をさせてみます。

### サンプルの概要

駅すぱあとWebサービスの機能の一つに、[駅付加情報](http://docs.ekispert.com/v1/api/station/info.html)があります。これは指定した駅について、乗り入れ路線や最寄り路線、福祉施設情報や出口情報を返す機能です。
例えば、東京駅(駅コードは`22828`)について得られる駅付加情報の種類を取得してみます。

```shell-session
$ curl -s -XGET 'http://api.ekispert.jp//v1/json/station/info?key=<アクセスキー>&code=22828&type=welfare'|jq '.ResultSet.Information.WelfareFacilities[].Name'
```

```json
"バリアフリー状況"
"エレベータ"
"エスカレータ"
"トイレ"
"スロープ"
"その他"
```

以下のような形で、それぞれの駅付加情報を取得できます。

```shell-session
$ curl -s -XGET 'http://api.ekispert.jp//v1/json/station/info?key=<アクセスキー>/v1/json/station/info?code=22828&type=welfare'|jq '.ResultSet.Information.WelfareFacilities[0].Comment'
```

```json
"　※段差なしでの移動経路\n（○：有り　△：要駅員設備　×：無し）\n【ＪＲ東日本】【ＪＲ東海】：○\n【東京メトロ】：○"
```

そこで以下のように「stname:東京」のように入力した場合に、指定した駅の福祉施設情報の候補を一覧表示し、それに対してユーザーが取得したい情報の番号をアイコンで指定するとSlack Botが詳細情報を返してくる、という処理にしてみましょう。

![img904.png](https://qiita-image-store.s3.amazonaws.com/0/152217/89cc08e1-d71d-4921-1ef3-6bebd49a9d12.png)

### ソースコードの解説

ソースコード全体は以下のようになります。Slack Botと対話的に処理している部分について簡単に補足します。

```ruby
#!/usr/bin/env ruby
# coding: utf-8

require 'net/http'
require 'uri'
require 'erb'
include ERB::Util
require 'json'

require 'slack'

Slack.configure {|config|
  config.token = ENV['SLACK_TOKEN']
}
client = Slack.realtime

key = ENV['EKISPERT_ACCESS_KEY']
stinfo_cache = {}
icons = [ 
  'one', 'two',   'three', 'four', 'five',
  'six', 'seven', 'eight', 'nine', 'keycap_ten'
]

# URLリクエストを投げる
def request(url_str)
  url = URI.parse(url_str)
  req = Net::HTTP::Get.new(url.request_uri)
  
  if ENV['http_proxy'] != nil
    ENV['http_proxy'] =~ /http:\/\/(.*?):(.*?)\//
    proxy = Net::HTTP::Proxy($1, $2)
    res = proxy.start(url.host, url.port) {|http|
      http.request(req)
    }
  else
    Net::HTTP.start(url.host, url.port) {|http|
      http.request(req)
    }
  end
end

start_time = Time.now.to_i
client.on :reaction_added do |data|
  item = stinfo_cache[data["item"]["ts"]]

  stname = item["stname"]
  stcode = item["stcode"]

  reaction = data["reaction"]

  # 駅情報APIから駅にある福祉施設一覧を取得する
  url = "http://api.ekispert.jp/v1/json/station/info?key=#{key}&code=#{stcode}&type=welfare"
  result = JSON.parse(request(url).body)["ResultSet"]["Information"]["WelfareFacilities"]

  idx  = icons.index(reaction)
  if idx != nil
    name    = result[icons.index(reaction)]["Name"]
    comment = result[icons.index(reaction)]["Comment"]

    param = {
      token:   ENV['SLACK_TOKEN'],
      channel: data["item"]["channel"],
      text:    stname << '駅の ' << name << ' 情報:' << "\n" << comment << "\n",
      username: "駅情報(#{stname}:#{stcode}):",
      icon_url: 'https://ekiworld.net/ekiworld/info/images/machinami.png'
    }
    Slack.chat_postMessage(param)
  end

end

client.on :message do |data|
  post_time = data["ts"].sub(/\..*$/, "").to_i
  next if post_time < start_time

  msg = data["text"]

  username = data["username"]
  if username =~ /駅情報\((.*?):(.*?)\)/
    stname = $1 # 駅名
    stcode = $2 # 駅コード
    ts = data["ts"] # timestamp

    stinfo_cache[ts.to_s] = {} if stinfo_cache[ts.to_s] == nil
    stinfo_cache[ts.to_s]["stname"] = stname
    stinfo_cache[ts.to_s]["stcode"] = stcode
  end

  if msg =~ /^stinfo:(.*?)$/
    stname = $1

    # 駅コードを得る
    url = "http://api.ekispert.jp/v1/json/station/light?key=#{key}&name=#{url_encode(stname)}"
    stcode = JSON.parse(request(url).body)["ResultSet"]["Point"][0]["Station"]["code"]

    # 駅情報APIから駅にある福祉施設一覧を取得する
    url = "http://api.ekispert.jp/v1/json/station/info?key=#{key}&code=#{stcode}&type=welfare"
    result = JSON.parse(request(url).body)["ResultSet"]["Information"]["WelfareFacilities"]

    msg = "#{stname}(#{stcode}):" << "\n"
    result.each_with_index do |item, idx|
      msg = msg << ':' << icons[idx] << ': ' << item["Name"] << "\n"
    end

    param = {
      token:   ENV['SLACK_TOKEN'],
      channel: data["channel"],
      text:    msg,
      username: "駅情報(#{stname}:#{stcode}):",
      icon_url: 'https://ekiworld.net/ekiworld/info/images/machinami.png'
    }
    Slack.chat_postMessage(param)

  end
end
STDERR.puts("ok.")

client.start
```

ユーザーが「stinfo:東京」と入力した際に取得した[駅付加情報](http://docs.ekispert.com/v1/api/station/info.html)をハッシュの形で保持しておきます。ハッシュのキーはレスポンスとして返されたデータのタイムスタンプを使用します。アイコンによるリアクションの際、このタイムスタンプによってリアクションがあった投稿を紐付ける形になります。
具体的には以下の部分です。Slack Botのレスポンスデータの`data["username"]`に駅名と駅コードを含めるようにしているので、それをハッシュのデータとして保存しておきます。

```ruby
client.on :message do |data|
  post_time = data["ts"].sub(/\..*$/, "").to_i
  next if post_time < start_time

  msg = data["text"]

  username = data["username"]
  if username =~ /駅情報\((.*?):(.*?)\)/
    stname = $1 # 駅名
    stcode = $2 # 駅コード
    ts = data["ts"] # timestamp

    stinfo_cache[ts.to_s] = {} if stinfo_cache[ts.to_s] == nil
    stinfo_cache[ts.to_s]["stname"] = stname
    stinfo_cache[ts.to_s]["stcode"] = stcode
  end
  ...
```

ユーザーがアイコンでリアクションすると、`client.on :reaction_added do |data| { ... }`ブロックが実行されます。ここでリアクション先の投稿のタイムスタンプとハッシュに保存しておいた駅コードから再度、[駅付加情報](http://docs.ekispert.com/v1/api/station/info.html)を取得し、ユーザが指定した番号の駅付加情報詳細を取得します。

```ruby
client.on :reaction_added do |data|
  item = stinfo_cache[data["item"]["ts"]]

  stname = item["stname"]
  stcode = item["stcode"]

  reaction = data["reaction"]

  # 駅情報APIから駅にある福祉施設一覧を取得する
  url = "http://api.ekispert.jp/v1/json/station/info?key=#{key}&code=#{stcode}&type=welfare"
  result = JSON.parse(request(url).body)["ResultSet"]["Information"]["WelfareFacilities"]

  idx  = icons.index(reaction)
  if idx != nil
    name    = result[icons.index(reaction)]["Name"]
    comment = result[icons.index(reaction)]["Comment"]

    param = {
      token:   ENV['SLACK_TOKEN'],
      channel: data["item"]["channel"],
      text:    stname << '駅の ' << name << ' 情報:' << "\n" << comment << "\n",
      username: "駅情報(#{stname}:#{stcode}):",
      icon_url: 'https://ekiworld.net/ekiworld/info/images/machinami.png'
    }
    Slack.chat_postMessage(param)
  end

end
```

### 動かしてみる

さっそく動作を試してみます。「stinfo:東京」と入力して駅付加情報(福祉施設情報)の候補一覧が返されている状態で、顔文字アイコンでリアクションしてみます。

<img width="393" alt="img905.png" src="https://qiita-image-store.s3.amazonaws.com/0/152217/23bbe5d5-05cc-d099-fe36-66c4c88a7d3d.png">

想定通りSlack Botが反応し、東京駅のバリアフリー状況を取得することができました。エレベータなどの他の情報についてもアイコンでリアクションすることで同様に取得できます。

![img907.png](https://qiita-image-store.s3.amazonaws.com/0/152217/09b1bcda-44f0-12ca-1bb3-9850a80b4afc.png)

## まとめ

駅すぱあとWebAPIとSlack Botを組み合わせて使用する例を紹介してみました。単純にBotにレスポンスを返させるだけでなく、顔文字アイコンなどでBotに対話的な処理を行わせるようにすると、複数ある駅名候補からユーザーがどれか一つ選択するといった応用ができそうです。