C# で Clova スキルを開発する

本記事では [CEK C# SDK](https://github.com/kenakamu/clova-cek-sdk-csharp) で Clova スキルのサーバー部分を開発する方法を紹介します。

### この記事では紹介しないこと

- CEK とは何か
- 対話スキルについて

これらのことは #cek タグで調べれば、多くの分かりやすい記事があるため、説明は割愛します。

- Web API のホスティング詳細
- dotnet core Web API の詳細

開発したサーバー側コードは、パブリックにアクセスできる場所に配置します。普段は Azure を使っていますが、dotnet core が動作すればどこでもいいです。

# 前提条件

- C# と dotnet core の基本的な知識
- [Visual Studio Code](https://code.visualstudio.com/)
- [dotnet core](https://www.microsoft.com/net/learn/get-started-with-dotnet-tutorial) 

# 基礎となるプロジェクトの準備

1\. ターミナルより新しく dotnet のプロジェクトを作成。

```shell
dotnet new webapi -n FirstCEK
```

2\. SDK の読込み。

```shell
cd FirstCEK
dotnet add package CEK.CSharp
dotnet build
```

3\. VS Code でプロジェクトを開く。

```shell
code .
```

4\. Controllers 配下の ValuesController.cs を ClovaController.cs にリネームして、以下のコードに差し替え。参照した C# の SDK と ClovaClient を作成。

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using CEK.CSharp;
using CEK.CSharp.Models;

namespace FirstCEK.Controllers
{
    [Route("api/[controller]")]
    public class ClovaController : Controller
    {
        private ClovaClient client;
        public ClovaController()
        {
            client = new ClovaClient();
        }
     
        // POST api/clova
        [HttpPost]
        public async Task<IActionResult> Post()
        {
            return new OkObjectResult(true);
        }
    }
}
```

以上でまずはサーバー側の準備が整いました。

# クローバーからの通信を処理

## メッセージの確認とパース

まずクローバーから送られてきた通信に対して、以下の処理を行います。

- 署名の確認
- 受け取ったデータのパース

SDK を使うと以下のコードでこれらの処理が出来ます。

```csharp
CEKRequest request = await client.GetRequest(
    Request.Headers["SignatureCEK"], 
    Request.Body
);
```

## リクエストタイプごとのハンドリング

次にどのようなリクエストが送られてきたかを確認します。CEK の[リクエストタイプ](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtRequestType)は全部で 4 種類あります。

[LaunchRequest](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtLaunchRequest) : ユーザーが特定のExtensionの使用を開始したことを示す。
[IntentRequest](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtIntentRequest) : 解析されたユーザーのリクエストを渡す。
[SessionEndedRequest](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtSessionEndedRequest) : ユーザーの特定のスキルの使用が終了したことを示す。
[EventRequest](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtEventRequest) : クライアントの状態の変化や、それに伴うリクエストをExtensionに渡すために使用される。

※EventRequest はオーディオコンテンツ系の処理で使うようなので、今回は割愛します。

イベントのタイプはパースされた request オブジェクトの Request.Type プロパティで取得できます。

```csharp
switch (request.Request.Type)
{
    case RequestType.LaunchRequest:
        break;
    case RequestType.SessionEndedRequest:
        break;
    case RequestType.IntentRequest:
        switch (request.Request.Intent.Name)
        {
            case "Clova.YesIntent":
                break;
            case "Clova.NoIntent":
                break;
            case "Clova.GuideIntent":
                break;
        }
        break;
}
```

## セッション情報の取得

ユーザーとのマルチターン対話を行う際にセッションに格納した値は GetSessionAttribute メソッドで取得可能です。既定値を渡すことでデータがない場合の値を指定します。

```csharp
var mySessionValue = request.GetSessionAttribute(
    "mySessionKey", 
    "defaultValue"
);
```

# クローバーへの返信

受信内容を確認したら、次に返信を作成してクローバーに返します。

## CEKResponse オブジェクト

クローバーに対しての返信は CEKResponse オブジェクトを作成して OKObjectResult に渡します。

```csharp
CEKResponse response = new CEKResponse();
...
return new OkObjectResult(response);
```

## 音声テキストまたは音声ファイルの指定

クローバーに発話させる内容として、テキストまたは音声ファイルを指定できます。テキストの場合は発話する言語が指定可能です。言語指定を省略すると日本語になります。
また複数のテキストや URL を指定できます。

```csharp
response.AddText("スキルへようこそ", Lang.Ja);
response.AddUrl("https://clova-common.line-scdn.net/dice/rolling_dice_sound.mp3");
```

単純なテキストではなく、要約音声情報と詳細音声情報を渡したい場合は、要約は AddBriefText/AddBriefUrl、詳細は AddVerboseText/AddVerboseUrl メソッドを使います。

```csharp
response.AddBriefText("天気予報です。");
response.AddVerboseText("週末まで全国に梅雨…猛暑和らぐ。");
response.AddVerboseText("明日全国的に梅雨…ところによって局地的に激しい雨に注意。");                            
```

## タイムアウト時の追加メッセージ

ユーザーインプットがないまま一定時間が経過した場合は、AddRepromptText/AddRepromptUrl で追加のメッセージを発話できます。

```csharp
response.AddRepromptText("何か言ってください。");                            
```

## セッション情報の格納

ユーザーとのマルチターン対話を行う際にセッションに情報を格納する場合、AddSession メソッドを使います。

```csharp
response.AddSession("mySessionKey", "mySessionValue");
```

## マルチターン会話の指定

会話が引き続きユーザー入力を期待する場合、response.ShouldEndSession プロパティを true に指定します。

```csharp
response.ShouldEndSession = true;
```

# サイコロサンプルを実装してみる

[サンプルサイコロスキル](https://github.com/line/clova-extension-sample-dice) は Node.js で書かれた CEK サンプルですが、ここでは C# で書き直してみます。動くかどうかは、実際に試してください。

```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using CEK.CSharp;
using CEK.CSharp.Models;

namespace FirstCEK.Controllers
{
    [Route("api/[controller]")]
    public class ClovaController : Controller
    {
        private ClovaClient client;
        public ClovaController()
        {
            client = new ClovaClient();
        }

        // POST api/clova
        [HttpPost]
        public async Task<IActionResult> Post()
        {
            CEKRequest request = await client.GetRequest(
                Request.Headers["SignatureCEK"],
                Request.Body
            );

            CEKResponse response = new CEKResponse();
            switch (request.Request.Type)
            {
                case RequestType.LaunchRequest:
                    response.AddText("いくつのサイコロを投げますか?");
                    response.AddSession("intent", "ThrowDiceIntent");
                    response.ShouldEndSession = false;
                    break;
                case RequestType.SessionEndedRequest:
                    response.AddText("サイコロを終了します。");
                    break;
                case RequestType.IntentRequest:
                    switch (request.Request.Intent.Name)
                    {
                        case "ThrowDiceIntent":
                            var diceCount = request.Request.Intent.Slots.ContainsKey("diceCount") ?
                                int.Parse(request.Request.Intent.Slots["diceCount"].ToString()) : 1;
                            response.AddText($"サイコロを{diceCount}個投げます。");
                            response.AddUrl("https://clova-common.line-scdn.net/dice/rolling_dice_sound.mp3");
                            var throwResult = ThrowDice(diceCount);
                            response.AddText($"{diceCount}個のサイコロの合計は{throwResult}です。");
                            break;
                        case "Clova.YesIntent":
                            break;
                        case "Clova.NoIntent":
                            break;
                        case "Clova.GuideIntent":
                            response.AddText("サイコロを1個投げて、と言ってみてください。");
                            response.ShouldEndSession = false;
                            break;
                    }
                    break;
            }
            return new OkObjectResult(response);
        }

        private int ThrowDice(int diceCount)
        {
            var totalCount = 0;
            for (int i = 0; i < diceCount; i++)
            {
                totalCount += (new Random()).Next(1, 6);
            }

            return totalCount;
        }
    }
}
```

# まとめ

CEK のサーバー側開発は結構シンプルですが、内部ロジックをどうするかによって面白いスキルも開発できるはずですので、是非色々なアイデアを試してください。

# 参照
[CEK ドキュメント](https://clova-developers.line.me/guide/)
[CEK C# SDK](https://github.com/kenakamu/clova-cek-sdk-csharp)
[CEK サンプルサイコロスキル](https://github.com/line/clova-extension-sample-dice)