LoginSignup
29
16

More than 5 years have passed since last update.

【Golang初心者 part2】お題:Slack Botを作る

Posted at

初めに

  • go の勉強を始めたので、アウトプットする

お題

Slack Botを作る

<参考サイト>
GolangでSlack Interactive Messageを使ったBotを書く
https://tech.mercari.com/entry/2017/05/23/095500

ソース

main.go
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))
        }
    }
}

実行

qiita_bot2.gif

達成:beers:

もう少し

さすがに物足りないので、もう少し

説明できるほど理解できなかったが、何となくの理解だと

① 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)
    }
}

こんな感じで bot へメンションつけて実行
image.png

 $ 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 する為に、メソッドを追加

main.go
// 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 側から呼ぶようにする

main.go
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)
            }
        }
    }
}

実行

qiita_bot2.gif

ちゃんとメンションに反応

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
}

qiita_bot6.gif

これのこと
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
}

qiita_bot4.gif
人を選んでいる

因みに 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
}

qiita_bot5.gif

おまけ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

qiita_bot7.gif

最後に使ったコード
https://github.com/andormeda/gobot

29
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
16