Go + LINE Messaging APIで東京メトロ運行情報botを作る

TECH::CAMP Advent Calendar17日目はGo + LINE Messaging APIで作る東京メトロ運行情報botについて書いていきたいと思います。

Go, LINE Messaging APIに関してはすでに良質な記事がqiitaにあるので、今回はそれらを参考にして、自分なりに形になるものを作ってみました。


Goは今まで扱ったことがなかったので、インストールからしていきます。

# Goのインストールからセットアップ

とりあえず、brewでgoをインストールします。

```sh:terminal
$ brew install go
```

.bash_profileに以下を追加し、`source ~/.bash_profile`を実行します。

```bash:.bash_profile
export GOROOT=/usr/local/opt/go/libexec
export GOPATH=$HOME
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
```

## HelloWorld
おきまりのHelloWorldもしていきます。

```go:hello.go
package main
import "fmt"

func main(){
    fmt.Println("HelloWorld")
}
```

```sh
$ go run hello.go
HelloWorld
```

# LINEBotアカウントの登録

Botを作るための下準備ということで、[こちら](https://business.line.me/ja/services/bot)からアカウントを登録していきます。

![スクリーンショット 2016-12-02 21.38.33.png](https://qiita-image-store.s3.amazonaws.com/0/94508/89b18fb5-f982-96b0-d690-4243fb82e912.png)

「Messaging APIを始める」と「Developer Trialを始める」の2つがありますが、これはどちらでも構いません。違いに関しては下の方に表でまとまっております。

![スクリーンショット 2016-12-02 21.43.33.png](https://qiita-image-store.s3.amazonaws.com/0/94508/94f60c2d-14c5-64b6-4185-e17b41b2c111.png)

登録後は[公式のリファレンス](https://developers.line.me/messaging-api/getting-started)があるのでそちらを参考にして設定していきます。動画付きでわかりやすくまとまっています。

# 東京メトロ開発者利用登録

今回は東京メトロが提供するapiを利用するため[開発者サイト](https://developer.tokyometroapp.jp/)からユーザー登録をします。

ユーザー登録が完了するとアクセストークンを確認・発行できるようになるので、確認します。

のちのコードで出てきますが、発行したアクセストークンを使い、

https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=YOURTOKEN

にアクセスすると東京メトロ9路線の運行情報がjson形式で取得できます。

```json
[
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE4","dc:date":"2016-12-02T22:10:03+09:00","dct:valid":"2016-12-02T22:15:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Ginza","odpt:timeOfOrigin":"2016-12-01T11:31:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE7","dc:date":"2016-12-02T22:05:03+09:00","dct:valid":"2016-12-02T22:10:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Marunouchi","odpt:timeOfOrigin":"2016-11-22T11:41:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE2","dc:date":"2016-12-02T22:10:03+09:00","dct:valid":"2016-12-02T22:15:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Chiyoda","odpt:timeOfOrigin":"2016-12-01T14:31:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE6","dc:date":"2016-12-02T22:10:03+09:00","dct:valid":"2016-12-02T22:15:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Hibiya","odpt:timeOfOrigin":"2016-12-01T11:38:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE8","dc:date":"2016-12-02T22:00:03+09:00","dct:valid":"2016-12-02T22:05:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Namboku","odpt:timeOfOrigin":"2016-12-01T10:33:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BEA","dc:date":"2016-12-02T22:00:03+09:00","dct:valid":"2016-12-02T22:05:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Yurakucho","odpt:timeOfOrigin":"2016-11-25T11:12:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE3","dc:date":"2016-12-02T22:10:03+09:00","dct:valid":"2016-12-02T22:15:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Fukutoshin","odpt:timeOfOrigin":"2016-12-01T12:42:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE5","dc:date":"2016-12-02T22:10:03+09:00","dct:valid":"2016-12-02T22:15:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Hanzomon","odpt:timeOfOrigin":"2016-12-02T10:34:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"},
  {"@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json","@id":"urn:ucode:_00001C000000000000010000030C3BE9","dc:date":"2016-12-02T22:00:03+09:00","dct:valid":"2016-12-02T22:05:03+09:00","odpt:operator":"odpt.Operator:TokyoMetro","odpt:railway":"odpt.Railway:TokyoMetro.Tozai","odpt:timeOfOrigin":"2016-12-02T11:50:00+09:00","odpt:trainInformationText":"現在、平常どおり運転しています。","@type":"odpt:TrainInformation"}
]
```

# サンプルコード

下準備が整ったので、実装に入ります。
足りないパッケージがある場合はインストールします。

```sh
$ go get github.com/gin-gonic/gin
$ go get github.com/line/line-bot-sdk-go/linebot
```

いよいよ、「運行情報」とユーザーが入力すると運行情報を返してくれるBotを作っていきます。

今回使用するサンプルコードは以下のようなコードになります。
※ goに触れたことはほとんどないので、より良い記述方法があるかもしれません。多めにみてください笑

```go:main.go
package main

import (
    "os"
    "fmt"
    "log"
    "time"
    "regexp"
    "strings"
    "net/http"
    "io/ioutil"
    "encoding/json"
    "github.com/gin-gonic/gin"
    "github.com/line/line-bot-sdk-go/linebot"
)

type TrainInfomation struct {
    Context                string    `json:"@context"`
    Id                     string    `json:"@id"`
    Type                   string    `json:"@type"`
    Date                   time.Time `json:"dc:date"`
    Valid                  time.Time `json:"dct:valid"`
    Operator               string    `json:"odpt:operator"`
    TimeOfOrigin           time.Time `json:"odpt:timeOfOrigin"`
    Railway                string    `json:"odpt:railway"`
    TrainInformationStatus string    `json:"odpt:trainInformationStatus"`
    TrainInformationText   string    `json:"odpt:trainInformationText"`
}

type TrainInformations []TrainInfomation

func fetchTrainName(railway string) string {
    name := map[string]string{
      "Ginza":      "銀座線",
      "Marunouchi": "丸の内線",
      "Chiyoda":    "千代田線",
      "Hibiya":     "日比谷線",
      "Namboku":    "南北線",
      "Yurakucho":  "有楽町線",
      "Fukutoshin": "副都心線",
      "Hanzomon":   "半蔵門線",
      "Tozai":      "東西線",
    }
    return name[railway]
}

func fetchTrainInfo(message string) string{
    info := "運行情報:\n"

    if message == "運行情報" {
        url := make([]byte, 0, 10)
        url = append(url, "https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey="...)
        url = append(url, os.Getenv("CONSUMER_KEY")...)

        res, err := http.Get(string(url))
        if err != nil {
            log.Fatal(err)
        }

        defer res.Body.Close()

        body, err := ioutil.ReadAll(res.Body)
        if err != nil {
            log.Fatal(err)
        }

        var trains TrainInformations
        err = json.Unmarshal(body, &trains)
        if err != nil {
            log.Fatal(err)
        }

        for _, train := range trains {
            rep  := regexp.MustCompile(`[A-Za-z]*odpt.Railway:TokyoMetro.`)
            railway := rep.ReplaceAllString(train.Railway, "")
            railway  = fetchTrainName(railway)
            text := train.TrainInformationText
            if len(train.TrainInformationStatus) > 0 {
                text = fmt.Sprintf("%s (%s)", train.TrainInformationStatus, train.TrainInformationText)
            }
            info += fmt.Sprintf("%s: %s\n", railway, text)
        }
    } else {
        info = "「運行情報」と入力すると東京メトロの運行情報を表示します"
    }
    return info
}

func main() {
    port := os.Getenv("PORT")

    if port == "" {
        log.Fatal("$PORT must be set")
    }

    bot, err := linebot.New(
        os.Getenv("CHANNEL_SECRET"),
        os.Getenv("CHANNEL_TOKEN"),
    )
    if err != nil {
        log.Fatal(err)
    }

    r := gin.New()
    r.Use(gin.Logger())

    r.POST("/callback", func(c *gin.Context) {
        events, err := bot.ParseRequest(c.Request)
        if err != nil {
            if err == linebot.ErrInvalidSignature {
                log.Print(err)
            }
            return
        }
        for _, event := range events {
            if event.Type == linebot.EventTypeMessage {
                switch message := event.Message.(type) {
                case *linebot.TextMessage:
                    text := fetchTrainInfo(message.Text)
                    if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(text)).Do(); err != nil {
                        log.Print(err)
                    }
                }
            }
        }
    })

    r.Run(":" + port)
}
```

# herokuへデプロイ

herokuでgoアプリを動かす方法などに関しては[公式](https://devcenter.heroku.com/categories/go)で説明がまとまっているので参考にしながら進めます。

まず、herokuアプリの起動時に参照されるProcfileを作成します。

```sh
$ echo "web: $(basename `pwd`)" > Procfile
```

次に、herokuでgoアプリを動かす際は、ビルドパックを使用しパッケージを管理しなければいけないので、今回はgovendorを用いて管理します。

```sh
$ go get -u github.com/kardianos/govendor
$ govendor init
$ govendor fetch +out
```

作成したアプリをgit配下におきます。

```sh
$ git init
$ git add .
$ git commit -m "first commit"
```

最後にherokuにデプロイしていきます。
herokuのアカウントがない場合は作成してください。
先ほど取得した、apiキーなども環境変数としてセットしておきます。

```sh
$ heroku login
$ heroku create
$ git push heroku master
$ heroku config:add CONSUMER_KEY="xxxxxxxxxx"
$ heroku config:add CHANNEL_SECRET="xxxxxxxxxx"
$ heroku config:add CHANNEL_TOKEN="xxxxxxxxxx"
```

あとは、作ったアプリのWebhook URL(`https://〇〇〇〇.herokuapp.com/callback `)をlinebotの管理画面で登録すれば完成です。


<img width="416" alt="スクリーンショット 2016-12-08 22.21.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/94508/890c19db-34c1-5112-53fe-cf17ea338d25.png">


# 参考文献

[東京メトロオープンデータ開発者サイト](https://developer.tokyometroapp.jp/documents/railway#%E5%88%97%E8%BB%8A%E9%81%8B%E8%A1%8C%E6%83%85%E5%A0%B1-odpt:TrainInformation)
[Go言語で東京メトロAPIを叩く](http://qiita.com/nayuneko/items/ca2651e3a613c8e3256a)