LINE bot作成 C#+AWS(Lambda)

#概要
自分でLINE botを作成した際、C#+AWS(Lambda)環境で実装してみたのですが、
この技術スタックでの情報が全く見つかりませんでした。
(自分の探し方が悪かっただけかもしれませんが…)
そこで、今後C#+AWS(Lambda)で作成したい！という方のために手順をまとめておきます。

本記事では、LINEから送信された文字をオウム返しするbotが作成できます。
このベースさえ作ってしまえば、LINE公式ドキュメントを参考に色々いじって楽しめると思います！

内容としては、LINE Developers登録からLambda関数のプログラミング、AWSへのデプロイから最終的に実際にLINEで確認するところまでやってしまいます。

※AWS Lambdaへのデプロイは、Windowsの場合、Visual Studioを使用すればGUIポチポチで完了するので簡単ですが、
Visual Studio for Macの場合はそうはいかないので、
今回はどの環境でも対応できるようCUIからデプロイする操作を記載しております。

#事前準備編(LINE Developers登録)

###LINE Developers登録及びチャンネル作成
LINE botを作成するにあたり、「LINE Developers」に登録する必要があります。
手順は簡単で、申請待ち時間も無いので数分で登録可能です。
下記サイトを参考に、アカウントの作成及びチャンネルの作成まで行います。
※1アカウントで「チャンネル」を複数作成することが可能で、「チャンネル」ごとに別のbotを作成できます。

https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-oa-manager

###チャンネルアクセストークンの発行
LINE botを作成するにあたり、最低限「チャンネルアクセストークン」は発行する必要があります。

