GAE/GoでLINE Bot Trial

オウム返しするLINE BotをGAE/Goで動かしてみました。

# コード
コードは以下の通りです

```yaml:app.yaml
runtime: go
api_version: go1
threadsafe: yes

default_expiration: 1d

instance_class: F1
automatic_scaling:
  min_idle_instances: 0
  max_idle_instances: 1
  min_pending_latency: automatic
  max_pending_latency: automatic
  max_concurrent_requests: 120

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

```go:main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httputil"
	"sync"

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

var (
	LINE_CHANNEL_ID     = ""
	LINE_CHANNEL_SECRET = ""
	LINE_CHANNEL_MID    = ""
)

type Result struct {
	ID          string                 `json:"id"`
	EventType   string                 `json:"eventType"`
	From        string                 `json:"from"`
	FromChannel int                    `json:"fromChannel"`
	ToChannel   int                    `json:"toChannel"`
	To          []string               `json:"to"`
	CreateTime  int                    `json:"createdTime"`
	Content     map[string]interface{} `json:"content"`
}

type Request struct {
	Result []*Result `json:"result"`
}

type Response struct {
	To        []string               `json:"to"`
	ToChannel int                    `json:"toChannel"`
	EventType string                 `json:"eventType"`
	Content   map[string]interface{} `json:"content"`
}

func (r *Response) Do(client *http.Client) (*http.Response, error) {
	b, err := json.Marshal(r)
	if err != nil {
		return nil, fmt.Errorf("Send error: %v\n", err)
	}
	req, err := http.NewRequest("POST", "https://trialbot-api.line.me/v1/events", bytes.NewReader(b))
	if err != nil {
		return nil, fmt.Errorf("NewRequest error: %v\n", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Line-ChannelID", LINE_CHANNEL_ID)
	req.Header.Set("X-Line-ChannelSecret", LINE_CHANNEL_SECRET)
	req.Header.Set("X-Line-Trusted-User-With-ACL", LINE_CHANNEL_MID)
	return client.Do(req)
}

func init() {
	http.HandleFunc("/line/receive", receiveLine)
}

func receiveLine(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	dump, err := httputil.DumpRequest(r, true)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	var req Request
	err = json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}

	ctx := appengine.NewContext(r)
	log.Infof(ctx, "%s\n", dump)
	log.Infof(ctx, "Request: %#v\n", req)

	client := urlfetch.Client(ctx)

	var wg sync.WaitGroup
	wg.Add(len(req.Result))
	for _, m := range req.Result {
		go func(m *Result) {
			defer wg.Done()
			res := &Response{
				To:        []string{m.Content["from"].(string)},
				ToChannel: 1383378250,
				EventType: "138311608800106203",
				Content:   m.Content,
			}
			resp, err := res.Do(client)
			defer resp.Body.Close()
			if err != nil {
				log.Errorf(ctx, "SendRequest error: %v\n", err)
				return
			}
			body, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				log.Errorf(ctx, "response pershing error: %v\n", err)
				return
			}
			log.Infof(ctx, "SendResponse: %s\n", body)
		}(m)
	}
	wg.Wait()
	fmt.Fprintf(w, "ok")
}
```

# CallbackとIP Addressの登録

Callbackに`https://{project-id}.appspot.com:443/line/receive`を登録したら
一度メッセージを送って

`{"statusCode":"427","statusMessage":"Your ip address [IP Address] is not allowed to access this API."}`
エラーメッセージに書かれてるIP Addressをwhitelistに登録すれば動きます。

# まとめ

IPがいつも固定とは限らないので変わってしまったら動かなくなります。
whitelistのIPを指定できるレンジをもっと広げてくれればGAEが使うIPを全部カバーできるのですが...

もし本番でもホワイトリストがこのままならメッセージの送信にはGCEとかでIPを固定できるインスタンスを建てる事になりそうですね。