Go入門！？LineAPIとGOでオウム返しbotを作る

# Go入門！？LineAPIとGOでオウム返しbotを作る

以下のサイトを参考にしながら、LineAPIとGolangで何かを作るのを目標に一進一退で1日頑張ってみる。

## まとめ
この記事で以下のことができるようになります。

* Line Massage APIを使って
* AWS APIGW　＋　EC2　の環境で
* Go言語を使って
* オウム返しするLineBOTが作れる

今回の前提となっている私の開発環境は、[mem. golangに入門してみた時にやったこと](https://qiita.com/ken1ro/items/14ab7c174d20e3986d74#comment-cd2fe22504ab73f424f9)を参照。

### 参考サイトメモ

[Go + LINE Messaging APIで東京メトロ運行情報botを作る](https://qiita.com/hc0208/items/7a63b2a0aea134babbb6)
[Go言語を使用して簡単なLineBotを作る](https://qiita.com/sao_rio/items/8801b78ba60acbb0ae41)
[Goアプリケーションを初めてawsにデプロイした話](https://qiita.com/Dragon-taro/items/09a67b93093770348ce5)

# ざっくりとした手順

1. Line周りの準備
2. LineSDKをローカル環境にインストール
3. SDKのサンプルソースを３行書き換え
4. deploy先となるEC2インスタンスを用意
5. ソースのビルドとEC2への配布
6. AWS APIGWの設定
7. はまり対策・・・

では、上記の順に実施していきます。

## Lineアカウントの準備

LineAPIを使うためには、Lineビジネスアカウントが必要、ということなので、作る。

* [Line for Business公式](https://www.linebiz.com/jp)からアカウントを開設。今回はメールアドレスで未認証アカウントを作った。
* 開設し、ログイン後に右上の設定ボタンからたどり着く画面で、Messaging APIを選択して、個人のLineアカウントと連携（止むを得ず）
* [Line Official account Manager](https://manager.line.biz)にログイン
* [Line Developpers](https://developers.line.biz)でシークレットキーとアクセストークンを確認（後で使う）

## Line SDKのインストール

Line Botのサンプルアプリがgitに公開されているため、それを利用する。
go getでgitからダウンロード。今回はローカルの開発環境に。

```go
$ go get github.com/line/line-bot-sdk-go
```

## ソースの修正

サンプルコードを修正する。
`github.com/line/line-bot-sdk-go/examples/echo_bot/server.go`の
25−32行目にあるシークレットキーとトークンを、上記で準備したアカウントのものに置き換える。

* 修正前 

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

* 修正後

```server.go
func main() {
	bot, err := linebot.New(
		"8b3-----------256",
		"TPr----------FU=",
	)
	if err != nil {
		log.Fatal(err)
	}
```

58行目のポートを変更する。

* 変更前

```go:server.go 
// This is just sample code.
// For actual use, you must support HTTPS by using `ListenAndServeTLS`, a reverse proxy or something else.
if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
  log.Fatal(err)
}
```

* 変更後

```go:server.go 
// This is just sample code.
// For actual use, you must support HTTPS by using `ListenAndServeTLS`, a reverse proxy or something else.
if err := http.ListenAndServe(":8080", nil); err != nil {
  log.Fatal(err)
}
```

## EC2インスタンスの用意

今回は試しにAWS EC2で動かしてみる。
Goはクロスコンパイルが簡単にできるらしいので。
まずは簡単なアプリ`hello`を動かして、環境に問題ないことを確認する。
主な流れは以下。

* AmazonLinuxの無料枠でインスタンス(t2.micro)を作る
* 8080ポートを開ける
* アプリ`hello`をビルド
* ビルド済みソースをEC2へ配置
* ブラウザから起動確認。

### Helloソースのビルド

以下のHelloソースを作る

```hello.go
package main

import (
	"fmt"
	"net/http"
)

func handler(writer http.ResponseWriter, request *http.Request) {
	fmt.Fprintf(writer, "Hello World, %s ", request.URL.Path[1:])
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}
```

### EC2用にビルド

```go
$ GOOS=linux GOARCH=amd64 go build hello.go
```

### EC2へ配置

ビルドすると`hello`という実行ファイルができるのでこれをEC2へアップする。
参考までアップロードコマンドをメモ。

```
scp -i [秘密鍵] [転送するファイルのパス] [EC2ユーザー名]@[パブリックDNS]:[コピー先のパス]
```

実際のアップロード

```
scp -i ~/Desktop/aws/xxx.pem  ./hello ec2-user@ecxxxxxxxxxx.ap-northeast-1.compute.amazonaws.com:/home/ec2-user
```

### ブラウザから確認

ブラウザから以下にアクセス。

http://ecxxxxxxxxxx.ap-northeast-1.compute.amazonaws.com:8080/test

画面が出ればOK。

## サンプルソースのコンパイルと配布

では、EC2側の環境の確認ができたので、先ほど修正したサンプルアプリを配布する。

まずコンパイル。

```go
$ GOOS=linux GOARCH=amd64 go build server.go
```
そしてアップロード

```
$ scp -i ~/Desktop/aws/xxxxxxx.pem  ./server ec2-user@ec2-xxxxxxxx.ap-northeast-1.compute.amazonaws.com:/home/ec2-user
```

と・・・ここまでやって、LineのWebhookがsslのみ対応ということがわかった。。
そして、[オレオレ証明書も無理](https://enz0.net/memo/line-bot/)とのこと。。。


## AWS APIGWを使う
解決策としてAWSのAPIGWをI/Fとして使うことにした。

* API名：LineBot-goEC2で作成
* アクション＞リソースの作成から`/callback`のリソースを作成
* メソッド`POST`を作る
* POSTのメソッドリクエストは変更なし
	* 認証：なし
	* リクエストの検証：なし
	* API キーの必要性：false
* 統合リクエストにEC2への転送設定を入れる
	* 統合タイプ：http
	* HTTPメソッド：POST
	* エンドポイントURL：EC2のURL:8080/callback（`http://ec2-xxxxxxxx.ap-northeast-1.compute.amazonaws.com:8080/callback`)
	* コンテンツの処理：パススルー
	* アクション＞APIのデプロイ　でAPIをprod環境へデプロイ

上記設定ののち、できたステージ（`https://xxxxxxxx/prod`）をLineに登録。

* Messaging API：Webhook URLに登録
	* `https://xxxxxxxx:443/prod/callback`
	* 念のため、443をつけておく

## これだけでは動かない。。。

上記でlineのBot情報を参照しLineからコメントをしてみるも動かず。。。

ここでかなりはまりました。。。

APIGWでリクエストヘッダー含む全てをログ出力するようにしてデバッグ。

結果、わかったことは・・・
APIGWがリクエスト転送する際にリクエストヘッダーを書き換えている。（知っている人には常識なのかも）なので、リクエストヘッダーが以下のように変化しています。

* LINE　->　APIGW　のリクエストヘッダ
* 
```
(xxxxxxxxxxxxxx) Method request headers: {X-Line-Signature=1+WxxxxxxxxxxxxxxSA=, User-Agent=LineBotWebhook/1.0, X-Forwarded-Proto=https, X-Forwarded-For=xxxxxxxxxxxxxx, Host=xxxxxxxxxxxxxx.ap-northeast-1.amazonaws.com, X-Forwarded-Port=443, X-Amzn-Trace-Id=Root=1-5xxxxxxxxxxxxxx716, accept=*/*, Content-Type=application/json;charset=UTF-8}
```

* APIGW　->　EC2　のリクエストヘッダ

```
(xxxxxxxxxxxxxx) Endpoint request headers: {x-amzn-apigateway-api-id=xxxxxxxxxxxxxx, Accept=application/json, User-Agent=AmazonAPIGateway_xxxxxxxxxxxxxx, X-Amzn-Trace-Id=Root=1-5xxxxxxxxxxxxxx716, Content-Type=application/json}
```

上記の通り`X-Line-Signature=1+WxxxxxxxxxxxxxxSA=`部分が消されてしまいます。LineBotのサンプルプログラムとSDKでは、この`X-Line-Signature`を使って署名の検証をしているので、この検証の部分でエラー扱いされて、常にレスポンス400を返すようになっていました。。
参考：[LINE Messaging APIでX-Line-Signatureの署名検証を行う(AzureFunctions/Node.js）](https://qiita.com/yorifuji/items/71e31baf896adb69f567)

## 修正・対策方法

このエラーハンドリングをしていたのは、`webhook.go`でした。
この38−42行目をコメントアウトすることで一旦回避。

```go:webhook.go
/*
	if !validateSignature(channelSecret, r.Header.Get("X-Line-Signature"), body) {
		return nil, ErrInvalidSignature
	}
*/
```

完全に一時的な回避策ですので、本来はAPIGW側でリクエストヘッダを引き継ぐ処理などを入れるのだと思います。どなたかわかれば教えてください。

## 完成！！

ということで、修正したモジュールを再配置したらオウム返しBOTができました！！

APIGWのログ解析の設定ができず、ここだけで半日くらいかかりました。。。

このあたりを参考に。
[新機能】Amazon API Gateway でアクセスログを記録する #reinvent](https://dev.classmethod.jp/server-side/serverless/aws-reinvent2017-api-gateway-access-log/)


## 余談

半日くらいはまっていたののもう一つの理由に作ったGoプログラムがログを出さないというのもありました。
暫定として標準出力にログを出すコードを埋め込んだ。
これは割と便利だったのでメモ。

[Goでのログ出力に標準logとcologを使う](https://qiita.com/kmtr/items/406073651d7a12aab9c6)

以下の感じでmainにベタ貼りして使っていました。

```main.go
colog.SetDefaultLevel(colog.LDebug)
colog.SetMinLevel(colog.LTrace)
colog.SetFormatter(&colog.StdFormatter{
	Colors: true,
	Flag:   log.Ldate | log.Ltime | log.Lshortfile,
})
colog.Register()

log.Printf("info: start server")
```