チャンネルの作成まで完了したら、[Line Develpers](https://developers.line.biz/console/)にログインし、作成したチャンネルのページにアクセスします。

![スクリーンショット 2021-06-03 17.16.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/b64c45de-412a-4b66-4bc7-3dfa4ab3651f.png)

「Messagin API設定」タブをクリックします。

![スクリーンショット 2021-06-03 17.22.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/914f8eab-009f-bdbe-3250-89816ebef98a.png)

ページ下部までスクロースし、「チャネルアクセストークン」の発行ボタンをクリックします。
発行されたキーは後ほど使用するのでどこかにメモをしておいて下さい。
※後ほど再度このページにアクセスして発行済のキーを確認することは可能です。

![スクリーンショット 2021-06-03 17.25.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/9f17f41a-8f9c-469a-1e4e-2ebdedb5b463.png)

#プログラミング編
無事アクセストークンの発行が完了したら、続いてプログラミングを行っていきます。
C#でのLambda関数プログラミングは、Windowsの場合はVisual Studioを使用すればあらかじめテンプレートが用意されているので簡単ですが、
Visual Studio for Macには用意されていないため、gitにテンプレートを用意しておきました。
※AWSへデプロイするための設定ファイル等も同梱してあります。

https://github.com/ryohei0109-develop/LambdaSample

ローカルにプロジェクトの準備が完了したら、「Function.cs」ファイルを開きます。
この中の「FunctionHandler()」メソッドがLambda実行時に呼び出されるメソッドです。
つまり、LINEのMessagingAPIからWebhookで呼び出されることになります。

今回はサンプルでbot利用ユーザが入力された文章をそのままオウム返しするようにしてみます。

●LINEからLambdaへのリクエスト内容をモデリング

```csharp:WebHookRequestModel.cs
using System.Collections.Generic;
using Newtonsoft.Json;

namespace LambdaSample.Models
{
    public class WebHookRequestModel
    {
        [JsonProperty("events")]
        public List<EventModel> EventModels { get; set; }
    }
}
```

```csharp:EventModel.cs
using Newtonsoft.Json;

namespace LambdaSample.Models
{
    public class EventModel
    {
        [JsonProperty("message")]
        public MessageModel Message { get; set; }

        [JsonProperty("replyToken")]
        public string ReplyToken { get; set; }
    }
}
```

```csharp:MessageModel.cs
using Newtonsoft.Json;

namespace LambdaSample.Models
{
    public class MessageModel
    {
        [JsonProperty("type")]
        public string Type { get; set; }

        [JsonProperty("text")]
        public string Text { get; set; }
    }
}
```

●LambdaからLINEへのリプライモデル

```csharp:ReplyRequestModel.cs
using System.Collections.Generic;
using Newtonsoft.Json;

namespace LambdaSample.Models
{
    public class ReplyRequestModel
    {
        [JsonProperty("replyToken")]
        public string ReplyToken { get; set; }

        [JsonProperty("messages")]
        public List<MessageModel> Messages { get; set; }

        /// <summary>
        /// true: ユーザに通知されない(デフォルト)
        /// false: ユーザに通知される
        /// </summary>
        [JsonProperty("notificationDisabled")]
        public bool NotificationDisabled { get; set; }
    }
}

```

●メインの処理

```csharp:Function.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using BasicExtension;
using LambdaSample.Models;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaSample
{
    public class Function
    {
        public Function()
        {
            _httpClient = new HttpClient();
            _httpClient.DefaultRequestHeaders.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
                Environment.GetEnvironmentVariable("CHANNEL_ACCESS_TOKEN"));
        }

        public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest input, ILambdaContext context)
        {
            WebHookRequestModel webHookRequestModel = input.Body.ToObject<WebHookRequestModel>();

            string replyToken = webHookRequestModel.EventModels[0].ReplyToken;
            MessageModel message = webHookRequestModel.EventModels[0].Message;

            ReplyToLine(replyToken, message.Text);

            return CreateLambdaApiResponse();
        }

        private void ReplyToLine(string replyToken, string inputText)
        {
            string stringRequest = CreateReplyRequestModel(replyToken, inputText).ToJson();
            StringContent stringContent = new StringContent(stringRequest, Encoding.UTF8, "application/json");
            _httpClient.PostAsync("https://api.line.me/v2/bot/message/reply", stringContent).Wait();
        }

        private ReplyRequestModel CreateReplyRequestModel(string replyToken, string inputText)
        {
            ReplyRequestModel replyRequestModel = new ReplyRequestModel();
            replyRequestModel.ReplyToken = replyToken;
            replyRequestModel.Messages = new List<MessageModel>()
            {
                new MessageModel()
                {
                    Type = "text",
                    Text = inputText
                }
            };
            replyRequestModel.NotificationDisabled = false;

            return replyRequestModel;
        }

        private APIGatewayProxyResponse CreateLambdaApiResponse()
        {
            return new APIGatewayProxyResponse
            {
                StatusCode = 200,
                Body = null,
                IsBase64Encoded = false,
                Headers = new Dictionary<string, string>() { { "Content-Type", "application/json" } }
            };
        }

        private readonly HttpClient _httpClient;
    }
}
```

#AWSへデプロイ

こちらの記事をご参考下さい。
https://qiita.com/ryohei0109_develop/items/ff05cfad2cb276af1169


今回の例ではjsonファイルはこのようになります。
※「function-role」だけはどうしても書き換えて下さい！

```json:aws-lambda-tools-defaults.json
{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "configuration": "Release",
  "framework": "netcoreapp3.1",
  "profile": "default", // AWS CLI プロフィール
  "region": "ap-northeast-1", // AWSリージョン指定
  "function-runtime": "dotnetcore3.1", // Lambbda実行ランタイム
  "function-memory-size": 512, // Lambdaメモリ(MB)
  "function-timeout": 30, // Lambdaタイムアウト時間(seconds)
  "function-handler": "LambdaSample::LambdaSample.Function::FunctionHandler", // Lambda実行時にコールするメソッドを指定
  "function-name": "LambdaSample", // Lambda名称
  "function-role": "arn:aws:iam::123456789012:role/xxxxxRole" // AWSロール名
}
```

#Lambda環境変数にチャンネルアクセストークンを設定
チャンネルアクセストークンを環境変数に設定しておくと、今後チャンネルアクセストークンの変更があった際、
プログラムを変更することなくGUIから操作するだけで対応可能になります。

AWSマネジメントコンソールにログイン->Lambda->先程デプロイした関数を選択->「設定」タブをクリック。
![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/420f4a04-b865-ffb4-983a-fcbf714254ba.png)

「編集」ボタンをクリック。
![スクリーンショット 2021-06-12 11.09.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/8a9e0a31-3200-c0e9-fbc9-8bf396de7c0f.png)

キーに「CHANNEL_ACCESS_TOKEN」を指定し、
値に先程Line Develpersで発行した「チャネルアクセストークン」を入力し、「保存」ボタンをクリック。
![スクリーンショット 2021-06-12 11.13.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/577b323d-8cef-108d-ceab-d813372d854e.png)

これで環境変数の設定は完了です！

ここで設定した環境変数は、C#側からは以下のような形で参照することができます。

```csharp
string value = Environment.GetEnvironmentVariable("CHANNEL_ACCESS_TOKEN")
```

#LambdaをAPI化
Lambda関数を作成しただけでは、まだLINEからのWebhookを受けることができません。
そこで「API Gateway」を行い、先程作成したLambda関数をAPI化します。

AWSのApiGatewayにアクセスし、「APIを作成」ボタンをクリックします。
![スクリーンショット 2021-06-04 18.38.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/06b6d066-9f10-b6fd-a59b-6b0d48964f93.png)

少し下にスクロールし、「REST API」の「構築」ボタンをクリックします。
![スクリーンショット 2021-06-04 18.41.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/708f6530-faf2-f569-2e24-fb944dacf679.png)

任意の「API名」を入力し、「APIの作成」ボタンをクリックします。
![スクリーンショット 2021-06-04 18.43.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/fea1f55e-6016-2a83-a3d2-301898f18926.png)

APIの大枠が作成できたら、続いてリソースを作成していきます。

「アクション」から「リソースの作成」をクリックします。
![リソースの作成_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/e70298b9-1ae1-abe8-7af8-d673812cdd53.png)

任意のリソース名を入力し、「API Gateway CROSを有効にする」にチェックを入れ、「リソースの作成」ボタンをクリックします。
![ほげ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/e3032d6b-1ca8-2a1e-fb8f-4bc905118ff4.png)

作成したリソースにPOSTメソッドを追加していきます。
「アクション」から「メソッドの作成」をクリックします。
![ほげ2_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/fd43f5fa-7609-1dc1-1e17-17afd7362d91.png)

プルダウンから「POST」を選択し、
![ほげ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/64c656da-bd1e-1b31-4166-ad0c33c2c00f.png)

チェックアイコンをクリックすると、POSTメソッドが作成できます。
![ほげ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/d7a61411-f7ae-591f-f62c-55b0c2917792.png)

「Lambdaプロキシ統合の使用」にチェックし、
先程作成したLambda関数名を入力します。(※サジェストしてくれます)
完了したら「保存」ボタンをクリックします。
![ほげ5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/b0587847-56e7-8b40-cad8-95a0649783fc.png)

「Lambda 関数に権限を追加する」というダイアログボックスが表示されるので、
「OK」ボタンをクリックするとPOSTメソッドの作成が完了します。
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/ea716253-a868-ed7d-d4b4-e84de829ce04.png)

