Google Cloud Platform, Cloud FunctionsでつくるLINE Bot(2)

[第1回](https://qiita.com/hsmtkk/items/ffea53fdae25cb50eddc)でCloud FunctionsとLINE Botを作成し、Cloud Functionsにログの出力されることを確認しました。
第2回では受信したメッセージをそのまま返信する機能(オウム返し)を実装します。

# 完成イメージ

![line-parrot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/4747a1f0-71aa-d6b1-59fa-64482a76d1e0.png)

# メッセージを受信する

受信するWebHookは以下のようなJSONになっています。
[Messaging APIリファレンス](https://developers.line.biz/ja/reference/messaging-api/#message-event)も参考にしてください。


```json
{
    "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構造体でデコードできます。

```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` 構造体にデコードするだけです。

```Go
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
}
```

# メッセージを返信する

[応答メッセージを送る](https://developers.line.biz/ja/reference/messaging-api/#send-reply-message)にしたがいコーディングします。

認証のためチャネルアクセストークンを付与します。
チャネルアクセストークンには3つ種類があります。
- v2.1
- 短期
- 長期

今回はもっとも簡単な「長期」のチャネルアクセストークンを使用することにします。
LINE Developersのコンソール画面で、長期のチャネルアクセストークンを発行します。

発行したチャネルアクセストークンをSecret Managerに登録します。
![secret-manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/9c1c1099-df83-72cb-9cca-cf27df211671.png)

さらにSecretをCloud Functionsの環境変数に設定します。
[コンソールやgcloud CLIで設定](https://cloud.google.com/functions/docs/configuring/secrets?hl=ja#making_a_secret_accessible_to_a_function)できます。
私は[Terraform](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudfunctions2_function#nested_secret_environment_variables)の機能を使いました。
![cloud-functions-secret.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1950110/d4253f0a-1a47-bbbc-3c48-967ad81a73c8.png)

これでCloud Functionsから安全にチャネルアクセストークンを取得できるようになります。

返信部分の処理は以下のとおりです。
受信したReplyTokenとオウム返しを `ReplyMessage` 構造体につめ、JSONにエンコードします。
またチャネルアクセストークンを Authorization Bearer ヘッダーにセットします。

```Go
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プラットフォーム外からのアクセスも受け入れてしまう問題があります。
[次回](https://qiita.com/hsmtkk/items/9dc31403ebdf1f11a0b1)はLINEプラットフォームからのアクセスだけ処理するように実装を改善します。