ヴァル研究所 Advent Calendar 2016、8日目は駅すぱあとWeb APIをSlack Botから使ってみる例を紹介してみようと思います。
Slack Botから駅すぱあとfor web URL生成機能を利用する
5日目の記事でRubyから駅すぱあとWebAPIを使ってみる例を紹介しました。Rubyにはslack-apiという、Slack APIを利用できるライブラリが存在しています。
このライブラリを使って、Slack Botから駅すぱあとWeb APIを使ってみます。
準備
まずはgemの準備です。以下のようにして必要なライブラリをインストールします。
$ cat <<_EOF > Gemfile
source 'https://rubygems.org'
gem 'slack-api'
_EOF
$ bundle install --path vender/bundle
ソースコードは以下のようになります。5日目の記事で解説したWebAPIの呼び出しを、Slackへの投稿に絡めて実行する形になっています。
Slackに投稿された内容(文字列)はdata["text"]
で取得できます。今回は「roote:東京から高円寺」のような投稿から駅名(東京、高円寺)を抜き出して駅すぱあと for web URL生成のWrbAPIを呼び出しています。
#!/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:東京から高円寺」と投稿してみます。
Slack Botが駅すぱあと for WEBへの探索結果URLをレスポンスとして返してくれました。
URLをクリックすると、探索結果のページが開きます。
Slack Botとアイコンで対話的処理してみる
無事にSlack Botから駅すぱあとWebAPIを利用できたのですが、文字列入力→探索結果表示なので、もう一歩すすんだSlack Botの活用を考えてみたいものです。
そこで応用例として、Slack Botの応答に対して、ユーザーが顔文字アイコンでリアクションすることで、Botに対話的な処理をさせてみます。
サンプルの概要
駅すぱあとWebサービスの機能の一つに、駅付加情報があります。これは指定した駅について、乗り入れ路線や最寄り路線、福祉施設情報や出口情報を返す機能です。
例えば、東京駅(駅コードは22828
)について得られる駅付加情報の種類を取得してみます。
$ curl -s -XGET 'http://api.ekispert.jp//v1/json/station/info?key=<アクセスキー>&code=22828&type=welfare'|jq '.ResultSet.Information.WelfareFacilities[].Name'
"バリアフリー状況"
"エレベータ"
"エスカレータ"
"トイレ"
"スロープ"
"その他"
以下のような形で、それぞれの駅付加情報を取得できます。
$ 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'
" ※段差なしでの移動経路\n(○:有り △:要駅員設備 ×:無し)\n【JR東日本】【JR東海】:○\n【東京メトロ】:○"
そこで以下のように「stname:東京」のように入力した場合に、指定した駅の福祉施設情報の候補を一覧表示し、それに対してユーザーが取得したい情報の番号をアイコンで指定するとSlack Botが詳細情報を返してくる、という処理にしてみましょう。
ソースコードの解説
ソースコード全体は以下のようになります。Slack Botと対話的に処理している部分について簡単に補足します。
#!/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:東京」と入力した際に取得した駅付加情報をハッシュの形で保持しておきます。ハッシュのキーはレスポンスとして返されたデータのタイムスタンプを使用します。アイコンによるリアクションの際、このタイムスタンプによってリアクションがあった投稿を紐付ける形になります。
具体的には以下の部分です。Slack Botのレスポンスデータのdata["username"]
に駅名と駅コードを含めるようにしているので、それをハッシュのデータとして保存しておきます。
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| { ... }
ブロックが実行されます。ここでリアクション先の投稿のタイムスタンプとハッシュに保存しておいた駅コードから再度、駅付加情報を取得し、ユーザが指定した番号の駅付加情報詳細を取得します。
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:東京」と入力して駅付加情報(福祉施設情報)の候補一覧が返されている状態で、顔文字アイコンでリアクションしてみます。
想定通りSlack Botが反応し、東京駅のバリアフリー状況を取得することができました。エレベータなどの他の情報についてもアイコンでリアクションすることで同様に取得できます。
まとめ
駅すぱあとWebAPIとSlack Botを組み合わせて使用する例を紹介してみました。単純にBotにレスポンスを返させるだけでなく、顔文字アイコンなどでBotに対話的な処理を行わせるようにすると、複数ある駅名候補からユーザーがどれか一つ選択するといった応用ができそうです。