POSTメソッドの作成が完了したので、続いてデプロイを行います。
「アクション」から「APIのデプロイ」をクリックします。
![スクリーンショット 2021-06-04 19.01.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/d8d538ed-c382-e0d7-a481-82f53737b7af.png)

「デプロイされるステージ」に「新しいステージ」を選択し、
「ステージ名」に任意の名前を入力し、「デプロイ」ボタンをクリックします。
![スクリーンショット 2021-06-05 10.38.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/51bf2529-5e09-b3e1-d03d-55d3d8a570e6.png)

これでAPI化の準備は完了です！
「POST」を選択し、「URLの呼び出し」に表示されているURLがAPIのエンドポイントになるので、メモしておきます。
![スクリーンショット 2021-06-05 10.42.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/6342f021-8c32-bca1-d159-fa5389ef9f59.png)

#自動応答メッセージをOFF
デフォルトの設定では、ユーザがLINEで文章を送ると自動的にメッセージが返信される設定になっています。
今回は作成したbotから返信を行うので、デフォルトの自動応答メッセージ機能をOFFにする必要があります。

[LINE Official Account Manager](https://manager.line.biz/)にログインします。 

作成したチャンネルをクリックします。
![スクリーンショット 2021-06-10 18.24.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/fc927ebc-4f21-92a2-0057-5864aac096c9.png)

画面右上の「設定」をクリックします。
![test4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/02874977-f97a-ce5e-d94d-1d72eaa347a1.png)

レフトナビの「応答設定」をクリックします。
※ちなみにこの画面でアカウント名やプロフィール画像を変更できます。
![スクリーンショット 2021-06-10 18.32.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/3a045130-4c3a-b2ae-9d90-1613f06ee537.png)

・応答モード: 「Bot」
&nbsp;&nbsp; 「チャットモード」にすると、Webhookが利用できません。

・あいさつメッセージ: 「オン」or「オフ」
&nbsp;&nbsp; 友達登録時の初回自動送信メッセージのON/OFF切り替えになります。
&nbsp;&nbsp; こちらは好みで良いと思います。

・応答メッセージ: 「オフ」
&nbsp;&nbsp; 今回は自分で作成したプログラムから応答するので、オフにしておきます。

・Webhook: 「オン」
&nbsp;&nbsp; オンでないとLINE botが作成できないですね！

![スクリーンショット 2021-06-10 18.34.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/8a447aef-52ec-559f-cf80-58595892da38.png)

これでこの画面での設定は完了です。
あともう少しです！がんばりましょう！

#Webhook登録
先程発行したAPIのエンドポイントを、LINE DevelopersにてWebhook URLとして登録します。

[LINE Developers](https://developers.line.biz/console/)にアクセスし、
作成したチャンネルのページにアクセスし、
「Messaging API設定」タブを選択します。

![スクリーンショット 2021-06-05 11.19.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/9bab67d2-76cc-ed24-bcfb-0dec967c7a97.png)

・ページ下部までスクロールし、Webhookの利用をON
・Webhook設定の「編集」をクリックし、先程発行したAPIのURLを登録します。
![test01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/f96bcb2d-362f-bd96-1f71-9410ce995442.png)

Webhook URLの設定が完了したら、「検証」ボタンをクリックし、疎通確認を行います。
正常に疎通できている場合は、「成功」と表示されます。
![スクリーンショット 2021-06-10 18.16.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/4b58347f-1b48-0dab-9353-19320e7f323c.png)

#LINEアプリで最終確認
最後は先程の画面に表示されているQRコードを読み取って友達登録を行い、
LINEアプリからも正常に動作するか確認してみましょう！

以上です！お疲れ様でした！

#蛇足
参考までに今回私が作成したconpassのイベント検索ができるLINE botも紹介させて下さい。
![048dzyjf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/694199/87fa2042-6826-e346-6a56-6f5e775a3a1c.png)

git: https://github.com/ryohei0109-develop/SearchConnpassLineBot