[LINE] For C#er (ASP.NET Core), Line Botの作成 - Part 2 - メッセージの返信

LINEからデータ（テキスト）を受取る方法は前回の記事、「[For C#er (ASP.NET Core), Line Botの作成 - Part 1](https://qiita.com/syantien/items/a331afc2592c3600c871)」をご参照ください。

ではでは、今日はLINEで受信したテキストに対して返信する仕組みを書きます。

# メッセージの返信

次はユーザーからきたメッセージに対して返信をします。シンプルにテキストを返すケースもあればよりリッチなコンテンツを返すケースもあります。まずはテキストの返信から。

ユーザーに返信したい場合は、下記のエンドポイントに対してPOSTリクエストを送信します。

`POST https://api.line.me/v2/bot/message/reply`

ヘッダーは下記のとおりです。

|リクエストヘッダー|説明|
|:---|:---|
|Content-Type|application/json|
|Authorization|Bearer {channel access token}|

ボディーは下記のとおりです。

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|replyToken|String|必須|Webhookで受信する応答トークン|
|messages|メッセージオブジェクトの配列|必須|送信するメッセージ、最大件数：5
|notificationDisabled|Boolean|任意|true or false、デフォルト値はfalseです。|

- notificationDisabled
 - true：メッセージ送信時に、ユーザーに通知されない。
 - false: メッセージ送信時に、ユーザーに通知される。ただし、LINEで通知をオフにしている場合は通知されません。

```csharp
    public class Message
    {
        public string id { get; set; }
        public string type { get; set; }
        public string text { get; set; }
    }

    public class LineTextReplyObject
    {
        public string replyToken { get; set; }
        public List<Message> messages { get; set; }
        public bool notificationDisabled { get; set; }
    }
```


##メッセージオブジェクトの種類

返信メッセージは下記のタイプがあります

- テキストメッセージ
- 画像メッセージ
- 動画メッセージ
- 音声メッセージ
- 位置情報メッセージ
- スタンプメッセージ
- イメージマップメッセージ
- テンプレートメッセージ
- Flex Message

そしてすべてのメッセージに共通してクイックリプライを付随することができます。 今日はテキストメッセージ、画像メッセージ、動画メッセージ、クイックリプライを使ってみます。

では早速テキストから、

## テキストメッセージで返信する
まずは、受け取ったメッセージをそのまま返すという、意味のない機能を実装してみます。

```csharp
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using LineApiApp.Models;
using System.Text;
using System.Security.Cryptography;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;

namespace LineApiApp
{
    public class LineReply
    {

        private const string _secret = "";
        private const string _token = "";
        private readonly HttpClient _httpClient;

        public LineReply()
        {
            _httpClient = new HttpClient();
        }

        [FunctionName("ReplyToMessage")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // Get headear value (signature)
            req.Headers.TryGetValue("X-Line-Signature", out var xlinesignature);

            // Get body
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            // Check for signature
            if (IsSingatureOk(xlinesignature, requestBody, _secret))
            {

                // Deserialize
                var data = JsonConvert.DeserializeObject<LineWebhookMessageObject>(requestBody);

                // Log
                log.LogInformation("Type is : " + data.events[0].type);

                // Let's make sure I am responding to text msg only 
                if (data.events[0].type == "message")
                {
                    // Log
                    log.LogInformation("M/D is : " + data.events[0].message.text + " / " + data.destination);

                    // Reply
                    await SendTextReplyAsync(data.events[0].replyToken, data.events[0].message.text);

                    // OK!
                    return new OkResult();
                }

                // Log
                log.LogInformation("Data type was not appropriate");

                // I am not yet able to handle your request
                return new BadRequestResult();
            }

            // Log
            log.LogInformation("Signature verification fail");

            // Uh.. NOT OK
            return new BadRequestResult();

        }

        /// <summary>
        /// Return if signature matches
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="text"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool IsSingatureOk(string signature, string text, string key)
        {

            var textBytes = Encoding.UTF8.GetBytes(text);
            var keyBytes = Encoding.UTF8.GetBytes(key);

            using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
            {
                var hash = hmac.ComputeHash(textBytes, 0, textBytes.Length);
                var hash64 = Convert.ToBase64String(hash);

                return signature == hash64;
            }
        }

        /// <summary>
        /// Send text back
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        private async Task SendTextReplyAsync(string replyToken, string message)
        {
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);

            var response = await _httpClient.PostAsJsonAsync<LineTextReplyObject>("https://api.line.me/v2/bot/message/reply", new LineTextReplyObject()
            {
                replyToken = replyToken,
                messages = new List<Message>()
                {
                    new Message(){
                        type = "text",
                        text = message
                    } 
                }
            });

            response.EnsureSuccessStatusCode();
        }
    }
}
```
### 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/4d4733b8-3fb0-9d4b-f2f1-d71e513b39cb.png)

次はクイックリプライに挑戦。


## クイックリプライで返信する

テキスト以外のフォーマットでも返信をしてみます。まずはクイックリプライ。クイックリプライの詳細に関してはこちらを参照。
https://developers.line.biz/ja/docs/messaging-api/using-quick-reply/


クイックリプライの定義はこちら

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|quickReply|Object|任意|itemsオブジェクト|

Itemsオブジェクト

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|items|Objectの配列|必須|クイックリプライボタンオブジェクト。最大オブジェクト数：13|

クイックリプライボタンオブジェクト

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|imageUrl|String|任意|ボタンの先頭に表示するアイコンのURL、最大文字数：1000、URLスキーム：https、画像フォーマット：PNG、アスペクト比：1：1、最大データサイズ：1MB、画像サイズに制限はありません。、actionプロパティに指定するアクションがカメラアクション、カメラロールアクション、または位置情報アクションで、imageUrlプロパティが未指定の場合、デフォルトのアイコンが表示されます。|
|action|Object|必須|タップされたときのアクション。アクションオブジェクトを指定します。|

クイックリプライではチャット画面下部分に小さなアイコン（ボタン）が表示されます、
ボタンをクリックしたときのアクションに、

- ポストバックアクション
- メッセージアクション
- 日時選択アクション
- カメラアクション
- カメラロールアクション
- 位置情報アクション

のいずれかを指定することができます。

全種類表示してみます。

```csharp
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using LineApiApp.Models;
using System.Text;
using System.Security.Cryptography;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;

namespace LineApiApp
{
    public class LineReply
    {

        private const string _secret = "";
        private const string _token = "";
        private readonly HttpClient _httpClient;

        public LineReply()
        {
            _httpClient = new HttpClient();
        }

        [FunctionName("ReplyToMessage")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // Get headear value (signature)
            req.Headers.TryGetValue("X-Line-Signature", out var xlinesignature);

            // Get body
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            // Check for signature
            if (IsSingatureOk(xlinesignature, requestBody, _secret))
            {

                // Deserialize
                var data = JsonConvert.DeserializeObject<LineWebhookMessageObject>(requestBody);

                // Log
                log.LogInformation("Type is : " + data.events[0].type);

                // Let's make sure I am responding to text msg only 
                if (data.events[0].type == "message")
                {

                    // Reply
                    await SendQuickReply(data.events[0].replyToken);

                    // OK!
                    return new OkResult();
                }

                // Log
                log.LogInformation("Data type was not appropriate");

                // I am not yet able to handle your request
                return new BadRequestResult();
            }

            // Log
            log.LogInformation("Signature verification fail");

            // Uh.. NOT OK
            return new BadRequestResult();

        }

        /// <summary>
        /// Return if signature matches
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="text"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool IsSingatureOk(string signature, string text, string key)
        {

            var textBytes = Encoding.UTF8.GetBytes(text);
            var keyBytes = Encoding.UTF8.GetBytes(key);

            using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
            {
                var hash = hmac.ComputeHash(textBytes, 0, textBytes.Length);
                var hash64 = Convert.ToBase64String(hash);

                return signature == hash64;
            }
        }

        /// <summary>
        /// Sebd quick reply
        /// </summary>
        /// <param name="replyToken"></param>
        /// <returns></returns>
        private async Task SendQuickReply(string replyToken)
        {
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);

            var response = await _httpClient.PostAsJsonAsync<LineTextReplyObject>("https://api.line.me/v2/bot/message/reply", new LineTextReplyObject()
            {
                replyToken = replyToken,
                messages = new List<Message>()
                {
                    new Message()
                    {
                        type="text",
                        text = "クイックリプライ",
                        quickReply = new QuickReplyItems(){
                            items = new List<QuickReplyItem>()
                            {
                                new QuickReplyItem(){
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "postback",
                                        label = "購入",
                                        data = "action=buy",
                                        displayText = "購入をタップ"
                                    }
                                },
                                new QuickReplyItem()
                                {
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "message",
                                        label= "メッセージ",
                                        text= "メッセージをタップ"
                                    }
                                },
                                new QuickReplyItem(){
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "datetimepicker",
                                        label= "日付を選択",
                                        data = "date=date",
                                        mode = "datetime",
                                        initial = "2019-09-09t00:00",
                                        max = "2020-02-01t23:59",
                                        min = "2019-09-09t00:00"
                                    }
                                },
                                new QuickReplyItem(){
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "camera",
                                        label = "カメラ"
                                    }
                                },
                                new QuickReplyItem(){
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "cameraRoll",
                                        label = "カメラロール"
                                    }
                                },
                                new QuickReplyItem(){
                                    type = "action",
                                    action = new QuickReplyAction()
                                    {
                                        type = "location",
                                        label = "ロケーション"
                                    }
                                }
                            }
                        }
                    }
                }
            });

            response.EnsureSuccessStatusCode();
        }
    }
}
```

###結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/a7bd6f5f-1cbf-563d-4bf2-1f0e2c614e92.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/103ed997-033b-0c15-1c70-6a6ea5943b63.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/fca90e05-0a0f-3b88-0ea9-a44efa5a134b.png)

## 画像を返信する

画像のオブジェクトは下記

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|type|String|必須|image|
|originalContentUrl|String|必須|画像のURL（最大文字数：1000）、HTTPS、JPEG、最大画像サイズ：4096×4096、最大ファイルサイズ：1MB|
|previewImageUrl|String|必須|プレビュー画像のURL（最大文字数：1000）、HTTPS、JPEG、最大画像サイズ：240×240、最大ファイルサイズ：1MB|

オリジナル画像とプレビュー画像の二つを準備します。

```csharp

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using LineApiApp.Models;
using System.Text;
using System.Security.Cryptography;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;

namespace LineApiApp
{
    public class LineReply
    {

        private const string _secret = "";
        private const string _token = "";
        private readonly HttpClient _httpClient;

        public LineReply()
        {
            _httpClient = new HttpClient();
        }

        [FunctionName("ReplyToMessage")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // Get headear value (signature)
            req.Headers.TryGetValue("X-Line-Signature", out var xlinesignature);

            // Get body
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            // Check for signature
            if (IsSingatureOk(xlinesignature, requestBody, _secret))
            {

                // Deserialize
                var data = JsonConvert.DeserializeObject<LineWebhookMessageObject>(requestBody);

                // Log
                log.LogInformation("Type is : " + data.events[0].type);

                // Let's make sure I am responding to text msg only 
                if (data.events[0].type == "message")
                {

                    // Reply
                    await SendImageReply(data.events[0].replyToken);

                    // OK!
                    return new OkResult();
                }

                // Log
                log.LogInformation("Data type was not appropriate");

                // I am not yet able to handle your request
                return new BadRequestResult();
            }

            // Log
            log.LogInformation("Signature verification fail");

            // Uh.. NOT OK
            return new BadRequestResult();

        }

        /// <summary>
        /// Return if signature matches
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="text"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool IsSingatureOk(string signature, string text, string key)
        {

            var textBytes = Encoding.UTF8.GetBytes(text);
            var keyBytes = Encoding.UTF8.GetBytes(key);

            using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
            {
                var hash = hmac.ComputeHash(textBytes, 0, textBytes.Length);
                var hash64 = Convert.ToBase64String(hash);

                return signature == hash64;
            }
        }

        /// <summary>
        /// Send image reply
        /// </summary>
        /// <param name="replyToken"></param>
        /// <returns></returns>
        private async Task SendImageReply(string replyToken)
        {
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);

            var response = await _httpClient.PostAsJsonAsync<LineTextReplyObject>("https://api.line.me/v2/bot/message/reply", new LineTextReplyObject()
            {
                replyToken = replyToken,
                messages = new List<Message>()
                {
                    new Message()
                    {
                        type="image",
                        originalContentUrl = "https://sqlvaidccykhi26mlc.blob.core.windows.net/misc-sho-junkbox/handstand-scropion-big.jpg",
                        previewImageUrl = "https://sqlvaidccykhi26mlc.blob.core.windows.net/misc-sho-junkbox/handstand-scropion-small.jpg"
                    }
                }
            });

            response.EnsureSuccessStatusCode();
        }

    }


}
```
###結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/89ef9075-b5dd-92c3-f903-b8929ead161b.png)

上で表示されているのがサムネールで、下がオリジナル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/4f2cda3f-c3a1-422d-98b2-4e440a27cff1.png)

