• 36
    Like
  • 0
    Comment
More than 1 year has passed since last update.

オウム返しする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を固定できるインスタンスを建てる事になりそうですね。