第1回でCloud FunctionsとLINE Botを作成し、Cloud Functionsにログの出力されることを確認しました。
第2回では受信したメッセージをそのまま返信する機能(オウム返し)を実装します。
完成イメージ
メッセージを受信する
受信するWebHookは以下のようなJSONになっています。
Messaging APIリファレンスも参考にしてください。
{
"events": [
{
"source": {
"userId": "U8510be0371fb4cf398a62d75427928b6",
"type": "user"
},
"webhookEventId": "01GQRGNPKWAB8PJX98PZGPZP5Y",
"deliveryContext": {
"isRedelivery": false
},
"replyToken": "b9de30e38be744f192141be6b0fb5b0c",
"timestamp": 1674786297985,
"type": "message",
"message": {
"type": "text",
"text": "こんにちは",
"id": "17539623968937"
},
"mode": "active"
}
]
}
したがって以下のようなGo構造体でデコードできます。
type MessageEvents struct {
Events []MessageEvent `json:"events"`
}
type MessageEvent struct {
ReplyToken string `json:"replyToken"`
Message Message `json:"message"`
}
type Message struct {
Type string `json:"type"`
Text string `json:"text"`
}
メッセージの返信に以下2つが必要です。
- (オウム返しなので)
Message
構造体のText
部分 - 認証のため
MessageEvent
構造体のReplyToken
部分
受信部分の処理は以下のとおりです。
受信したJSONを MessageEvents
構造体にデコードするだけです。
func receive(r *http.Request) (MessageEvents, error) {
var events MessageEvents
reqBytes, err := io.ReadAll(r.Body)
if err != nil {
return events, fmt.Errorf("failed to read request body; %w", err)
}
defer r.Body.Close()
if err := json.Unmarshal(reqBytes, &events); err != nil {
return events, fmt.Errorf("failed to decode JSON; %w", err)
}
return events, nil
}
メッセージを返信する
応答メッセージを送るにしたがいコーディングします。
認証のためチャネルアクセストークンを付与します。
チャネルアクセストークンには3つ種類があります。
- v2.1
- 短期
- 長期
今回はもっとも簡単な「長期」のチャネルアクセストークンを使用することにします。
LINE Developersのコンソール画面で、長期のチャネルアクセストークンを発行します。
発行したチャネルアクセストークンをSecret Managerに登録します。
さらにSecretをCloud Functionsの環境変数に設定します。
コンソールやgcloud CLIで設定できます。
私はTerraformの機能を使いました。
これでCloud Functionsから安全にチャネルアクセストークンを取得できるようになります。
返信部分の処理は以下のとおりです。
受信したReplyTokenとオウム返しを ReplyMessage
構造体につめ、JSONにエンコードします。
またチャネルアクセストークンを Authorization Bearer ヘッダーにセットします。
func reply(replyToken, text string) error {
url := "https://api.line.me/v2/bot/message/reply"
body, err := json.Marshal(ReplyMessage{
ReplyToken: replyToken,
Messages: []Message{{Type: "text", Text: text}},
})
if err != nil {
return fmt.Errorf("failed to encode JSON; %w", err)
}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to make new request; %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+os.Getenv("CHANNEL_ACCESS_TOKEN"))
reqDump, err := httputil.DumpRequest(req, true)
if err != nil {
return fmt.Errorf("failed to dump HTTP request; %w", err)
}
log.Print("dump HTTP request")
log.Print(string(reqDump))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send HTTP request; %w", err)
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return fmt.Errorf("failed to dump HTTP response; %w", err)
}
log.Print("dump HTTP response")
log.Print(string(respDump))
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return fmt.Errorf("got error HTTP response; %d; %s", resp.StatusCode, resp.Status)
}
return nil
}
ソースコード全体
ソースコード全体は以下のとおりです。
https://github.com/hsmtkk/qiita-gcp-cf-line-bot/blob/main/parrot/parrot.go
次回
以上でオウム返しの処理を実装できました。
しかし、今の実装ではLINEプラットフォーム外からのアクセスも受け入れてしまう問題があります。
次回はLINEプラットフォームからのアクセスだけ処理するように実装を改善します。