初めに
- go の勉強を始めたので、アウトプットする
お題
Slack Botを作る
<参考サイト>
GolangでSlack Interactive Messageを使ったBotを書く
https://tech.mercari.com/entry/2017/05/23/095500
ソース
package main
import (
"github.com/nlopes/slack"
)
func main() {
api := slack.New(
"MY TOKEN",
)
rtm := api.NewRTM()
go rtm.ManageConnection()
for msg := range rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
rtm.SendMessage(rtm.NewOutgoingMessage("ホリネズミです。塊茎(球根)食べたい", ev.Channel))
}
}
}
実行
達成
もう少し
さすがに物足りないので、もう少し
説明できるほど理解できなかったが、何となくの理解だと
① rtm.ManageConnection() を goroutine で実行して、 RTMEvent を RTMEvent 型のチャネルの RTM.IncomingEvents で 受信し続けてる
② for range でチャネルから値を取り出している
③ switch 式で Data interface{} 型を元の型に戻して型により振る舞いを変える
④ slack.MessageEvent は、Msg が埋め込まれた Message 型のstruct
で、基本的には MessageEvent を見張っておけばよい
で、受信時に参考になりそうなフィールドを表示してみた
go rtm.ManageConnection() // ①
for msg := range rtm.IncomingEvents { // ②
switch ev := msg.Data.(type) { // ③
case *slack.MessageEvent: // ④
log.Printf("Type: %s", ev.Msg.Type)
log.Printf("Channel: %s", ev.Msg.Channel)
log.Printf("User: %s", ev.Msg.User)
log.Printf("Text: %s", ev.Msg.Text)
log.Printf("Timestamp: %s", ev.Msg.Timestamp)
log.Printf("Attachments: %v", ev.Msg.Attachments)
log.Printf("Files: %v", ev.Msg.Files)
log.Printf("Team: %s", ev.Msg.Team)
}
}
$ go run main.go
2018/12/18 08:14:21 Type: message
2018/12/18 08:14:21 Channel: CEQ8EPMPX # 受信したチャンネル
2018/12/18 08:14:21 User: U1VF3F8SX # 送信者
2018/12/18 08:14:21 Text: <@UEPBH79XH> こんにちは:+1: # メッセージ本体
2018/12/18 08:14:21 Timestamp: 1545088462.000500
2018/12/18 08:14:21 Attachments: [] # アタッチメントの場合ここで拾える
2018/12/18 08:14:21 Files: [] # スニペット等はここで拾える
2018/12/18 08:14:21 Team: T1VDA8TEX # ようわからん…
※ Textにある、botにメンション付けた @UEPBH79XH
が bot の ID
一通り Validate 出来そうな情報は取得できた!
特定のチャンネルで bot 宛て(メンション付)のみ反応
MessageEvent を Validate する為に、メソッドを追加
// Slackparams is slack parameters
type Slackparams struct {
tokenID string
botID string
channelID string
rtm *slack.RTM
}
// ValidateMessageEvent is Validate Message Event
func (s *Slackparams) ValidateMessageEvent(ev *slack.MessageEvent) error {
// Only response in specific channel. Ignore else.
if ev.Channel != s.channelID {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Only response mention to bot. Ignore else.
if !strings.HasPrefix(ev.Msg.Text, s.botID) {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
s.rtm.SendMessage(s.rtm.NewOutgoingMessage("メンション付いてるな。呼んだか?", ev.Channel))
return nil
}
if ev.Channel != s.channelID
でチャンネルの検証
if !strings.HasPrefix(ev.Msg.Text, s.botID)
で botID の検証
で main 側から呼ぶようにする
func main() {
params := Slackparams{
tokenID: "xoxb-xxxxx-xxxxx-xxxxx",
botID: "<@UEPBH79XH>",
channelID: "CEQ8EPMPX",
}
api := slack.New(params.tokenID)
params.rtm = api.NewRTM()
go params.rtm.ManageConnection()
for msg := range params.rtm.IncomingEvents {
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
if err := params.ValidateMessageEvent(ev); err != nil {
log.Printf("[ERROR] Failed to handle message: %s", err)
}
}
}
}
実行
ちゃんとメンションに反応
bot 宛て(メンション付)で特定のメッセージに反応
strings.Split(strings.TrimSpace(ev.Msg.Text), " ")[1:]
でメッセージを抽出
// ValidateMessageEvent is Validate Message Event
func (s *Slackparams) ValidateMessageEvent(ev *slack.MessageEvent) error {
// Only response in specific channel. Ignore else.
if ev.Channel != s.channelID {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Only response mention to bot. Ignore else.
if !strings.HasPrefix(ev.Msg.Text, s.botID) {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Parse message start
m := strings.Split(strings.TrimSpace(ev.Msg.Text), " ")[1:]
if len(m) == 0 {
return fmt.Errorf("invalid message")
}
if m[0] == "ホリネズミ?" {
s.rtm.SendMessage(s.rtm.NewOutgoingMessage("そうだ!", ev.Channel))
return nil
}
if m[0] == "ネズミ?" {
s.rtm.SendMessage(s.rtm.NewOutgoingMessage("ちがう!", ev.Channel))
return nil
}
return nil
}
これのこと
https://ja.wikipedia.org/wiki/%E3%83%9B%E3%83%AA%E3%83%8D%E3%82%BA%E3%83%9F
bot 宛て(メンション付)で特定のユーザのみ反応
ev.Msg.User
でユーザを判定
// ValidateMessageEvent is Validate Message Event
func (s *Slackparams) ValidateMessageEvent(ev *slack.MessageEvent) error {
// Only response in specific channel. Ignore else.
if ev.Channel != s.channelID {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Only response mention to bot. Ignore else.
if !strings.HasPrefix(ev.Msg.Text, s.botID) {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Only response specific user. Ignore else.
if ev.Msg.User == "U1VF3F8SX" {
s.rtm.SendMessage(s.rtm.NewOutgoingMessage("Nothing much.", ev.Channel))
return nil
}
return nil
}
因みに webhook を curl で叩くときにメンション付ける場合は以下でできる
#!/bin/bash
DATA=`cat << EOS
payload={
"channel": "#bot_test",
"text": "<@UEPBH79XH> Hello, rat!",
}
EOS`
curl -X POST --data-urlencode "$DATA" https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX
おまけ
bot 宛て(メンション付)で特定のワードでwikipediaの「ホリネズミ」の最初の一段落を返す
イベントドリブン感だしたかったので、発火したら goquery で web スクレイピングしてみる
特定のワードに合致したら wiki()
を叩く
// ValidateMessageEvent is Validate Message Event
func (s *Slackparams) ValidateMessageEvent(ev *slack.MessageEvent) error {
// Only response in specific channel. Ignore else.
if ev.Channel != s.channelID {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Only response mention to bot. Ignore else.
if !strings.HasPrefix(ev.Msg.Text, s.botID) {
log.Printf("%s %s", ev.Channel, ev.Msg.Text)
return nil
}
// Parse message start
m := strings.Split(strings.TrimSpace(ev.Msg.Text), " ")[1:]
if len(m) == 0 {
return fmt.Errorf("invalid message")
}
if m[0] == "ホリネズミってなに?" {
_, lead := wiki()
s.rtm.SendMessage(s.rtm.NewOutgoingMessage(lead, ev.Channel))
return nil
}
s.rtm.SendMessage(s.rtm.NewOutgoingMessage("メンション付いてるな。呼んだか?", ev.Channel))
return nil
}
func wiki() (title string, lead string) {
url := "https://ja.wikipedia.org/wiki/%E3%83%9B%E3%83%AA%E3%83%8D%E3%82%BA%E3%83%9F"
doc, err := goquery.NewDocument(url)
if err != nil {
log.Println("Wikipedia scraping failed.")
os.Exit(1)
}
title = doc.Find("#firstHeading").Text()
lead = doc.Find("#mw-content-text p").First().Text()
return title, lead
}
おまけ2
シュッと Docker にして シュッと GKE(kubernetes) で動かしてみる
centos 使う事が多いので cent で docker 環境作る
# 余計なの消す
$ sudo yum remove docker docker-common docker-selinux docker-engine
# 必要なpkg入れる
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2 git
# repo追加
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# おまじないw
$ sudo yum makecache fast
# インストール可能なdocker一覧
$ sudo yum list docker-ce.x86_64 --showduplicates | sort -r
# latestのインストール
$ sudo yum install -y docker-ce
# 起動
$ sudo systemctl start docker
# 自動起動
$ sudo systemctl enable docker
Dockerファイル作る
FROM golang:1.7
RUN go get github.com/PuerkitoBio/goquery
RUN go get github.com/nlopes/slack
WORKDIR /go/src/
ADD . /go/src/bot
RUN go install bot
ENTRYPOINT /go/bin/bot
build して GCR(Google Container Registry) へ push する
※あ、gce上でやってるので、 gcloud とか入ってます…
$ export PROJECT_ID="$(gcloud config get-value project -q)"
$ docker build -t gcr.io/${PROJECT_ID}/gobot:v1 .
$ gcloud docker -- push gcr.io/${PROJECT_ID}/gobot:v1
GKE Cluster作る
$ gcloud beta container --project "prj-name" clusters create "test-gke-cluster" --zone "asia-northeast1-a" --username "admin" --cluster-version "1.10.9-gke.5" --machine-type "n1-standard-1" --image-type "COS" --disk-type "pd-standard" --disk-size "100" --scopes "https://www.googleapis.com/auth/devstorage.read_only","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/monitoring","https://www.googleapis.com/auth/servicecontrol","https://www.googleapis.com/auth/service.management.readonly","https://www.googleapis.com/auth/trace.append" --num-nodes "1" --enable-cloud-logging --enable-cloud-monitoring --no-enable-ip-alias --network "projects/prj-name/global/networks/default" --addons HorizontalPodAutoscaling,HttpLoadBalancing --enable-autoupgrade --enable-autorepair
GKE にdeploy!
$ sudo yum install -y kubectl
$ kubectl run gobot --image=gcr.io/${PROJECT_ID}/gobot:v1
kubectl get pod
NAME READY STATUS RESTARTS AGE
gobot-c7b4bd4f5-n2ls5 1/1 Running 0 2m
最後に使ったコード
https://github.com/andormeda/gobot