Go で ChatOps 用 bot 作成ハンズオン

社内のハンズオン向けに書いていた資料ですが、まぁ Public Qiita でも差し支えない内容だったしそれなりに良い内容にまとまったと思ったので公開します。

※ 筆者の Go 力は「たったの 5 か、ゴミめ」レベルなので解説とか間違いの指摘大歓迎

# 背景

ChatOps したい。したくない？
Go で Slack bot 作ると結構簡単に ChatOps できるよ。


# 使うもの

- Go 1.12
    - https://github.com/nlopes/slack
- Slack (Bots)
- macOS 10.14.5


# ハンズオン

## Go をインストールしよう

筆者は公式サイト派。.pkg なのでダブルクリックするだけ。
https://golang.org/dl/

```console
$ go version
go version go1.12.5 darwin/amd64
```

一瞬。

Homebrew でもインストールできるらしいがやってない。


## Go で Hello, World してみよう

まぁ定番ですよね。
インタプリタは無いので、vim で書きます。
vim じゃない人は知りません、お使いのエディタで書いてください。

```go:hello.go
package main

import "fmt"

func main() {
	fmt.Println("Hello, world.")
}
```

go run で実行。

```console
$ go run hello.go
Hello, world.
```

簡単ですね。
ビルドもしてみましょう。

go build

```console
$ go build hello.go
$ ls
hello		hello.go
$ ./hello
Hello, world.
```

とっても簡単ですね。
ちなみにちょっと間違えてみましょう。

```go:hello.go
  1 package main
  2
  3 import "fmt"
  4
  5 func main() {
  6     hello = "Hello, world."
  7     fmt.Println(hello)
  8 }
```

```console
$ go build hello.go
# command-line-arguments
./hello.go:6:2: undefined: hello
./hello.go:7:14: undefined: hello
```

`hello` は宣言されていない変数のため、値の代入時に「`hello` なんて宣言されてないよ」と怒られています（6 行目）。
また、宣言できていないため、7 行目の `fmt.Println` での呼び出しについても怒られています。
コンパイルは実行前にエラーが分かっていいですね。

### ちょっと解説

`package` は Go で名前空間を定義するために使うようです。
一つのファイルに全部書いちゃうなら、main だけでいいですね。
http://cuto.unirita.co.jp/gostudy/post/go-package/


### 余談

筆者は vim-go 使ってます。
が、特にこれと言って機能を使い込んでいるわけではありません。
ハイライトがいい感じです。
https://github.com/fatih/vim-go


## Slack bot を使う準備をしよう

そんなに（bot 自身に）尖った機能をつける気はないので、Apps から Bots の Integration を追加しましょう。

Slack の Workspace で Apps 検索
<img width="252" alt="スクリーンショット 2019-05-30 16.15.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/4d8f6f67-fafb-f556-e1a0-d6c48750f914.png">


「View App Directory」
<img width="700" alt="スクリーンショット 2019-05-30 16.16.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/be8b267a-9162-585e-055f-9131a99ef967.png">


Bots で検索
<img width="373" alt="スクリーンショット 2019-05-30 16.17.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/da17c583-bd9b-6ff7-6280-7ec3dffe1cfd.png">


Add Configurations で追加できます
<img width="1007" alt=" .png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/edde7ec0-b889-b193-6249-6cea48c52a33.png">


名前には日本語は（今は）使えません（前は使えたと思うんだけど・・・）
<img width="974" alt="スクリーンショット 2019-05-30 16.28.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/0d3635df-1d08-ad5b-5a0a-9a852603eab2.png">


- 追加完了したら API Token を控えましょう（漏れないよう注意！）
- emoji とか icon とかいい感じに設定しましょう
- 説明を入れましょう
    - 雑すぎる説明だと管理者にうっかり消されちゃうかも知れません
- API Token を利用した実行 IP が固定されてるならついでに IP 制限を設定しておきましょう


## Bot で Slack に Hello, World してみよう

API Token さえ手に入ればあとはやりたい放題です。
まずは Slack bot をいい感じに使えるパッケージを入れましょう。
今回使うのはこちらです。
https://github.com/nlopes/slack

※ 同じディレクトリに複数の main() は存在できないので、さっきの hello は消しておきましょう。（ディレクトリを変えるのでも化）

まずは（API をうっかりお漏らししたくないので）他のファイルに書いて、import するようにします。

```console
$ ls
bots.go	token
$ ls token/
slack.go
$ cat token/slack.go
package token

const Slack = "INPUT YOUR SLACK TOKEN" 　←ここに書いておく
```

ついでにいつうっかり git push してもいいように ignore しておきましょう。

```console
$ cat .gitignore
token/*
```

これでまぁ間違っても漏らさないでしょう。
（git の管理とか Token の管理は、漏洩しなければ好きにしてもらって構いません。もっといい方法をご存知であれば教えてください。）

指定のチャンネルにメッセージをポストしてみます。

```go:bots.go
package main

import (
	"./token"
	"github.com/nlopes/slack"
)

func main() {
	api := slack.New(token.Slack)
	channel := "Input your channel name(or ID)"
	value := "hello, world!"
	api.PostMessage(channel, slack.MsgOptionText(value, false))
}
```

怒られましたね。

