SlackのSlash CommandsをApp Engineで稼働させる

こんにちは、Pairs事業部のkaneshinです!

GWなので自己研鑽として勉強しようと意気込んでいる人も多いと思いますが何を勉強していいのかわからない方もいると思うので、本日はそんな方へSlackのIntegrationの一つ「Slash Commands」をGoogle App Engineで動かす簡単なサンプルを紹介しようと思います。

Slack — Slash Commands

slack_monochrome_black

SlackではSlash Commandsという機能を各自が追加することが可能です。
Real Time Messaging APIを利用したHubotをBotとしてSlackと連携させることは最近では珍しくありませんが、簡単な機能追加ならばSlash Commandsだけで実現可能です。

使い方(実装方法)

Slash Commandsの使い方は「https://api.slack.com/slash-commands」に記載がありますが、大きく分けると下記のようになります。

1. コマンドの定義

/hello のように、コマンドを定義します。既にSlackが用意しているものや別のIntegrationで定義されているものを上書きすることはできません。

2. コマンド実行

仮に、/hello コマンドが定義されたとして、これを実行されると指定しているURLにPOSTのリクエストが下記のようなデータと共に送信されてきます。

token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=94070
response_url=https://hooks.slack.com/commands/1234/5678

※これらのデータを使って処理を実装しますが、データを完全無視してもPOSTのリクエストをハンドリングしてSlash Commandsを実装することも可能です。

3. トークンを検証

不正なリクエストでないかを token を用いて行います。 token 自体は無視しても可能ですが、無闇に叩かれたら困る場合は検証するロジックを実装しておいたほうが無難です。

4. コマンドにレスポンスを返却

レスポンスに下記のようなデータを追記して返却することでSlackにメッセージが表示されます。

{
"response_type": "in_channel",
"text": "It's 80 degrees right now.",
"attachments": [
{
"text":"Partly cloudy today and tomorrow"
}
]
}

response_typeephemeralを指定すると自分だけしか見えないメッセージになりますが、あまり使用しないと思います。

attachmentsに指定できるJSONはこちらを参考にしてください。

Slash Commands + App Engine

gcp_gae_logos.jpg

Slash CommandsのIntegrationについては説明したので早速実装に入りたいと思います。
App EngineでHTTPをハンドリングするために今回はGoCon 2016 Spring でvvakameさんが紹介していたuconというフレームワークを使用します。

実装

HTTPリクエスト(for App Engine)

uconnet/httpの世界観を崩さないで利用できるためとても素晴らしいです。

package app
import (
"net/http"
"github.com/favclip/ucon"
)
func init() {
ucon.Orthodox()
ucon.HandleFunc("POST", "/command", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
ucon.DefaultMux.Prepare()
http.Handle("/", ucon.DefaultMux)
}
今回は下記のように/commandというパスにPOSTメソッドでリクエストが来ることを想定したハンドリングを用意します。
ucon.HandleFunc("POST", "/command", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
}
つまり、 https://[project-id].appspot.com/commandでPOSTリクエストを受け取ることを想定しています。
App Engineへデプロイ
appdirにApp Engineの設定となるapp.yamlが存在していると仮定すれば下記のようなコマンドでデプロイが可能です。
$ goapp deploy -application=[project-id] /path/to/appdir
Slash Commandsの設定
URLが定まったので、Slash CommandsのIntegrationを設定します。今回は/yakisobaというコマンドを作成します。
これでSlack上で /yakisoba コマンドが定義されました。コマンドのヘルプを表示したい場合は下記のように設定することでヘルプを表示することが可能になります。
ね、簡単でしょう?
Slackへのレスポンス
さて、早速定義されコマンドを叩いてもまだ Hello World!しか返却していないので下記のように自分にしか見えないメッセージが返却されます。
では、まずは全員に見えるメッセージとして「オープンソース!」と発言するように実装します。
ucon.HandleFunc("POST", "/command", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
data := map[string]string{
"response_type": "in_channel",
"text": "オープンソース!",
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(data)
w.Write(buf.Bytes())
})
このように実装して、/yakisobaを実行すると下記のようにメッセージが表示されます。
これだけだと少しつまらないので、こちらがコマンドを叩くときに「オープンソース」という文字を含めたら「焼きそば!」/yakisoba オープンソースをメッセージングするようにします。
ucon.HandleFunc("POST", "/command", func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
data := map[string]string{
"response_type": "in_channel",
}
if t := r.PostFormValue("text"); strings.Contains(t, "オープンソース") {
data["text"] = "焼きそば!"
} else {
data["text"] = "オープンソース!"
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(data)
w.Write(buf.Bytes())
})
今回のサンプルプロジェクトはこちらにて公開しています。
また、今回かなり省きましたが、前述したとおりtokenは各自で検証した方が良いです。
おわりに
Slash Commandsの触りだけを紹介しましたが、あとは各自のアイディア次第でいろいろ拡張することができると思います。例えば、サブコマンドを定義する(/yakisoba echo)しておいて、それに反応するようにしたり、Incoming Webhooksと連携してコマンド経由で何かメッセージを投稿したりなど。
実際、社内Slackにはkaonashiが定義されていたりします。
App Engineでホスティングしていれば開発だけに集中できるので、非常にオススメです。
GWにやることが決まっていない方は簡単なSlash Commandsを作成するのはいかがでしょうか。