オウム返しするLINE BotをGAE/Goで動かしてみました。
コード
コードは以下の通りです
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
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を固定できるインスタンスを建てる事になりそうですね。