Bot Framework V4で編集しやすいベースとなるボットを作成してAzureにデプロイ

#背景
Microsoft Bot Framework V4が2018年9月末にGAしました。Bot Framework V3で元々ボット開発をしていましたが、GAされたからには最新バージョンで作らねば...ということで手を出しましたが、結構躓きましたorz（V3とV4はもはや別フレームワークw）
まだGAされてから時間があまり経っていないということもあって世の中に情報も少ないので、記事を書かせていただきました。誰かのお役に立てれば幸いです。
#目的
**Bot Framework V4で編集しやすいベースとなるボットを作成してAzureにデプロイ**
(タイトル通りですが...)

既に投稿されている記事の多くは下記二つの方法が多いです。(2018/12時点)
1. Azure PortalでWeb App Botを作成⇒ソースコードダウンロード⇒ローカルで修正⇒Azureに再デプロイ
2. Visual Studioテンプレートを利用⇒ローカルで修正⇒Azureにデプロイ⇒Azure PortalでBot Channel Registration作成
 
確かにどちらの方法でもボット作成が可能ですが、
1.の方法ではVSプロジェクトやアプリ名が固定されてしまう。
2.の方法ではテンプレート自体を理解することが初心者には厳しい。 
　(ミドルウェア、アクセサーなどの各単語を理解しておく必要あり)
などちょっと扱いづらい部分が多々あり、これをベースにして自分用にカスタマイズするのはちょっと辛いかなと感じています。

そこで、ボットは単なるWeb APIアプリケーションのことであるというところを考えて作成していきます。
（この記事ではカスタマイズしやすいボットを作成してデプロイすることが目的なので、ボット自体は簡単なユーザが入力した文字とその文字数を返すボットを作成します。）

#開発に必要な環境
Visual Studio 2017
dotnet core 2.1
Bot Framework Emulator
Microsoft Azure 

#WebAPIプロジェクト作成（Visual Studio 2017）
まず下記の手順でWeb APIプロジェクトを作成します。

