InCaaaan:印鑑マシーンの作り方 部下ツール編

#InCaaaanとは？
[InCaaaan:印鑑マシーンの作り方 超概要編](https://qiita.com/oic0310/items/fb0aca7d355df9ddafb3)に、全体的な構成と動画を置いてますので、ご確認ください。
[InCaaaan:印鑑マシーンの作り方 上司ツール編](https://qiita.com/oic0310/items/a9b3f2178a11f0774bd8)に上司が使うツールと、バックエンドシステムの大半を書いてますので、ご確認ください

では、早速作り方を！！！！と書きつつ、上司編でほとんど書いてしまっているので、実は部下ツールのネタが少ないことに不安しかない。。。

#LINE Front-end Framework(以降LIFF)の準備
ここでは、Messaging APIのすべてを説明せず、LIFFで表示するWebのURLを設定するまでの解説です。botとのやりとりは、As You Likeです。

##プロバイダーを作る

プロバイダーとは、アプリを提供する組織のことです ([LINE Developers公式サイトより](https://developers.line.biz/ja/docs/messaging-api/getting-started/))

そして、LINE Developersの [公式サイト](https://developers.line.biz/ja/docs/messaging-api/getting-started/)を見てもらえれば、~~ここで書いてあるよりも詳しいので~~実のところの解説は不要に・・・でもとりあえず書いておく

以下の図で青丸で囲んでいる箇所の「*作成*」をクリックすると、

<img width="1270" alt="スクリーンショット 2020-01-04 22.50.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/13945f2d-1b03-f1ab-bc49-5ebe1c542aa9.png">

プロバイダ名を入力する箇所が出てくる為、好きな名前で作成してください

<img width="1277" alt="スクリーンショット 2020-01-04 23.00.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/8287fb29-141c-7eb4-b224-51c73c0c1997.png">

##チャンネルの作成
チャネルは、LINEプラットフォームが提供する機能を、プロバイダーが開発するサービスで利用するための通信路です。LINEプラットフォームを利用するには、チャネルを作成し、サービスをチャネルに関連付けます。チャネルを作成するには、名前、説明文、およびアイコン画像が必要です。チャネルを作成すると、固有のチャネルIDが識別用に発行されます。
([LINE Developers公式サイトより](https://developers.line.biz/ja/docs/messaging-api/getting-started/))

作成したプロバイダーを開くと「新規チャンネル作成」というボタンがあるので、押します。

<img width="601" alt="スクリーンショット 2020-01-04 23.01.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/fe6d8dfa-a891-015e-9acb-07e7442cde7d.png">

「Messaging API」を選択します。

<img width="743" alt="スクリーンショット 2020-01-04 23.01.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/2ea45ac9-e9a9-e55e-5869-853594452d27.png">

必要事項を入力して、「作成」ボタンを押します。

<img width="1279" alt="スクリーンショット 2020-01-04 23.08.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/8815cf7a-e082-4c1b-42d9-898b0e038ea8.png">

チャンネルを開いて、「LIFF」のタブをクリックします。

<img width="1280" alt="スクリーンショット 2020-01-04 23.16.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/a727722c-4ded-e5e6-0257-400c417feb4a.png">

LIFFアプリの「追加」ボタンをクリックします。

<img width="972" alt="スクリーンショット 2020-01-04 23.19.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/7c9c4d21-3bb7-ae23-5699-5bca323763f7.png">

以下の図の青色の四角で囲んでいる部分の「エンドポイントURL」に、LINE上に表示したいWebサイトのURLを入力します。
サイズは、「Full」にすればLINEの画面を覆いかぶせるような表示をおこなうことができます。

<img width="981" alt="スクリーンショット 2020-01-04 23.22.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/311d63d8-8ad3-c966-e822-02e520dd4d47.png">

##LIFFに表示するコンテンツの準備

[InCaaaan:印鑑マシーンの作り方 上司ツール編](https://qiita.com/oic0310/items/a9b3f2178a11f0774bd8)でも少し触れていますが、こんな画面を作りました。

![PNGイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/744aa834-9696-ce24-fe53-a70500c04dde.png)

LIFFの「エンドポイントURL」に設定しているURLは、IBM Cloud上のNode-REDで処理しています。

LIFFでHTTP GETされたら、HTMLコンテンツをレスポンスするという、シンプルな構造です。

<img width="588" alt="スクリーンショット 2020-01-04 23.31.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/4f35df5e-1171-1fc8-737d-d318da466e48.png">

Node-REDの「template」ノードには、以下のソースを記述している。
ソースきったねぇw

```html:LIFFに表示しているコンテンツのソース

<html>
    <head>
    <meta name="viewport" content="width=device-width,
            initial-scale=1.0,user-scalable=yes" />
	<style>
        /* Fonts */
        @import url(https://fonts.googleapis.com/css?family=Open+Sans:400);

        /* fontawesome */
        @import url(http://weloveiconfonts.com/api/?family=fontawesome);
        [class*="fontawesome-"]:before {
        font-family: 'FontAwesome', sans-serif;
        }

        /* Simple Reset */
        * { margin: 0; padding: 0; box-sizing: border-box; }

        /* body */
        body {
        background: #e9e9e9;
        color: #5e5e5e;
        font: 400 87.5%/1.5em 'Open Sans', sans-serif;
        }

        /* Form Layout */
        .form-wrapper {
        background: #fafafa;
        margin: 3em auto;
        padding: 0 1em;
        max-width: 370px;
        }

        h1 {
        text-align: center;
        padding: 1em 0;
        }

        form {
        padding: 0 1.5em;
        }

        .form-item {
        margin-bottom: 0.75em;
        width: 100%;
        }

        .form-item input {
        background: #fafafa;
        border: none;
        border-bottom: 2px solid #e9e9e9;
        color: #666;
        font-family: 'Open Sans', sans-serif;
        font-size: 1em;
        height: 50px;
        transition: border-color 0.3s;
        width: 100%;
        }

        .form-item input:focus {
        border-bottom: 2px solid #c0c0c0;
        outline: none;
        }

        .button-panel {
        margin: 2em 0 0;
        width: 100%;
        }

        .button-panel .button {
        background: #f16272;
        border: none;
        color: #fff;
        cursor: pointer;
        height: 50px;
        font-family: 'Open Sans', sans-serif;
        font-size: 1.2em;
        letter-spacing: 0.05em;
        text-align: center;
        text-transform: uppercase;
        transition: background 0.3s ease-in-out;
        width: 100%;
        }

        .button:hover {
        background: #ee3e52;
        }

        .form-footer {
        font-size: 1em;
        padding: 2em 0;
        text-align: center;
        }

        .form-footer a {
        color: #8c8c8c;
        text-decoration: none;
        transition: border-color 0.3s;
        }

        .form-footer a:hover {
        border-bottom: 1px dotted #8c8c8c;
        }
        
        </style>
    </head>
    <body>
        <div class="stuff">
        <h1>精算書</h1>
        <div class="button-panel">
            <button id="profile" class="button">ユーザ情報取得</button>
        </div>
        <form name="myForm" action="" method="GET">
            <div class="form-item">
                <label class="label" for="name" name="name">ID　</label>
                <input id="LINE_ID" type="password" class="text" readonly />
            </div>
            <div class="form-item">
                <label class="label" for="name" name="name">氏名</label>
                <input id="LINE_NAME" type="textbox" class="text" readonly />
            </div>
            <div class="form-item">
                <label class="label" for="name" name="name">URL</label>
                <input id="LINE_PROFILE" type="textbox" class="text" readonly />
            </div>
            <div class="form-item">
                <label class="label" for="name" name="name">書類</label>
                <input id="FileName" type="textbox" class="text" value="精算書" readonly />
            </div>
            <div class="form-item">
                <label class="label" for="name" name="name">金額</label>
                <input id="currency" type="number" placeholder="金額" onkeyup="this.setAttribute('value', this.value);" class="text"/>
            </div>
            <div class="form-item">
                <label class="label" for="Reason">理由</label>
                <input id="Reason" type="textbox" placeholder="理由" onkeyup="this.setAttribute('value', this.value);" class="text"/>
            </div>
            <div class="button-panel">
                <button id="message" class="button"> 申請＆押印請求</button>
            </div>
        </form>
        </div>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script src="https://d.line-scdn.net/liff/1.0/sdk.js" type="text/javascript"></script>
        <script type="text/javascript">
            //liff init
            liff.init(
                data => {
			        document.getElementById( "LINE_ID" ).value = data.context.userId;                
                },
                err => {
                    console.log('error', err);
                }
            );
            document.getElementById('profile').addEventListener('click', function () {
                liff.getProfile()
                .then(profile => {
                    document.getElementById("LINE_NAME").value = profile.displayName;
                    document.getElementById("LINE_PROFILE").value = profile.pictureUrl;
                })
                .catch((err) => {
                    console.log('error', err);
                });
            });

            $("button#message").click(function() {
                // 多重送信を防ぐため通信完了までボタンをdisableにする
                var button = $(this);
                button.attr("disabled", true);

                // 各フィールドから値を取得してJSONデータを作成
                var data = {
                    LINE_ID: $("#LINE_ID").val(),
                    LINE_NAME: $("#LINE_NAME").val(),
                    LINE_PROFILE: $("#LINE_PROFILE").val(),
                    FileName: $("#FileName").val(),
                    currency: $("#currency").val(),
                    Reason: $("#Reason").val()
                };

                // 通信実行
                $.ajax({
                    type:"POST",                // method = "POST"
                    url:"KINTONE API経由でデータを登録するURL",        // POST送信先のURL
                    data:JSON.stringify(data),  // JSONデータ本体
                    contentType: 'application/json', // リクエストの Content-Type
                    dataType: "json",           // レスポンスをJSONとしてパースする
                    success: function(json_data) {   // 200 OK時
                        // JSON Arrayの先頭が成功フラグ、失敗の場合2番目がエラーメッセージ
                        // if (!json_data[0]) {    // サーバが失敗を返した場合
                        //     alert("Transaction error. " + json_data[1]);
                        //     return;
                        // }
                        // 成功時処理
                        location.reload();
                    },
                    error: function() {         // HTTPエラー時
                        alert("Server Error. Pleasy try again later.");
                    },
                    complete: function() {      // 成功・失敗に関わらず通信が終了した際の処理
                        button.attr("disabled", false);  // ボタンを再び enableにする
                    }
                });
            });
        </script>
    </body>
</html>

```

申請＆押印請求ボタンを押すと、AjaxでNode-REDに入力された値をPOSTする。

<img width="823" alt="スクリーンショット 2020-01-04 23.37.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123800/f873805b-449f-a1fc-ae32-cc573ce3348b.png">

「Header追加」ノードでは、KINTONE APIにデータをPOSTするためにJSONの形式を整えるとともにKINTONEのAPIキーを設定している。

```javascript:Header追加ノードのソース

var api_key = 'APIキー';

msg.headers = {
    'X-Cybozu-API-Token': api_key,
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + api_key   
    //"X-HTTP-Method-Override": "GET"
};

msg.payload = {
			app: 1,
			record: {
				'LINE_ID': {
					value: msg.payload.LINE_ID
				},
				'LINE_NAME': {
					value: msg.payload.LINE_NAME
				},
				'LINE_PROFILE': {
					value: msg.payload.LINE_PROFILE
				},
				'FileName': {
					value: msg.payload.FileName
				},
				'currency': {
					value: msg.payload.currency
				},
				'Reason': {
					value: msg.payload.Reason
				}
				
			}
}



return msg;

```

[InCaaaan:印鑑マシーンの作り方 上司ツール編](https://qiita.com/oic0310/items/a9b3f2178a11f0774bd8)でも書いていますが、InCaaaanの申請・承認を保管するKintoneのレコード以下の項目を配置している。括弧の中は変数名です。

- レコード番号(recordId)
- 承認キー(Approval)
- LINE ID(LINE_ID)
- 氏名(LINE_NAME)
- LINE プロファイル(LINE_PROFILE)
- 書類名(FileName)
- 金額(currency)
- 申請理由(Reason)

#レッドハッカソンでボツになったAlexa機能

プレゼンで成功する確率を心配して、確実なLIFFに逃げてボツとなったAlexa機能。
レッドハッカソンの時は、Alexa経由で承認要求をする、という機能が存在したのであった・・・

スキルの作り方は細かくは書かないが、本番中に失敗はイヤだ・・・という思いから、スキルが起動すれば、押印請求のURLをPOSTするプログラムを割り込ませていた。

ビビリなので、そもそもスキルが起動しなかったらどうしようと思いって使わなかったが、きっと私のようなビビリな方に役に立つと思い、記述を残しておく・・・（ハッカソンだけで使ってね）

~~割とすぐに金額と申請理由をしゃべればKintoneに登録するアプリが作れる・・・~~

```python:Alexaスキル起動直後のソース

class LaunchRequestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool

        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "OK"
        import urllib.request
    
        url = 'レッドハッカソンURL'
        
        req = urllib.request.Request(url)
        try:
            with urllib.request.urlopen(req) as res:
                body = res.read()
        except urllib.error.HTTPError as err:
            return {
                'statusCode': 404,
                'body': json.dumps(err.code)
            }
        except urllib.error.URLError as err:
            
            return {
                'statusCode': 500,
                'body': json.dumps(err.reason)
            }
    
        return {
            'statusCode': 200,
            'body': json.dumps(body)
        }
        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )
```