LINE BotのAPIが新しくなりました!
MessageAPIがreleaseされて 新しいメッセージタイプやグループへの招待ができるようになりました。
お試しのTrialBotはこれからはDeveloper Trialとなります。
友達制限が50人以外は有料のプランと同等の機能が試せるとのことなので 名前の通り開発用ですね。
App Engineを使う理由として豊富な無料枠と標準でHTTPS通信が可能なためです。
そして Bot Trial の時はIPを登録する必要がありましたが 現在はオプショナルとなっているのでIPを登録する必要がなくなってるのでIPが不定のAppEngineでも扱いやすくなりました!!
事前準備
Developer Trialを登録するにはLINEアカウントが必要です。
Google App Engineを利用するには事前にGoogleアカウントが必要です。
以下のURLからGAE/Go SDKのダウンロードが必要です。(Homebrewでも入れられます)
Cloud Consoleで新しいプロジェクトを作成してprojectIDをメモっておきましょう。
登録
LINE BUSINESS CENTERから登録できます。
右上のボタンからログインして、ログインできたら右上から [会社/事業者未選択] を選択し
[会社/事業者を追加する]ボタンをクリックして必要事項を記入していきます。
作成できたら MessageAPIをクリックして Developer Trialを選択します。
アカウント名と業種を入力して LINE@ MANAGERに行きます。
Bot設定から[APIを利用する]ボタンを押して有効にします。
そしたら各種設定ができるようになります。
今回はBotの設定を以下のようにしました。
Webhook送信とBotグループトーク参加を利用するにしました。
またLINEビジネスセンターに戻りアカウントリストから登録したBotアカウントを見つけて、Messaging APIの横に[LINE Developers]のボタンをクリックしてLINE Developersに行きます。
そこに Client Secret と Client Token が手に入るのでメモっておきましょう。
下のほうにいくと EDITとあるので編集画面に行きWebhookにhttps://<projectID>.appspot.com/callback
を設定します。
公式サイトに動画があるのでそちらも確認してください。
simple Bot
今回はSDKを使いつつ AppEngineらしいやり方で実装した 単純にEventを受けて "OK"と返すだけのBotを作りました。
SDKのインストール
イベントのWebhookはJSONで受け取ることができますが 公式に何種類かの言語に対応したSDKが配布されていて その中にGoがあるので それを使います。
インストールは以下のコマンドです。 ついでに今回使うパッケージもインストールしておきます。
$ 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
app.yaml
適当なディレクトリを作成します。
$ mkdir linebot; cd linebot
そのディレクトリの中にapp.yaml
を作成します。
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
application
のところには 最初に用意したGCPのprojectIDを入れておきましょう 例えば linebot-example
だったら以下のようになります。
application: linebot-example
あとはAppEngineのほぼ最小構成に/task
以下は管理者のみのアクセスに指定限定した設定です。
/task
には後ほど TaskQueue を使ってアクセスするために設定しています。
実装
まずは 設定ファイルの作成をします。
line.env
というファイルを作成します。
これはChannelSecretなどの秘密情報を書いておくためのファイルです。
LINE_BOT_CHANNEL_SECRET=<ChannelSecret>
LINE_BOT_CHANNEL_TOKEN=<AccessToken>
このファイルをgodotenv
を使って環境変数に読み込みます。
僕は秘密情報はコードには直接含めずに 暗号化して デプロイ環境で復号化してデプロイをするという構成を取っているためです。
めんどくさい人は コードに直接書いてもいいと思いますが そのコードは公開しないように注意しておきましょう。
以下のコードはline.env
を環境変数に読み込むための処理です。
読み込みに失敗したらそのまま死にます。
func init() {
err := godotenv.Load("line.env")
if err != nil {
panic(err)
}
}
次に Handler を設定しておきます。
Webhookを受け取るやつと実際に処理をするためのHandlerを作成します。
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)
}
// Webhook を受け取って TaskQueueに詰める関数
func handleCallback(evs []*linebot.Event, r *http.Request) {
}
// 受け取ったメッセージを処理する関数
func handleTask(w http.ResponseWriter, r *http.Request) {
}
次に *linebot.Client を作成するための 関数を作っておきます。
handler内に毎回書いてては長くなってしまうためです。
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...)
}
特徴としては linebot.WithHTTPClient(urlfetch.Client(c))
です。
urlfetchのクライアントを使って通信するように設定します。
本当はnewLINEBot
って関数を書きたくはなく*linebot.Client
は常に一つでリクエストごとにTransportを切り替えたいのですが(issue/15) とりあえずこういう書き方になります。
そして次は webhook を受け取る Handler を実装します。
まずは関数全体から
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, "")
}
linebot/httphandler.WebhookHandler
を使っているので検証をした状態で[]*linebot.Event
を渡してくれます。 (Add event handler #17で取り込まれました)
なのでこのhandler
では[]*linebot.Event
を一度jsonにシリアライズして PushQueue用のTaskのdata
にセットして、最後にtaskqueue.AddMulti(c, ts, "")
でdefaultのPushQueueにTaskを詰めます。
次に処理側です。
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)
}
以下の部分でまずデシリアライズしています。
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
}
あとは 単純にok
を返すだけの処理です。
m := linebot.NewTextMessage("ok")
if _, err = bot.ReplyMessage(e.ReplyToken, m).WithContext(c).Do(); err != nil {
errorf(c, "ReplayMessage: %v", err)
return
}
デプロイ
Deployには以下のコマンドでできます。
$ goapp deploy
今回のコードはGitHubにあげてあります。
まとめ
ちょっとめんどくさく感じるかもしれませんが
大量のメッセージに耐える設計を意識しているためです。
参考: 大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ
ちなみに新しいSDKがreleaseされた時はGAE/Go SDKでビルドできない問題があったり
TaskQueueに詰めるためにJSONにシリアライズしようとしたら、できなかったりで躓きまくってましたが、なんとか最低限AppEngineで実装できるところまでは行けるようになりました。
次回は Beaconかもうちょい実践的なBotを作りたいと思います。