1. Visual Studio 2017を起動
2. 「ファイル」＞「新規作成」＞「プロジェクト」を選択
3. 「ASP.NET Core Webアプリケーション」プロジェクトテンプレートを選択し、[名前]を適当に入力し「OK」を選択（この記事では名前をStrCountBotとします）![project_making.png](https://qiita-image-store.s3.amazonaws.com/0/281074/776ec1c2-1a93-1475-b80d-0822ca456096.png)
4. 「API」を選択し、「OK」を選択することでWebAPIプロジェクトが作成されます![aspnetcorewebapplication.png](https://qiita-image-store.s3.amazonaws.com/0/281074/96adbf0a-4f05-984d-3b49-f66a9370e4e7.png)

#Botにするための改修
ここは主に[@kenakamuさん](https://qiita.com/kenakamu)の[記事](https://qiita.com/kenakamu/items/ccc8559cd9b8c1b20f7a)を参考にさせていただいています。

1. コントローラは不要なので、Contollersフォルダを削除
2. プロジェクト名を右クリック、「NuGetパッケージの管理」を選択
3. NuGetパッケージマネージャが表示されるので、参照のタブで「Microsoft.Bot.Builder.Integration.AspNet.Core」を検索し、インストール![nuget.png](https://qiita-image-store.s3.amazonaws.com/0/281074/92d74b92-fa96-9d97-3b2d-635ff21803ff.png)
4. プロジェクトにStrCountBot.csを追加し、コードを以下のようにする

    ```C#
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Schema;

    namespace StrCountBot
    {
        public class StrCountBot : IBot
        {
            public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
            {
                // ユーザが入力した文字とその文字数を返す
                if (turnContext.Activity.Type == ActivityTypes.Message && !string.IsNullOrEmpty(turnContext.Activity.Text))
                    await turnContext.SendActivityAsync("「" + turnContext.Activity.Text + "」の文字数は" + turnContext.Activity.Text.Length + "文字です");
            }

        }
    }
    ```

5. Startup.csのコードを下記に修正

    ```C#
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;

    namespace StrCountBot
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddBot<StrCountBot>();
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                app.UseBotFramework();
            }
        }
    }
    ```

以上で、ボットが作成できたので動作を確認してみます。
(なぜこんなコードになるのか気になる人は冒頭で述べたようにこの[記事](https://qiita.com/kenakamu/items/ccc8559cd9b8c1b20f7a)を見てください。非常にわかりやすいです。)

#Botをローカルでテスト (Bot Framework Emulator)

1. 「デバッグの開始」を押して実行
2.  Visual Studioの出力タブより、ローカルで接続するhttpのポートを確認する（今回は64553）![http_endpoint.png](https://qiita-image-store.s3.amazonaws.com/0/281074/10c456a3-ef7d-2b07-23a5-ab3c66b7f128.png)
3.  Bot Framework Emulatorを起動
4.  Welcomeタブにある「create a new bot configuration.」をクリック
5.  「名前」を「StrCountBot」、「Endpoint URL」を「http\://localhost:64553/api/messages」と入力し、「Save and connect」をクリックして、.botファイルをプロジェクトフォルダに保存![emu_create.png](https://qiita-image-store.s3.amazonaws.com/0/281074/3732936a-18ab-1b9b-1576-5df2ba2f65f9.png)![botfile.png](https://qiita-image-store.s3.amazonaws.com/0/281074/95893841-ce19-9789-24d5-58f32514bb02.png)
6.  チャット画面が表示されるので、メッセージを送信して動くことを確認する。![chat.png](https://qiita-image-store.s3.amazonaws.com/0/281074/4bdda31a-d822-51be-40d8-372d05b2bc19.png)

#Azureにデプロイ (Viusal Studio 2017)
ローカルで動作することを確認したのでAzureにデプロイします。
1. プロジェクト名を右クリック、「発行」を選択
2. 発行先を「App Service」を選択し、「詳細設定...」を選択
3. ファイル発行オプションの「発行先の追加オプションを削除する」にチェックマークを付けて保存
4. プロファイルの作成を選択
5. 「アプリ名」「サブスクリプション」「リソースグループ」「ホスティングプラン」「Application Insights」の各項目を入力して作成　(「リソースグループ」「ホスティングプラン」は既存のものがない場合は新規作成しましょう)![publish.png](https://qiita-image-store.s3.amazonaws.com/0/281074/7bff274c-020a-7b69-2886-bdb89f8d8b22.png)
6. 作成が完了したら「発行」を選択![publish_2.png](https://qiita-image-store.s3.amazonaws.com/0/281074/2b7dce48-00bd-e775-c250-1cfc8912103d.png)
以上でAzureにデプロイができました。次は、Bot Serviceへの登録作業になります。

#Bot Channels Registrationを作成 (Azure Portal)
Azureにデプロイしたボットを登録します。
1. Azure Portalで「リソースの作成」で「Bot Channels Registration」を作成
2. 「ボット名」「サブスクリプション」「リソースグループ」「場所」「価格レベル」「メッセージングエンドポイント」「Application Insights」「MicrosoftアプリIDとパスワード」の各項目を入力して作成![botregistration.png](https://qiita-image-store.s3.amazonaws.com/0/281074/b0c95583-16f2-e0ba-0042-b6d1feaae0be.png)＊「メッセージングエンドポイント」は、https://~.azurewebsites.net/api/messages
3. 作成が完了したら、種類が「ボットチャンネル登録」の項目ができているので選択![Bots.png](https://qiita-image-store.s3.amazonaws.com/0/281074/243a3f7d-8773-1959-2fdd-39fb5d924c9f.png)
4. 「Webチャットでテスト」で動作確認![webchat.png](https://qiita-image-store.s3.amazonaws.com/0/281074/75760f55-8c8b-f6a6-e3a4-5b9ee8c3899d.png)しかし、この画像のようにメッセージが送信できません...

これは当然の結果で、Bot Framework Connectorを使用してボットを接続するためには、Bot Framework認証が必要で、作成したボットにはその部分が入っていないためメッセージが送れません。なので、次は認証部分をボットに追加してみましょう。

#Botにセキュリティ認証部分を追加
前節で述べたように、Bot Framework Connectorを使用してボットを接続するためにはBot Framework認証が必要となります。これはマイクロソフトアプリケーションとして登録されていることが必要になるのですが、実はBot Channels Registrationを作成した際に既に登録されています。（「MicrosoftアプリIDとパスワード」の部分）

1. 「設定」のノードで、Microsoft App IDの「管理」を選択![bot_microsoftid.png](https://qiita-image-store.s3.amazonaws.com/0/281074/d1c6e8cf-a5c7-fae2-954c-0e085a4e5a5b.png)
2. ボットの登録情報が記載されているページから「アプリケーションID」と「パスワード」を確認し、どこかに控えておきましょう。![microsoftid.png](https://qiita-image-store.s3.amazonaws.com/0/281074/e8df3389-0964-07c4-2fd6-e9d6873e903c.png)＊既に作成されているパスワードは表示することができないので、「新しいパスワードを生成」から新たに作成したパスワードを使用してください。またその際に保存を忘れずに...
3. Startup.csのコードを下記に修正("アプリケーションID","パスワード"は控えたものを使用)

    ```C#
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;
    using Microsoft.Bot.Connector.Authentication;  //認証

    namespace StrCountBot
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddBot<StrCountBot>(options =>
                {
                    //APP認証
                    options.CredentialProvider = new SimpleCredentialProvider("アプリケーションID", "パスワード");
                });
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                app.UseBotFramework();
            }
        }
    }

    ```

4. Azureに再デプロイして、「Webチャットでテスト」で動作確認![webchat2.png](https://qiita-image-store.s3.amazonaws.com/0/281074/0188a415-5696-ceab-41af-c5a5ff0b71a7.png)無事メッセージが送れるようになりました！ボットもしっかりと返答しています。

これで、ローカルで作成したボットをAzureにデプロイして動作させるところまでの一連の流れができました。今後は、ローカルでコード修正・動作確認して、Azureにデプロイ！という循環を回して自分用にカスタマイズしていきたいところですが、Startup.csに認証部分を実装するだけの修正方法ではこれから開発する上で問題が生じます。
というのも、この実装だとクラウドでは動くのですが、ローカルでは動かなくなりますw (アプリケーション登録しているのはクラウドの方だけなので、ローカルで実行すると認証部分でむしろエラーになります...)
また、IDやPasswordをコードに直接書き込む行為はセキュリティ面からいってあり得ないと思われます。

なので、セキュリティに少し配慮しつつ、ローカル／クラウドのどちらでもボットが動くように修正していきます。


#Botをローカル／クラウドで動作できるように改修
Bot Framework V4ではボットファイル(.bot)がボットに関わる全ての設定を定義するようになっています。実際にボットをローカルでテストする際にBot Framework Emulatorで作成した.botを見てみると、

```C#
{
    "name": "StrCountBot",
    "description": "",
    "services": [
        {
            "type": "endpoint",
            "appId": "",
            "appPassword": "",
            "endpoint": "http://localhost:64553/api/messages",
            "id": "89534f60-f8a9-11e8-a165-e702897bc7f2",
            "name": "http://localhost:64553/api/messages"
        }
    ],
    "padlock": "",
    "version": "2.0",
    "overrides": null,
    "path": "C:\\Users\\<username>\\source\\repos\\StrCountBot\\StrCountBot\\StrCountBot.bot"
}
```
となっていて、servicesのキーに対する値として、ローカルでテストするときのエンドポイント情報が書き込まれています。現在のコードでは.botを参照せずに実行していますが、Bot Framework V4ではリソース管理をこのボットファイルで行うため、参照して実行するのが普通となります。
なので、.botにクラウドでのエンドポイント情報を追加し、コードに.botを参照すること、また実行環境がローカルかクラウドかを認識させて認証するかしないかの分岐をすることでローカルでもクラウドでも動作できるようになります。その作業をしていきましょう！

1. .botにクラウドのエンドポイント情報を追加
2. .botのローカル／クラウドのそれぞれのエンドポイントのnameのキーに対する値をdevelopment/productionにしておく

    ```C#
    {
        "name": "StrCountBot",
        "description": "",
        "services": [
            {
                "type": "endpoint",
                "appId": "",
                "appPassword": "",
                "endpoint": "http://localhost:64553/api/messages",
                "id": "89534f60-f8a9-11e8-a165-e702897bc7f2",
                "name": "development"
            },
            {
                "type": "endpoint",
                "appId": "アプリケーションID",
                "appPassword": "パスワード",
                "endpoint": "https://~.azurewebsites.net/api/messages",
                "id": "345",
                "name": "production"
            }
        ],
        "padlock": "",
        "version": "2.0",
        "overrides": null,
        "path": "C:\\Users\\<username>\\source\\repos\\StrCountBot\\StrCountBot\\StrCountBot.bot"
    }
    ```
＊"id"は適当な文字列で問題ないです。

3. Bot Framework Emulatorで、.botを開き、歯車のマークを選択![emu_set.png](https://qiita-image-store.s3.amazonaws.com/0/281074/76ca2665-fb09-e877-e001-e9f924b73dab.png)

4. 「Encrypt keys stored in your bot configuration」にチェックマークを付け、.botを暗号化（Secretキーは控えておきましょう!）![emu_secret.png](https://qiita-image-store.s3.amazonaws.com/0/281074/15979a20-38de-88d7-75b2-8a5253bdb6a8.png)＊.botは初めに述べたように、設定を持つファイルなので取り扱いに注意する必要があり、Bot Framework V4では .bot の中のセキュリティに関する値を暗号化する機能があるため、この作業をしています。

5. appsetting.jsonを下記に修正 ("Secretキー"は控えたものを使用)

    ```C#
    {
        "Logging": {
        "LogLevel": {
            "Default": "Warning"
            }
        },
        "botFilePath": "./StrCountBot.bot",
        "botFileSecret": "Secretキー",
        "AllowedHosts": "*"
    }
    ```

6. Startup.csを下記に修正

    ```C#
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;
    using Microsoft.Bot.Connector.Authentication;  //認証
    using Microsoft.Bot.Configuration;
    using System;
    using System.Linq;

    namespace StrCountBot
    {
        public class Startup
        {
            private bool _isProduction = false;

            public Startup(IHostingEnvironment env)
            {
                //現在の実行環境がProductionかチェック
                _isProduction = env.IsProduction();

                //AppSettingsと環境変数を取得
                var builder = new ConfigurationBuilder()
                    .SetBasePath(env.ContentRootPath)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddEnvironmentVariables();

                Configuration = builder.Build();
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                // .botのパスとシークレット取得
                var secretKey = Configuration.GetSection("botFileSecret")?.Value;
                var botFilePath = Configuration.GetSection("botFilePath")?.Value;

                // .botの読み込み
                var botConfig = BotConfiguration.Load(botFilePath ?? @".\BotConfiguration.bot", secretKey);
                services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot config file could not be loaded. ({botConfig})"));

                // 現在の実行環境におけるエンドポイントを取得
                var environment = _isProduction ? "production" : "development";
                var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == environment).FirstOrDefault();
                if (!(service is EndpointService endpointService))
                {
                    throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
                }

                services.AddBot<StrCountBot>(options =>
                {
                    //APP認証情報
                    options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
                });
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                app.UseBotFramework();
            }
        }
    }
    ```
＊Microsoft.Bot.Configurationはおそらくないはずなので、「Nugetパッケージの管理」からインストールする必要があります。
7. ローカルで動作することを確認![emu_test.png](https://qiita-image-store.s3.amazonaws.com/0/281074/c0644bd1-aadd-7065-b387-19e9f0f19a65.png)
8. Visual Studio 2017で.botのプロパティで出力ディレクトリにコピーを「常にコピーする」に変更してAzureにデプロイ
![vs_property.png](https://qiita-image-store.s3.amazonaws.com/0/281074/8061a1eb-b101-8ef0-c5f6-a5b67e4e5870.png)

9. Webチャットでテストして動作確認![webchat_test.png](https://qiita-image-store.s3.amazonaws.com/0/281074/f994b5d4-1353-b0ba-d7f9-69740d8c9edf.png)

これで、セキュリティに少し配慮しつつ、ローカル／クラウドのどちらでもボットが動くようになりました！


#最後に
ちょっと長くなりましたが、いかがでしたでしょうか？（手順通りやったのにできないとかあったらすみません...）
これで、ローカルでもクラウドでも動くようなベースとなるボットができたので、あとはBot Framework V4について理解を深めるなりして、LUISと連携させたり、ステート管理した対話をさせたりなどカスタマイズしてみてください。