## 動画で返信

動画のオブジェクトは下記、画像と一緒ですね。

|プロパティ|タイプ|必須|説明|
|:---|:---|:---|:---|
|type|String|必須|video|
|originalContentUrl|String|必須|動画ファイルのURL（最大文字数：1000）、HTTPS、mp4、最大長：1分、最大ファイルサイズ：10MB、一定以上に縦長・横長の動画を送信した場合、一部の環境では動画の一部が欠けて表示される場合があります。|
|previewImageUrl|String|必須|プレビュー画像のURL（最大文字数：1000）、HTTPS、JPEG、最大画像サイズ：240×240、最大ファイルサイズ：1MB|

240 x 240 のjpgイメージと10MB以下のmp4ファイルを用意してサーバーにあげておきます。

```csharp

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using LineApiApp.Models;
using System.Text;
using System.Security.Cryptography;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;

namespace LineApiApp
{
    public class LineReply
    {

        private const string _secret = "";
        private const string _token = "";
        private readonly HttpClient _httpClient;

        public LineReply()
        {
            _httpClient = new HttpClient();
        }

        [FunctionName("ReplyToMessage")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // Get headear value (signature)
            req.Headers.TryGetValue("X-Line-Signature", out var xlinesignature);

            // Get body
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            // Check for signature
            if (IsSingatureOk(xlinesignature, requestBody, _secret))
            {

                // Deserialize
                var data = JsonConvert.DeserializeObject<LineWebhookMessageObject>(requestBody);

                // Log
                log.LogInformation("Type is : " + data.events[0].type);

                // Let's make sure I am responding to text msg only 
                if (data.events[0].type == "message")
                {

                    // Reply
                    await SendVideoReply(data.events[0].replyToken);

                    // OK!
                    return new OkResult();
                }

                // Log
                log.LogInformation("Data type was not appropriate");

                // I am not yet able to handle your request
                return new BadRequestResult();
            }

            // Log
            log.LogInformation("Signature verification fail");

            // Uh.. NOT OK
            return new BadRequestResult();

        }

        /// <summary>
        /// Return if signature matches
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="text"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private bool IsSingatureOk(string signature, string text, string key)
        {

            var textBytes = Encoding.UTF8.GetBytes(text);
            var keyBytes = Encoding.UTF8.GetBytes(key);

            using (HMACSHA256 hmac = new HMACSHA256(keyBytes))
            {
                var hash = hmac.ComputeHash(textBytes, 0, textBytes.Length);
                var hash64 = Convert.ToBase64String(hash);

                return signature == hash64;
            }
        }

        /// <summary>
        /// Send video reply
        /// </summary>
        /// <param name="replyToken"></param>
        /// <returns></returns>
        private async Task SendVideoReply(string replyToken)
        {
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);

            var response = await _httpClient.PostAsJsonAsync<LineTextReplyObject>("https://api.line.me/v2/bot/message/reply", new LineTextReplyObject()
            {
                replyToken = replyToken,
                messages = new List<Message>()
                {
                    new Message()
                    {
                        type="video",
                        originalContentUrl = "https://sqlvaidccykhi26mlc.blob.core.windows.net/misc-sho-junkbox/coverr-kiev-architecture-1566805889945.mp4",
                        previewImageUrl = "https://sqlvaidccykhi26mlc.blob.core.windows.net/misc-sho-junkbox/coast-preview.jpg"
                    }
                }
            });

            response.EnsureSuccessStatusCode();
        }
    }
}
```

返信に関する参考ドキュメント
https://developers.line.biz/ja/reference/messaging-api/#send-reply-message

###結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/390f25b7-422d-1f15-674b-e0f085964896.png)

静止画だと表現できませんが、下が動画です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/5908a845-d026-7f94-6917-be6f3de22a8c.png)

#次

引き続き違う種類の返信タイプを試してみます。