LINE BOOT AWARDS 公式夏期講習  ～2018 年 7 月時点での LINE Bot の機能を振り返る～

※本記事は「LINE BOOT AWARDS 公式夏期講習 ～Messaging API アップデートハンズオン～ 」用の記事です。

LINE は Chatbot プラットフォームを発表後、実に多くの機能を追加実装してきました。また LINE BOOT AWARD (何故か O が 2 個？？) が開催されるということなので、この記事ではこれまでに追加された機能のおさらいと、連携について node.js を使って見ていきます。

正式な記事は [LINE Developer ドキュメント](https://developers.line.me/ja/docs/) を参照してください。

# Messaging API

Messaging API は LINE Bot を作成するための API であり、これなしに話は始まりません。まさに基本中の基本であり多くのチュートリアルがあります。この記事では、Messaging API についての紹介は省略しますが、残りの機能については以下記事で作成したボットを使います。

[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

上記を参照して開発すると、実際に動作するオウム返しボットが出来ます。

# リッチメニュー

リッチメニューは個人的に LINE Bot で最も重要で、ユーザーエクスペリエンスを大きく左右する機能です。
ボット起動時、または任意のタイミングでボットの画面にメニューを出すことができるため、ユーザーがボットの使い方をすぐに理解できます。

<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/a89ee2cd-a227-cce9-973e-fb8cef477e87.png">

## LINE@ による既定のリッチメニュー

LINE@ は LINE Bot の一部機能を管理できるツールであり、そこから既定のリッチメニューを GUI で作成することが出来ます。

1\. [LINE@](https://admin-official.line.me/) にアクセスしてログイン。
2\. リッチメニューを作りたいボットを、一覧より選択。
3\. リッチコンテンツ作成からリッチメニューをクリック。
<img width=600 src="https://qiita-image-store.s3.amazonaws.com/0/214116/7d3fa6ac-0825-4213-9535-096e7052fb96.png">
4\. 新規作成ボタンをクリックし、タイトルなど必要な情報を入力。
5\. メニューは用意した画像で作成することも、既定のアイコンを利用することも可能。以下は 4 アイコンタイプを選択した場合。
<img width=800 src="https://qiita-image-store.s3.amazonaws.com/0/214116/3b5b4e2d-1e64-8f43-aab6-f9ae5a99494d.png">
6\. 各アイコンではクリックした場合の挙動を指定可能。ここではクリックした場合に「新商品情報」をというテキストを送信。
<img width=600 src="https://qiita-image-store.s3.amazonaws.com/0/214116/e7f861a0-9d1d-c4df-20c2-56ec84e18504.png">
7\. 残りのアイコンも設定して保存。
8\. 表示する設定で期間内の場合、即座に反映される。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/9a39bf00-2bf4-c44f-832d-ef2ee5663dce.png">

## API 経由でのリッチメニュー

LINE@ では既定に 1 メニューしか登録できませんが、API を使うことで最大 10 個までリッチメニューが登録できる他、ユーザーのステータスや時期など動的にメニューを変えることも出来ます。
正式なツールではありませんが、コミュニティとして GUI で登録できるツールもあります。
[リッチメニューで LINE ボットの UX を向上](https://qiita.com/kenakamu/items/38cadd65ac87282d0d5f)

## リッチメニューの活用方法

リッチメニューでは Bot に送るテキストだけでなく、URL やポストバックを利用できます。それぞれ以下のような活用方法があります。
- テキスト: 画面にテキストが出るため、ユーザーとして Bot に何が送られたかすぐに理解できる。
- URL: 外部のアドレスだけでなく、後述する LINE URL スキームや LIFF のアドレスを指定できる。
- ポストバック: 画面にテキストを出すことなく、Bot にデータを送れるため、メニューの切り替えなど行う際に便利。

# LINE URL スキーム

写真を送ってほしい、位置情報を送ってほしいなど、ユーザーに特定のアクションを取ってもらいたい場合、便利な機能です。
非常に多くの機能があるため、詳細は [LINE URLスキームを使う](https://developers.line.me/ja/docs/messaging-api/using-line-url-scheme/) を参照してください。ここでは写真と位置情報を送ってもらうシナリオで実装します。

## カメラを起動する。

カメラの起動は line://nv/camera/ という URL を使います。まずは先ほど紹介したリッチメニューを使ってみます。

1\. [LINE@](https://admin-official.line.me/) にアクセスしてログイン。
2\. 一覧より先ほどのボットを選択。
3\. リッチコンテンツ作成からリッチメニューをクリック。
4\. 一覧よりさきほど作成したリッチメニューを選択。
5\. 任意のメニューをカメラアイコンに変更し、URL を選択。アドレスに line://nv/camera/ を設定。
<img width=600 src="https://qiita-image-store.s3.amazonaws.com/0/214116/058b1d0b-430c-0969-a21f-5263dd23af44.png">
6\. 保存後、LINE クライアントより新しく登録したメニューをクリック。カメラが起動することを確認。
<img width=600 src="https://qiita-image-store.s3.amazonaws.com/0/214116/b51dd723-9ac3-ac26-6f83-a8e3c02ea6f8.png">
7\. 写真を撮ってそのまま Bot に送信できることを確認。

## 位置情報を要求する

位置情報の取得は line://nv/location で行えます。ここでは Bot にコードを実装してみましょう。

1\. server.js の handleEvent 関数を以下のコードに書き換え。

```javascript
function handleEvent(event) {
    if (event.type !== 'message') {
        return Promise.resolve(null);
    }

    if (event.message.type === 'image') {
        return client.replyMessage(event.replyToken,
            {
                type: 'template',
                altText: '位置情報を送ってください。',
                template: {
                    type: 'buttons',
                    title: '位置情報',
                    text: '位置情報を送ってください。',
                    actions: [
                        { label: '位置情報を送る', type: 'uri', uri: 'line://nv/location' },
                    ]
                }
            }
        )
    }

    else {
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: event.message.text //実際に返信の言葉を入れる箇所
        });
    }
}
```

2\. サーバーを起動後、LINE クライアントより写真を Bot に送信。位置情報を聞くボタンテンプレートが返ってくる。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/4084a1ce-7bec-c20c-ad9b-0f25cb639515.png">
3\. 位置情報を送るリンクをクリック。地図が起動することを確認。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/197ae888-bb36-4302-26f1-f6b794edd787.png">

# Flex Message

LINE Bot はもともとテンプレートと呼ばれるリッチなメッセージフォーマットをサポートしていますが、Flex Message を使うとより柔軟なメッセージを送ることが出来ます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3f0de425-b277-c6a2-d7d3-afedaaa12659.png)

Flex Message は柔軟にメッセージを作成できるため構成要素が多くなっています。各要素などの詳細は [Flex Messageを使う](https://developers.line.me/ja/docs/messaging-api/using-flex-messages/) を参照してください。

## Flex Message シミュレーター

Flex Message が実際どのように画面に表示されるか確認できるツールです。

1\. [Flex Message シミュレーター](https://developers.line.me/console/fx/) にアクセス。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/12f5b06f-96c4-840c-23a4-b10ab8e788f5.png)
2\. ここでは Brown's Burger を選択し「作成する」をクリック。
3\. 実際にどのように見えるかと、対応する JSON/YAML が表示される。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/b78d96e4-4199-fc0a-d42e-40039a92d1de.png)
4\. 少しシンプルにするために、body -> contents の 2 つ目の要素の contents より、コンテンツを 1 つ削除。(34 行目から 57行目)。
5\. 値段を $15.5 から 1500円に変更。
6\. 一番下の「Add to Cart」を「購入」に変更。
7\.「プレビュー」ボタンをクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9a5cbb10-3bdd-53cb-8ba7-d241e5b7feec.png)
8\.「クリップボードにコピー」をクリックするとコピーが可能。

## Bot への反映

では次にこの Flex Message を Bot から返せるようにします。

1\. server.js の handleEvent 関数を以下のコードに差し替え。flex の中身は上記シミュレーターで作成した JSON。

```javascript
function handleEvent(event) {
    if (event.type !== 'message') {
        return Promise.resolve(null);
    }

    if (event.message.type === 'image') {
        return client.replyMessage(event.replyToken,
            {
                type: 'template',
                altText: '位置情報を送ってください。',
                template: {
                    type: 'buttons',
                    title: '位置情報',
                    text: '位置情報を送ってください。',
                    actions: [
                        { label: '位置情報を送る', type: 'uri', uri: 'line://nv/location' },
                    ]
                }
            }
        )
    }
    else if (event.message.type === 'text' && event.message.text === 'おすすめバーガー') {
        return client.replyMessage(event.replyToken, {
            type: 'flex',
            altText: 'おすすめバーガー',
            contents:
            {
                "type": "bubble",
                "hero": {
                    "type": "image",
                    "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_2_restaurant.png",
                    "size": "full",
                    "aspectRatio": "20:13",
                    "aspectMode": "cover",
                    "action": {
                        "type": "uri",
                        "uri": "https://linecorp.com"
                    }
                },
                "body": {
                    "type": "box",
                    "layout": "vertical",
                    "spacing": "md",
                    "action": {
                        "type": "uri",
                        "uri": "https://linecorp.com"
                    },
                    "contents": [
                        {
                            "type": "text",
                            "text": "Brown's Burger",
                            "size": "xl",
                            "weight": "bold"
                        },
                        {
                            "type": "box",
                            "layout": "vertical",
                            "spacing": "sm",
                            "contents": [
                                {
                                    "type": "box",
                                    "layout": "baseline",
                                    "contents": [
                                        {
                                            "type": "icon",
                                            "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/restaurant_large_32.png"
                                        },
                                        {
                                            "type": "text",
                                            "text": "1500円",
                                            "weight": "bold",
                                            "margin": "sm",
                                            "flex": 0
                                        },
                                        {
                                            "type": "text",
                                            "text": "550kcl",
                                            "size": "sm",
                                            "align": "end",
                                            "color": "#aaaaaa"
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            "type": "text",
                            "text": "Sauce, Onions, Pickles, Lettuce & Cheese",
                            "wrap": true,
                            "color": "#aaaaaa",
                            "size": "xxs"
                        }
                    ]
                },
                "footer": {
                    "type": "box",
                    "layout": "vertical",
                    "contents": [
                        {
                            "type": "spacer",
                            "size": "xxl"
                        },
                        {
                            "type": "button",
                            "style": "primary",
                            "color": "#905c44",
                            "action": {
                                "type": "uri",
                                "label": "購入",
                                "uri": "https://linecorp.com"
                            }
                        }
                    ]
                }
            }
        });
    }
    else {
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: event.message.text //実際に返信の言葉を入れる箇所
        });
    }
}
```
2\. 保存後、サーバーを起動。
3\. LINE クライアントより「おすすめバーガー」をメッセージを送信。Flex Message が返ってくることを確認。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/d1331d26-c2c4-8749-b314-a446ab81e1ac.png"/>

# LINE Pay

支払いや割り勘など、お金に関する便利機能がある LINE Pay ですが、Bot と連携して支払いをすることも出来ます。詳細な手順は [LINE Pay APIを使ってアプリに決済を組み込む方法](https://qiita.com/nkjm/items/b4f70b4daaf343a2bedc) に紹介がありますが、ここではバーガーが買えるようにしてみます。

## LINE Pay と Bot の関係

LINE Pay は Bot 専用という事ではなく、通常の Web アプリやモバイルアプリと一緒に使えるサービスです。

## ドキュメント

LINE Pay のドキュメントは Web 上に無いようで、PDF として [LINE Pay技術連動ガイド](
https://pay.line.me/jp/developers/documentation/download/tech?locale=ja_JP) からダウンロードできます。

## Bot へ組み込む

今回は Bot へ直接組み込んで使ってみます。

1\. [LINE Pay APIを使ってアプリに決済を組み込む方法](https://qiita.com/nkjm/items/b4f70b4daaf343a2bedc) にある [LINE Pay Sandboxの申請と設定](https://qiita.com/nkjm/items/b4f70b4daaf343a2bedc#line-pay-sandbox%E3%81%AE%E7%94%B3%E8%AB%8B%E3%81%A8%E8%A8%AD%E5%AE%9A) の手順を行い、まず LINE Pay のサンドボックス環境を作成。普段使っている LINE 開発者用アカウントとは別に、LINE Pay 専用のアカウントが払い出されるので混同しないように注意。
2\. Bot アプリのディレクトリより以下コマンドを実行して LINE Pay SDK 他、必要なモジュールを追加。

```shell
npm install -s dotenv line-pay memory-cache uuid 
```
3\. Server.js の最後に以下コードを追加。channelId、channelSecret、confirmUrl を更新。

```javascript
// ユーザーが支払いをする際のリダイレクト URL
app.get("/pay/confirm", (req, res, next) => {
    if (!req.query.transactionId){
        console.log("Transaction Id が見つかりません。");
        return res.status(400).send("Transaction Id が見つかりません。");
    }

    // キャッシュより reservation 情報を取得
    let reservation = cache.get(req.query.transactionId);
    if (!reservation){
        console.log("Reservation が見つかりません。");
        return res.status(400).send("Reservation が見つかりません。")
    }

    console.log(`以下の reservation を取得`);
    console.log(reservation);

    let confirmation = {
        transactionId: req.query.transactionId,
        amount: reservation.amount,
        currency: reservation.currency
    }

    console.log(`以下 options で確認ページに遷移`);
    console.log(confirmation);

    // 支払いを取得
    return pay.confirm(confirmation).then((response) => {
        res.sendStatus(200);

        // ユーザーに支払い完了の旨を通知。
        let messages = [{
            type: "sticker",
            packageId: 2,
            stickerId: 144
        },{
            type: "text",
            text: "ありがとうございます、決済が完了しました。"
        }]
        return client.pushMessage(reservation.userId, messages);
    });
});

require("dotenv").config();

const uuid = require("uuid/v4");
const cache = require("memory-cache");
const line_pay = require("line-pay");

// LINE Pay API SDK のインスタンス作成
const pay = new line_pay({
    channelId: "<LINE Pay ID>",
    channelSecret: "<LINE Pay チャネル シークレット>",
    hostname: "sandbox-api-pay.line.me",
    isSandbox: true
})
```

4\. handleEvent を以下コードに差し替え。confirmUrl を適宜変更。

```javascript
function handleEvent(event) {
    if (event.type !== 'message') {
        return Promise.resolve(null);
    }

    if (event.message.type === 'image') {
        return client.replyMessage(event.replyToken,
            {
                type: 'template',
                altText: '位置情報を送ってください。',
                template: {
                    type: 'buttons',
                    title: '位置情報',
                    text: '位置情報を送ってください。',
                    actions: [
                        { label: '位置情報を送る', type: 'uri', uri: 'line://nv/location' },
                    ]
                }
            }
        )
    }
    else if (event.message.type === 'text' && event.message.text === 'おすすめバーガー') {
        let options = {
            productName: "Brown's Burger L サイズ",
            amount: 1500,
            currency: "JPY",
            orderId: uuid(),
            confirmUrl: "https://<ngrok host>/pay/confirm"
        }
    
        pay.reserve(options).then((response) => {
            let reservation = options;
            reservation.transactionId = response.info.transactionId;
            reservation.userId = event.source.userId;
            
            console.log(`Reservation を以下の内容で作成`);
            console.log(reservation);
    
            // Save order information
            cache.put(reservation.transactionId, reservation);
          
        return client.replyMessage(event.replyToken, {
            type: 'flex',
            altText: 'おすすめバーガー',
            contents:
            {
                "type": "bubble",
                "hero": {
                    "type": "image",
                    "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_2_restaurant.png",
                    "size": "full",
                    "aspectRatio": "20:13",
                    "aspectMode": "cover",
                    "action": {
                        "type": "uri",
                        "uri": "https://linecorp.com"
                    }
                },
                "body": {
                    "type": "box",
                    "layout": "vertical",
                    "spacing": "md",
                    "action": {
                        "type": "uri",
                        "uri": "https://linecorp.com"
                    },
                    "contents": [
                        {
                            "type": "text",
                            "text": "Brown's Burger",
                            "size": "xl",
                            "weight": "bold"
                        },
                        {
                            "type": "box",
                            "layout": "vertical",
                            "spacing": "sm",
                            "contents": [
                                {
                                    "type": "box",
                                    "layout": "baseline",
                                    "contents": [
                                        {
                                            "type": "icon",
                                            "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/restaurant_large_32.png"
                                        },
                                        {
                                            "type": "text",
                                            "text": "1500円",
                                            "weight": "bold",
                                            "margin": "sm",
                                            "flex": 0
                                        },
                                        {
                                            "type": "text",
                                            "text": "550kcl",
                                            "size": "sm",
                                            "align": "end",
                                            "color": "#aaaaaa"
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            "type": "text",
                            "text": "Sauce, Onions, Pickles, Lettuce & Cheese",
                            "wrap": true,
                            "color": "#aaaaaa",
                            "size": "xxs"
                        }
                    ]
                },
                "footer": {
                    "type": "box",
                    "layout": "vertical",
                    "contents": [
                        {
                            "type": "spacer",
                            "size": "xxl"
                        },
                        {
                            "type": "button",
                            "style": "primary",
                            "color": "#905c44",
                            "action": {
                                "type": "uri",
                                "label": "購入",
                                "uri": response.info.paymentUrl.web
                            }
                        }
                    ]
                }
            }
        });
    })
    }
    else {
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: event.message.text //実際に返信の言葉を入れる箇所
        });
    }
}
```
5\. 全てを保存して LINE クライアントより「おすすめバーガー」と送信。返ってきた Flex Message の購入をクリックしたら LINE Pay に遷移することを確認。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/580bb796-7bad-43d6-a0ff-b2b452687242.png"/>

6\. 「PAY NOW」をクリック。支払いが完了したことを確認。支払いが完了すると Bot 側でメッセージが表示される。
<img width=300 src="https://qiita-image-store.s3.amazonaws.com/0/214116/cecf6417-d5aa-a04f-ae3a-163b23f44a4e.png"/>

リッチメニューから「おすすめバーガー」を送信させると、よりスムーズな流れとなります。

# LIFF

これまでも LINE クライアントから Web ページを開くことが出来ましたが、LIFF はその体験を数段階引き上げるものです。
画面のサイズが選択できるほか、Web アプリ上で現在のユーザー情報を取れるなど、連携が強化されています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/72c4faae-fa66-c805-8359-6eee3c75db1d.png)

詳細は[LINE の LIFF 開発超入門 : LIFF SDK を使った Web アプリの開発](https://qiita.com/kenakamu/items/44cba247ab51b6c5ed51)を参照してください。

## LIFF の使いどころ

Chatbot でフォーム的な情報を入力することは、ユーザーにとっても容易ではありません。その場合はやはり Web 画面で入力したほうがスムーズに処理が出来ます。この場合 LIFF を使うと以下のメリットがあります。

- Bot のコンテキストにいることをアピールしつつ情報を入力してもらえる
- 結果を Bot に返せるため、Web 用のサーバー処理を別途用意しなくていい
- ログインユーザーの情報を自動で取得できる

## LIFF と他機能の連携

LIFF はただの URL で起動できるため、以下のシナリオがあります。

- リッチメニューから直接起動
- テンプレートや Flex Message のボタンから起動

# ビーコン

LINE Bot で忘れてはいけないものとして LINE Beacon があります。これまで LINE Beacon を入手するのは多少面倒でしたが、現在は micro:bit 用のビーコンパッケージが公開されているため、非常に簡単にビーコンを試すことが出来ます。[linebeacon](https://makecode.microbit.org/pkg/pizayanz/pxt-linebeacon)
詳細は[ビーコンを使う](https://developers.line.me/ja/docs/messaging-api/using-beacons/) を参照してください。

詳細は後日ブログを書きます。

# まとめ

上記で紹介したように、LINE Bot はただ会話をするだけのプラットフォームから大きく進化し、より高度な対応ができるボットが開発可能です。LINE BOOT AWARD に向け何か作ってみてはいかがでしょうか。