at kaneshin

Free space for me.

golang で始める Slack bot 開発

f:id:laplus-knsn:20161203150907j:plain

この記事は Slack Advent Calendar 2016 - Qiita の3日目の記事です。

昨日は Kinoppyd さんの「今そこにあるSlack」でした。

さて、今回、この記事では golang で Slack bot を実装する方法を紹介しようと思います。

世に蔓延る Slack bot

f:id:laplus-knsn:20161203155008j:plain

これから bot を世に放とうとしている人は、是非、1日目と2日目の記事を読み、事前知識を頭に叩き込んでおくと良いと思います。

基本的に、自身で作成した bot はもちろん好きになると思いますが、人によっては理解不能な bot や、意味不明な場面で反応したりと、「邪魔だな」と思われてしまうことがあります。そのため、bot を開発する人は「謙虚・尊敬・信頼」(Team Geekより)を持った上で、世に放ちましょう。

golang で bot を開発

f:id:laplus-knsn:20161203155235p:plain

Golang の開発環境は整っているとします。

bot の新規作成

Slack の bot の仕様については下記のページに色々と書かれています。

api.slack.com

アプリケーションに統合された bot の開発も可能ですが、今回は簡単に実装するために Slack Bots からトークンを取得します。

Slack Bots の新規作成はこちらから作成ができ、下記のようなページで新規作成となります。

f:id:laplus-knsn:20161203154451p:plain

作成が完了すると、下記の画像の設定ページに遷移するので、ここにあるトークンを使用して bot を稼働させます。

f:id:laplus-knsn:20161203154501p:plain

実装

Go で開発をすすめます。 今回、簡単に Slack API 連携するために github.com/nlopes/slack を先に go get しておきます。

$ go get -u github.com/nlopes/slack

コード全体

package main

import (
    "log"
    "os"

    "github.com/nlopes/slack"
)

func run(api *slack.Client) int {
    rtm := api.NewRTM()
    go rtm.ManageConnection()

    for {
        select {
        case msg := <-rtm.IncomingEvents:
            switch ev := msg.Data.(type) {
            case *slack.HelloEvent:
                log.Print("Hello Event")

            case *slack.MessageEvent:
                log.Printf("Message: %v\n", ev)
                rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel))

            case *slack.InvalidAuthEvent:
                log.Print("Invalid credentials")
                return 1

            }
        }
    }
}

func main() {
    api := slack.New("[YOUR-API-TOKEN]")
    os.Exit(run(api))
}

順を追って解説します。

slack.Client の生成

func main() {
    api := slack.New("[YOUR-API-TOKEN]")
    os.Exit(run(api))
}

slack.New 関数に先ほど取得したAPIトークンを渡して、 slack.Client を生成します。これは Slack API のクライアントになるので、bot 以外でももちろん API を叩くことが可能になります。

api := slack.New("[YOUR-API-TOKEN]")

Real Time Messaging API

func run(api *slack.Client) int {
    rtm := api.NewRTM()
    go rtm.ManageConnection()

    for {
        select {
        case msg := <-rtm.IncomingEvents:
            switch ev := msg.Data.(type) {
            case *slack.HelloEvent:
                log.Print("Hello Event")

            case *slack.MessageEvent:
                log.Printf("Message: %v\n", ev)
                rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel))

            case *slack.InvalidAuthEvent:
                log.Print("Invalid credentials")
                return 1

            }
        }
    }
}

Real Time Messaging を使用して、 Websocket から情報を取得します。

rtm := api.NewRTM()
go rtm.ManageConnection()

コネクションを張り次第、ひたすらループして情報を取得します。情報はチャネルの IncomingEvents に enqueue されるので、それを dequeue して型によって振る舞いを変えます。

for {
    select {
    case msg := <-rtm.IncomingEvents:
        switch ev := msg.Data.(type) {
        case *slack.HelloEvent:
            log.Print("Hello Event")

        case *slack.MessageEvent:
            log.Printf("Message: %v\n", ev)
            rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel))

        }
    }
}

今回の例では誰かがこの bot が招待されているチャネルに投稿すると、slack.MessageEvent が発火して、 "Hello world" をそのチャネルに投稿しています。

rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", ev.Channel))

実行

上に記述したコードを main.go という名前で実装していると仮定すると。

$ cd /path/to/slack-bot
$ go run main.go
2016/12/03 06:32:53 Hello Event
2016/12/03 06:32:53 Message: &{{message XX XX Hello world 1480746760.000010 false [] [] <nil>  false     <nil>      [] <nil> false <nil>  1  []} <nil>}

のように出力されれば動いています。

デーモン化

bot が死んでいたら悲しいので、デーモン化をちゃちゃっとしちゃいます。

まず、作成した bot をコンパイルしてバイナリにしておきます。

$ go build -o /path/to/bot main.go

次に、デーモン化ですが、今回は supervisor での例を載せておきます。

[program:bot]
command=/path/to/bot
user=someone
autostart=true
autorestart=true
stderr_logfile=/var/log/bot.err.log
stdout_logfile=/var/log/bot.out.log

上記を conf に用意して、 supervisord を再起動させましょう。これでデーモン化された bot の完成です。

おわりに

慣れている言語でちゃちゃっと bot は実装したいと思うので、各言語での bot 実装法はもっと出回っていいのではと思っています。最近、 Crystal にハマっているので、今度機会があれば Crystal バージョンを書こうと思います。