```console
$ go run bots.go
bots.go:5:2: cannot find package "github.com/nlopes/slack" in any of:
	/usr/local/go/src/github.com/nlopes/slack (from $GOROOT)
	/Users/hogehoge/go/src/github.com/nlopes/slack (from $GOPATH)
```

インストールしていないパッケージについてはこのように怒られます。
慌てず騒がず go get しましょう。

```console
$ go get github.com/nlopes/slack
```

もう一度。

```console
$ go run bots.go
```

slack にメッセージが投稿されました :tada: 

<img width="153" alt="スクリーンショット 2019-05-30 17.02.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/a3d03b92-fa49-1dfe-070a-90dc58064cee.png">


## Bot をチャンネルに常駐させて特定のワードに反応させてみよう

※ 先に、ターゲットとなるチャンネルに作成した bot を invite しておいてください。

このへんから Go の良さみを感じられます。

いい感じにコードを書きます。

```go:bots.go
package main

import (
	"./token"
	"github.com/nlopes/slack"
	"log"
	"os"
	"strings"
)

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

	for {
		msg := <-rtm.IncomingEvents
		log.Printf("MSG: %#v\n", msg.Data)

		switch ev := msg.Data.(type) {
		case *slack.HelloEvent:
			log.Printf("Start up!")

		case *slack.MessageEvent:
			if strings.Contains(ev.Text, "おはよう") {
				text := "Good morning!"
				rtm.SendMessage(rtm.NewOutgoingMessage(text, ev.Channel))
			}
		}
	}
}

func main() {
	api := slack.New(token.Slack)
	os.Exit(run(api))
}
```

実行します。

```console
$ go run bots.go
2019/05/30 17:51:06 MSG: &slack.ConnectingEvent{Attempt:1, ConnectionCount:0}
2019/05/30 17:51:08 MSG: &slack.ConnectedEvent{ConnectionCount:0, Info:(*slack.Info)(0xc0000)}
2019/05/30 17:51:08 MSG: &slack.HelloEvent{}
2019/05/30 17:51:08 Start up!
```

特定ワード「おはよう」を~~ツイート~~つぶやいてみましょう。
<img width="296" alt="スクリーンショット 2019-05-30 17.52.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88428/854e52af-5064-5a26-5a37-05cccc9b8bc9.png">



bot が反応して挨拶してくれました :tada: 
これでいくらでも人工無脳が作れますね。
slackbot の１機能を車輪の再開発できました。

### ちょっと解説

```go
 13     go rtm.ManageConnection()
```

ここはいわゆる goroutine というやつですね。
ここから非同期で `rtm.ManageConnection()` が実行されるので、落ちるまで Slack のコネクションが張ってもらえる感じです。たぶん。

```go
 15     for {
 16         msg := <-rtm.IncomingEvents
 17         log.Printf("MSG: %#v\n", msg.Data)
 18
 19         switch ev := msg.Data.(type) {
 20         case *slack.HelloEvent:
 21             log.Printf("Start up!")
 22
 23         case *slack.MessageEvent:
 24             if strings.Contains(ev.Text, "おはよう") {
 25                 text := "Good morning!"
 26                 rtm.SendMessage(rtm.NewOutgoingMessage(text, ev.Channel))
 27             }
 28         }
 29     }
 30 }
```

無限ループでイベントを監視し続けています。

```go
 19         switch ev := msg.Data.(type) {
 20         case *slack.HelloEvent:
```

というふうにして、msg の型に応じて処理を分けられます。
どんなイベントがあるかはソース読むとかなんとかすればわかると思います。たぶん。
https://github.com/nlopes/slack/tree/95b04eeaeb73ff58f430062cd0d39592e03d01ad/slackevents


## 社内サーバでデーモンとして実行させて常駐させよう

社内サーバがどうなっているかはわからないので、
「Centos デーモン化」
とかそんな感じでググってどうにかしてください。

ただし、Go ではクロスコンパイルが容易にできるので

```console
$ GOOS=linux GOARCH=amd64 go build bots.go
$ ls
bots	bots.go	token
```

とかやるだけで簡単に Linux 用の実行ファイルがシングルバイナリでできちゃいます。
あとはこれ（`bots`）をサーバに配布して実行するだけ。とっても簡単ですね。

## 小ネタ

せっかくスラッシュコマンドではなく Bot にしたんだから何か話しかけてる感欲しい！
と思ったときは、MessageEvents の Message の先頭に `@bots` があるかどうか、みたいな判定をしてトリガーにしています。

```go
 30         case *slack.MessageEvent:
 31             log.Printf("Message: %#v\n", ev)
 32             message := ""
 33
 34             if strings.Contains(ev.Text, isuzu) {
```

このときの `isuzu` には Bot の UserID（装飾済み）を記述しています。

```go
 18     isuzu := "<@hogehoge>"
```

Bot が受け取るメッセージではメンションが上記のように記載されているためです。
Bot の ID 取得するのにも API が使えますので、活用してみてください。

# まとめ

Go で bot を作るとサーバに余計なものインストールしないで実行可能なバイナリを作れるので、社内 bot 作るのにとても便利です。
スラッシュコマンドと違って、誰かに使ってもらえてるってのがすごく可視化されるのと、ちょっとした単純な~~コールアンドレタスポン酢~~単語へのレスポンスを登録しておくだけで他の人へ挨拶したりして愛着が湧くので、とてもメンテナンスモチベーションが湧きます。
Slack bot おすすめですよ。