概要
Slack Botで実行したいコマンドは通常hubotなどで立ち上げてその中にコードを記述することが多いと思いますが、今回はコマンドの処理を全てニフティクラウドのスクリプトというサービスで一元管理してみたいと思います。
さらにgolangのアプリケーションからスクリプトを呼び出して、結果をチャットに返すというボットを作成します。
ニフティクラウドスクリプトとはAWSのLambdaみたいなもので、サーバーサイドのスクリプトをクラウド上で実行できるサービスです。
メリット
botコマンドを全てニフティクラウドスクリプトにすることで、クライアント側に囚われず同じ処理を実行できるようになります。
例えばSlack以外の別のチャットツールを使うことになった場合も、ニフティクラウドスクリプトを呼び出すことで同じコマンドを実行することができます。
botコマンドのソースをエンハンスする場合、ニフティクラウドスクリプトのコードを修正するだけで全てのクライアントから呼び出すコマンドに修正が反映されるので、デプロイも簡単です。
作る
slackでBot Userを作る
まず https://my.slack.com/services/new/bot からBot Userを作成します。
作成が完了したら、APIトークンをめもっておきましょう。
ニフティクラウドスクリプトのセットアップ
続いてコントロールパネルから以下のような簡単なスクリプトをping.jsという名前で作成します。
module.exports = (req, res) => {
res.send("pong")
}
golangでslack botを作る
以下の3ファイルに分けて記述します
botを生成し、webSocketから情報を取得する
package main
import (
"os"
"strings"
"github.com/nlopes/slack"
)
var (
botId string
botName string
)
func main() {
token := os.Getenv("SLACK_API_TOKEN")
bot := NewBot(token)
go bot.rtm.ManageConnection()
for {
select {
case msg := <-bot.rtm.IncomingEvents:
switch ev := msg.Data.(type) {
case *slack.ConnectedEvent:
botId = ev.Info.User.ID
botName = ev.Info.User.Name
case *slack.MessageEvent:
user := ev.User
text := ev.Text
channel := ev.Channel
if ev.Type == "message" && strings.HasPrefix(text, "<@"+botId+">") {
bot.handleResponse(user, text, channel)
}
}
}
}
}
ポイント
- golangからSlackを操作するためにgithub.com/nlopes/slackというライブラリを使います
- 環境変数SLACK_API_TOKENに作成したボットのAPI_TOKENを設定しておきます
-
@bot名
のメッセージがきた場合のみレスポンスを返すようにします
botのコマンドを実行する
package main
import (
"github.com/nlopes/slack"
"github.com/smartystreets/go-aws-auth"
"fmt"
"os"
"strings"
)
const botIcon = ":innocent:"
var (
commands = map[string]string{
"help": "Displays all of the help commands.",
"ping": "Reply with pong.",
}
)
type Bot struct {
api *slack.Client
rtm *slack.RTM
}
func NewBot(token string) *Bot {
bot := new(Bot)
bot.api = slack.New(token)
bot.rtm = bot.api.NewRTM()
return bot
}
func (b *Bot) handleResponse(user, text, channel string) {
var cmd string
commandArray := strings.Fields(text)
if len(commandArray) <= 1 {
cmd = "help"
} else {
cmd = commandArray[1]
}
var attachment slack.Attachment
var err error
switch cmd {
case "ping":
attachment, err = b.ping()
case "help":
attachment = b.help()
default:
attachment = b.help()
}
if err != nil {
b.rtm.SendMessage(b.rtm.NewOutgoingMessage(fmt.Sprintf("Sorry %s is error... %s", cmd, err), channel))
return
}
params := slack.PostMessageParameters{
Attachments: []slack.Attachment{attachment},
Username: botName,
IconEmoji: botIcon,
}
_, _, err = b.api.PostMessage(channel, "", params)
if err != nil {
b.rtm.SendMessage(b.rtm.NewOutgoingMessage(fmt.Sprintf("Sorry %s is error... %s", cmd, err), channel))
return
}
}
func (b *Bot) help() (attachment slack.Attachment) {
fields := make([]slack.AttachmentField, 0)
for k, v := range commands {
fields = append(fields, slack.AttachmentField{
Title: "@" + botName + " " + k,
Value: v,
})
}
attachment = slack.Attachment{
Pretext: botName + "Command List",
Color: "#B733FF",
Fields: fields,
}
return attachment
}
func (f *Bot) ping() (attachment slack.Attachment, err error) {
scriptClient := NewScriptClient(awsauth.Credentials{
AccessKeyID: os.Getenv("NIFTY_ACCESS_KEY_ID"),
SecretAccessKey: os.Getenv("NIFTY_SECRET_KEY")})
res, err := scriptClient.ExecuteScript(ExecuteScriptParams{ScriptIdentifier: "ping.js", Method: "GET"})
if err != nil {
return
}
attachment = slack.Attachment{
Pretext: res.ExecuteScriptResult.Result.ResponseData,
Color: "#A9F5F2",
}
return attachment, nil
}
ポイント
- 環境変数NIFTY_ACCESS_KEY_ID/NIFTY_SECRET_KEYに自分のアカウントに認証情報を設定しておきます
- ping が実行された場合ニフティクラウドスクリプトのping.jsを呼び出すようにします
- ping 以外のコマンドが実行された場合はhelpを表示しています
ニフティクラウドスクリプトを呼び出す
package main
import (
"encoding/xml"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/smartystreets/go-aws-auth"
)
const (
scriptEndpoint = "https://script.api.cloud.nifty.com/2015-09-01"
scriptVersion = "2015-09-01"
)
type ExecuteScriptParams struct {
ScriptIdentifier string
Method string
Query string
Body string
Header string
}
type ExecuteScriptResponse struct {
ExecuteScriptResult struct {
Result struct {
ResponseData string `xml:"ResponseData"`
} `xml:"Result"`
} `xml:"ExecuteScriptResult"`
}
type ErrorResponse struct {
Error Error `xml:"Error"`
RequestID string `xml:"RequestId"`
}
type Error struct {
Type string `xml:"Type"`
Message string `xml:"Message"`
Code string `xml:"Code"`
}
func (err Error) Error() string {
return err.Message
}
type ScriptClient interface {
ExecuteScript(params ExecuteScriptParams) (res *ExecuteScriptResponse, err error)
}
type scriptClient struct {
credentials []awsauth.Credentials
}
func NewScriptClient(credentials ...awsauth.Credentials) ScriptClient {
return &scriptClient{
credentials: credentials,
}
}
func (c *scriptClient) ExecuteScript(params ExecuteScriptParams) (res *ExecuteScriptResponse, err error) {
const action = "ExecuteScript"
if params.Query == "" {
params.Query = "{}"
}
if params.Body == "" {
params.Body = "{}"
}
if params.Header == "" {
params.Header = "{}"
}
body := map[string]string{
"ScriptIdentifier": params.ScriptIdentifier,
"Method": params.Method,
"Query": params.Query,
"Body": params.Body,
"Header": params.Header,
}
res = new(ExecuteScriptResponse)
err = c.doRequest(c.makeRequest(action, body), res)
return
}
func (c *scriptClient) makeRequest(action string, body map[string]string) (req *http.Request) {
endpoint, _ := url.Parse(scriptEndpoint)
encoceBody := url.Values{}
for k, v := range body {
encoceBody.Add(k, v)
}
req, _ = http.NewRequest("POST", endpoint.String(), strings.NewReader(encoceBody.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("X-Amz-Target", scriptVersion+"."+action)
awsauth.Sign4(req, c.credentials...)
return
}
func (c *scriptClient) doRequest(req *http.Request, result interface{}) (err error) {
res, err := http.DefaultClient.Do(req)
if err != nil {
return
}
dumpReq, _ := httputil.DumpRequest(req, true)
dumpRes, _ := httputil.DumpResponse(res, true)
if res.StatusCode != 200 {
fmt.Printf("%s", dumpReq)
fmt.Printf("%s", dumpRes)
var errResp = ErrorResponse{}
err = xml.NewDecoder(res.Body).Decode(&errResp)
if err != nil {
return
}
return errResp.Error
}
err = xml.NewDecoder(res.Body).Decode(&result)
defer res.Body.Close()
return
}
ポイント
- API認証のために、go-aws-authというライブラリを使用します
- レスポンスとして返されるxmlをパースして返却します
完成
go run *.go でbotを起動します。
Slack から @bot名 ping
を打って pongが返ってくれば成功です。
仕上げ
BOTが落ちないようにデーモン化させましょう
Dockerコンテナとして立ち上げるのがお勧めです
FROM golang:1.7
RUN go get github.com/nlopes/slack
RUN go get github.com/smartystreets/go-aws-auth
ADD . /go/src/mygit.com/myname/bot
RUN go install mygit.com/myname/bot
ENTRYPOINT /go/bin/bot
$ docker build -t bot .
$ docker run -d --env-file ./env bot