GAEにgolangでlinebotをデプロイする

# 背景

以下を参考にさせてもらいました。
[LINE Developer Trialを GAE/Go で始める #linedevday](http://qiita.com/koki_cheese/items/66980888d7e8755d01ec)

# ソース

[fk2000/linebot-gae-golang](https://github.com/fk2000/linebot-gae-golang)

# GAE環境の準備

1. GAE/Go SDKをダウンロードする
2. [Cloud Console](https://console.cloud.google.com/)で新しいプロジェクトを作成してprojectIDをメモする

# LINE Developers設定

## MessageAPIを始める

1. [LINE BUSINESS CENTER](https://business.line.me/ja/)でMessageAPIを始める
2. Bot設定から[APIを利用する]ボタンを押して有効したのち、bot設定を以下のように設定する<br/><table><thead><tr><th>項目</th><th>設定</th></tr></thead><tbody><tr><td>Webhook送信</td><td>利用する</td></tr><tr><td>Botのグループトーク参加</td><td>利用する</td></tr><tr><td>自動応答メッセージ</td><td>利用しない（デフォルトは利用する）</td></tr><tr><td>友だち追加時あいさつ</td><td>利用する</td></tr></tbody></table>
3. LINE Developersで「Channel Secret」と「Channel Access Token」をメモする[^1]
4. 同じ画面の下の方に、EDITとあるので編集画面に行きWebhookに`https://<projectID>.appspot.com/callback`と設定する

[^1]: Messaging APIになってから、「Channel ID」と「MID」は使用しなくなりました。

# SDKをインストール

## go getでインストール

```
$ go get -u github.com/line/line-bot-sdk-go/linebot
$ go get -u github.com/joho/godotenv
$ go get -u google.golang.org/appengine
```

## GAEにdeployするための、app.yamlを作成

```
$ mkdir linebot;cd linebot
$ vi app.yaml
```

&lt;projectID&gt;にprojectIDを入力してください。

```yaml:app.yaml
python:
application: <projectID>
version: 1
runtime: go
api_version: go1

handlers:
- url: /task.*
  script: _go_app
  login: admin
  secure: always
- url: /.*
  script: _go_app
  secure: always
```


## line.envを作成する

「Channel Secret」と「Channel Access Token」を環境変数として入力します。

このファイルの取り扱いには注意が必要です。ソースコードをgithubで保管する場合は.gitignoreに`/line.env`と記入してpushされないようにしてください。

```env:line.env
LINE_BOT_CHANNEL_SECRET=[Your secret]
LINE_BOT_CHANNEL_TOKEN=[Your Channel Token]
```

## app.goを作成する

LINEでテキストを送信すると「ok」と返すだけのコードです。
※ソースコードの解説は省きます。

```golang:app.go
package main

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/url"
	"os"

	"google.golang.org/appengine"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/taskqueue"
	"google.golang.org/appengine/urlfetch"

	"golang.org/x/net/context"

	"github.com/joho/godotenv"
	"github.com/line/line-bot-sdk-go/linebot"
	"github.com/line/line-bot-sdk-go/linebot/httphandler"
)

var botHandler *httphandler.WebhookHandler

func init() {
	err := godotenv.Load("line.env")
	if err != nil {
		panic(err)
	}
	botHandler, err = httphandler.New(
		os.Getenv("LINE_BOT_CHANNEL_SECRET"),
		os.Getenv("LINE_BOT_CHANNEL_TOKEN"),
	)
	botHandler.HandleEvents(handleCallback)

	http.Handle("/callback", botHandler)
	http.HandleFunc("/task", handleTask)
}

func newLINEBot(c context.Context) (*linebot.Client, error) {
	return botHandler.NewClient(
		linebot.WithHTTPClient(urlfetch.Client(c)),
	)
}

// newContext は appengine.NewContext を短く書くための関数
func newContext(r *http.Request) context.Context {
	return appengine.NewContext(r)
}

// logf は log.Infof を短く書くための関数
func logf(c context.Context, format string, args ...interface{}) {
	log.Infof(c, format, args...)
}

// errorf は log.Errorf を短く書くための関数
func errorf(c context.Context, format string, args ...interface{}) {
	log.Errorf(c, format, args...)
}

// Webhook を受け取って TaskQueueに詰める関数
func handleCallback(evs []*linebot.Event, r *http.Request) {
	c := newContext(r)
	ts := make([]*taskqueue.Task, len(evs))
	for i, e := range evs {
		j, err := json.Marshal(e)
		if err != nil {
			errorf(c, "json.Marshal: %v", err)
			return
		}
		data := base64.StdEncoding.EncodeToString(j)
		t := taskqueue.NewPOSTTask("/task", url.Values{"data": {data}})
		ts[i] = t
	}
	taskqueue.AddMulti(c, ts, "")
}

// 受け取ったメッセージを処理する関数
func handleTask(w http.ResponseWriter, r *http.Request) {
	c := newContext(r)
	data := r.FormValue("data")
	if data == "" {
		errorf(c, "No data")
		return
	}

	j, err := base64.StdEncoding.DecodeString(data)
	if err != nil {
		errorf(c, "base64 DecodeString: %v", err)
		return
	}

	e := new(linebot.Event)
	err = json.Unmarshal(j, e)
	if err != nil {
		errorf(c, "json.Unmarshal: %v", err)
		return
	}

	bot, err := newLINEBot(c)
	if err != nil {
		errorf(c, "newLINEBot: %v", err)
		return
	}

	logf(c, "EventType: %s\nMessage: %#v", e.Type, e.Message)

	m := linebot.NewTextMessage("ok")
	if _, err = bot.ReplyMessage(e.ReplyToken, m).WithContext(c).Do(); err != nil {
		errorf(c, "ReplayMessage: %v", err)
		return
	}

	w.WriteHeader(200)
}

```

# デプロイする

```
$ goapp deploy
```


もしここで、

```
ERROR appcfg.py:2411 An error occurred processing file '': HTTP Error 403: Forbidden Unexpected HTTP status 403. Aborting.
```

と出たら、ホームディレクトリに`.appcfg_oauth2_tokens`が生成されているためなので、このファイルを削除してdeployを再実行してください。うまくいくとブラウザが開き、googleアカウントでログインする画面が表示されます。

ログインするとデプロイ処理が進みます。Compile failedが出る場合はソースコードに誤りがあります。直して再デプロイしてください。`Deployment successful.`と表示されたら成功です。

## GCP Consoleでロギングを確認。

<img width="1158" alt="スクリーンショット 2016-10-18 13.04.00.png" src="https://119.qiita.com/files/8302840b-a372-c964-ff07-ed7f803f2ba0.png">

「!!」アイコンの行を展開すると、最下段に

```
ReplayMessage: linebot: APIError 403 Access to this API denied due to the following reason: Your ip address [Your IP address] is not allowed to access this API. Please add your IP to the IP whitelist in the developer center.
```

と表示されているのがわかります。

これはLINE DevelopersのServer IP Whitelistに[Your IP address]が登録されていないために、GCPからLINE APIの使用が許可されていないことを表すエラーメッセージとなります。

## IP AddressをServer IP Whitelistに登録
*10月18日時点の状態です。現在は制限が無く、IP Whitelistへの追加は不要かもしれません。*

LINE DevelopersのServer IP WhitelistにGCPで確認したIP Addressを追加します。[^2]

![line_dev.png](https://119.qiita.com/files/78c628c6-9fdd-d877-da67-391904cd081e.png)


[^2]: IP Addressは複数あるので/24か/30のレンジで登録します。

# LINEで友達登録しテキスト送信

<img width="365" alt="スクリーンショット 2016-10-18 13.16.56.png" src="https://119.qiita.com/files/42800b46-cc81-1b72-84b9-3389f5864e50.png">