セキュリティ周りをLINEさんとGoogleさんに丸投げして、個人情報を保護した診断名メモアプリを作成する

# みんな自分の『診断名』を覚えていない。
みなさん、病院で言われた診断名って覚えていますか？？
例えば『慢性炎症性脱髄性多発神経炎』のような診断名を聞いても、『邪王炎殺黒龍波』みたいな必殺技（この疾患で悩まれている方、申し訳ありません）に聞こえて、覚えられませんよね？？
でも大丈夫！
自分専用の診断名メモアプリを作成しました！
挙動は以下の通りです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ラインからのポストでも診断名を入力可能にアップデート！<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://twitter.com/hashtag/deployorlose?src=hash&amp;ref_src=twsrc%5Etfw">#deployorlose</a> <a href="https://t.co/G8aCq4nowf">pic.twitter.com/G8aCq4nowf</a></p>&mdash; 北城雅照|VRサーフィン&quot;In The Greenroom&quot;を開発中の整形外科医 (@teru3_kitashiro) <a href="https://twitter.com/teru3_kitashiro/status/1369563199574405126?ref_src=twsrc%5Etfw">March 10, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

LINEIDに紐づいているので、__自分の診断名は自分しか見ることができません！__
今回、セキュリティ関しては大手のLINEさんとGoogleさんに丸投げすることで、無料で堅牢なものが作れました。
記事を参考に、みなさんもぜひ作ってください！

#システムの概要
![protooutstudio.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581193/110fa483-384c-257b-2a36-945afdb3138b.jpeg)


# 開発環境の下準備
１）　VScodeのインストール
VScodeのインストールついては、googleなどで他の記事を検索してください。

２）　node.jsとnpmのインストール
Macでの環境作りは、こちらの別の記事にまとめてあります。
参考にしてください。
[Macにnode.js,npmのインストール方法](https://qiita.com/Teru_3/items/cc8a4968eff131d00784)

3) npmのパッケージをインストール

```:terminalコマンド
$ npm init -y
```



# firebaseの設定
## １）　google firebaseへ移動しコンソールを開く  [firebase](https://firebase.google.com/?hl=ja)

## ２）　新しいプロジェクトを作成する
[![Image from Gyazo](https://i.gyazo.com/c4c010c1fc604ddc48dd94fb65184774.png)](https://gyazo.com/c4c010c1fc604ddc48dd94fb65184774)

プロジェクト名を設定し続行
[![Image from Gyazo](https://i.gyazo.com/a968fa6d404b3e8ef34c8e82b8f3a921.png)](https://gyazo.com/a968fa6d404b3e8ef34c8e82b8f3a921)

アナリティクスを無効にしてプロジェクト作成
[![Image from Gyazo](https://i.gyazo.com/04673995d655e1cd901f0cfec8d426e7.png)](https://gyazo.com/04673995d655e1cd901f0cfec8d426e7)
しばらく待ってから、続行

[![Image from Gyazo](https://i.gyazo.com/b31c0c91fea354cc245f1a4698ae3894.png)](https://gyazo.com/b31c0c91fea354cc245f1a4698ae3894)
その後プロジェクトが完成します。

## 3)プロジェクトのCDNを取得する
webを選択
![スクリーンショット 2021-03-13 18.47.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581193/6d6f4e8b-f8e3-ba10-8e04-37c00393a012.png)

適当に名前をつける
[![Image from Gyazo](https://i.gyazo.com/2f0dbb261e617044bc306a88be3f5083.png)](https://gyazo.com/2f0dbb261e617044bc306a88be3f5083)

CDNが発行されるので、firebaseCongif部分をメモっておく
[![Image from Gyazo](https://i.gyazo.com/40ade00a1e0a8da431ce4652b0264c5f.png)](https://gyazo.com/40ade00a1e0a8da431ce4652b0264c5f)
＊Configを忘れても、プロジェクトの設定から確認可能です。

## 4)Cloud Firestore
Cloud Firestoreを選択
[![Image from Gyazo](https://i.gyazo.com/31502e630c09ebab81257574e2f1fe96.png)](https://gyazo.com/31502e630c09ebab81257574e2f1fe96)

テストモードを選択
[![Image from Gyazo](https://i.gyazo.com/cb860f0400553fff4d38f794f7186f95.png)](https://gyazo.com/cb860f0400553fff4d38f794f7186f95)

時刻としてasia-northeast1を選択
[![Image from Gyazo](https://i.gyazo.com/7d135641123e43a2bbd9cef1e6a7599f.png)](https://gyazo.com/7d135641123e43a2bbd9cef1e6a7599f)

構造として、コレクションというフォルダの中に、ドキュメントというデータファイルが入っており、その中にデータが格納される、という感じです。
[![Image from Gyazo](https://i.gyazo.com/13c469612b800cf5beff211b7899407d.png)](https://gyazo.com/13c469612b800cf5beff211b7899407d)



# タイムライン側の設定
## 1)　フォルダ内にラインボット用のパッケージをnpmでインストール

```:terminal
npm i @line/bot-sdk express firebase
```

## 2) LINEのMessaging APIの設定

LINEのMessaging APIについては下記を参考に設定してください。
[LINEのアカウントを作ってLINE massaging APIで返答したい（Python + Django）](https://qiita.com/TRO555/items/57d80514e358f80f26de)


## 3) タイムライン側のscript

```javascript:addDBline.js
'use strict'; // おまじない

// ########################################
//               初期設定など
// ########################################

// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const firebase = require("firebase/app");
require("firebase/firestore");

const firebaseConfig = {
// Configの部分を貼り付ける
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();

// ローカル（自分のPC）でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
  channelSecret: 'Messaging APIのchannelSecretを貼り付ける',
  channelAccessToken: 'Messaging APIのchannelAccessTokenを貼り付ける'
};

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
        console.log("------");
        console.log(event);
        console.log("------");
        console.log(event.source);
        console.log("------");
        console.log(event.source.userId);

  // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  // データベースにデータを追加
  const now = new Date();            //日付取得準備（必須）！！
  const month = now.getMonth() + 1;  //月+１を足す
  const date = now.getDate();        //日       
  const h = now.getHours();          //時
  const m = now.getMinutes();        //分
  const s = now.getSeconds();       //秒
  //日時表示文字列の作成
  const time = month + "/" + date + " " + h + ":" + m + ":" + s;
  const dgDay = time
  await db.collection(event.source.userId).add({
    dgname : event.message.text,
    dgday: dgDay,
    time: now
  });

  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  return client.replyMessage(event.replyToken,[
     {
    type: 'text',
    text: '『' + event.message.text + '』' + 'を診断名として入力しました。' // ← ここに入れた言葉が実際に返信されます
  },{
    type: 'text',
    text: '下のメニューからご確認ください。' // ← ここに入れた言葉が実際に返信されます
  }]
  );
};


// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get('/', (req, res) => res.send('Hello LINE BOT! (HTTP GET)'));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  // Webhookの中身を確認用にターミナルに表示します
  console.log('tets1:'+req.body.events);

  // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
  // 削除しても問題ありません
  if (req.body.events.length == 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
    console.log('検証イベントを受信しました！'); // ターミナルに表示します
    return; // これより下は実行されません
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
```

### scripの肝

```javascript:addDBline.js
  await db.collection(event.source.userId).add({
    dgname : event.message.text,
    dgday: dgDay,
    time: now
  });
```
'event.source.userId'はLINEのUserIDを意味し、この名前でfirebseにコレクションを作成する。


## 4)Herokuへのデプロイ
node.jsが動作する無料のクラウドサーバーであるHerokuにデプロイします。

### 下準備
１）　Heroku CLIのインストール
https://devcenter.heroku.com/articles/heroku-cli

２）　Herokuアカウントの作成
https://signup.heroku.com/

### デプロイ
ターミナルで下記コマンドを実行します。

```:terminalコマンド
$ heroku login
```
実行するとブラウザが自動的に起動するので、ログインします。
すると、ターミナル上でもログインが完了します。

その後、package.jsonの"scripts"の中に、"start"を下記のように追記します。

```:package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node addDBline.js"
  },
```
Heroku内にデプロイ先のディレクトリを作成します。
下記の順でターミナルにコマンドを入力します。

```:terminalコマンド
$ git init
```

```:terminalコマンド
$ heroku create
```
Heroku内にデプロイ先のディレクトリが作成されると、ターミナルに下記のように表示されます。

```:terminal
Creating app... done, ⬢ デプロイ先の名前
https://デプロイ先の名前.com/ | https://git.heroku.com/デプロイ先の名前.git
```
このデプロイ先ディレクトリに、作成したディレクトリを入れていきます。
下記の順でターミナルにコマンドを入力します。

```:terminalコマンド
$ git add .
```

```:terminalコマンド
$ git commit -m 'init'
```

```:terminalコマンド
$ git push heroku master
```
最終的に下記のように表示されれば完了です。

```:terminal
remote:        https://デプロイ先の名前.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/デプロイ先の名前.git
 * [new branch]      master -> master
```
これでデプロイ完了です。

### ファイルを更新する時
まず、更新するファイルが入ったディレクトリがherokuのデプロイ先のディレクトリと紐づいているかを確認します。

```:terminalコマンド
$ git remote -v
```

#### 紐づいている場合
紐づいている場合は、ターミナルに下記のように表記されます。

```:terminal
heroku https://git.heroku.com/heroku上のデプロイ先の名前 (fetch)
heroku https://git.heroku.com/heroku上のデプロイ先の名前 (push)
```

#### 紐づいていない場合
紐づいていない場合は、ターミナルに下記のように表記されます。

```:terminal
fatal: not a git repository (or any of the parent directories): .git
```

その場合は、ターミナルに下記の順に入力し、空のディレクトリを作成し、heroku上のデプロイしたいディレクトリと紐付けを行います。

```:terminalコマンド
$ git init　//空のディレクトリを作成
```
```:terminalコマンド
$ heroku git:remote -a heroku上のデプロイしたいディレクトリ名　
　　//herokuのデプロイ先と先ほど作成した空のtディレクトリを紐付け
```

その上で、下記の順にコマンドを入力し、ファイルを更新します。

```:terminalコマンド
$ git add .
```

```:terminalコマンド
$ git commit -m 'init'
```

```:terminalコマンド
$ git push heroku master
```

### webhookの設定
Herokuから取得したデプロイ先のURLをMessaging APIに入力。
/webhookをつけることを忘れずに。
![スクリーンショット 2020-11-22 12.20.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581193/964dc27f-002a-37b3-f795-7d8a6007afaf.png)

# リッチメニュー側の設定　

## 1) LIFFのLINEロクインの設定

LIFFのLINEロクインについては下記を参考に設定してください。
[LIFF v2でLINEログインを試してみた](https://qiita.com/h-takauma/items/f954a6c2a8ad9ee4396b)

## 2) リッチメニュー側のscript

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- JQuery -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" 
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

</head>
<body>
    <div id="app" style="padding: 30px;">
        <div>
        診断名：<input id="dgname" placeholder="診断名">
        <button id='post'>送信</button>
        </div>
        <div id = "output" style="height: 300px; overflow: auto"></div>
    </div>

    
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-firestore.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vConsole/3.3.4/vconsole.min.js"></script>
  <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
  <script>
    window.onload = function () {
      initVConsole();
    }
    // Initialize vConsole
    function initVConsole() {
      window.vConsole = new window.VConsole({
        defaultPlugins: ['system', 'network', 'element', 'storage'],
        maxLogNumber: 1000,
        onReady: function () {
          console.log('vConsole is ready.');
        },
        onClearLog: function () {
          console.log('on clearLog');
        }
      });
    }
    // Your web app's Firebase configuration
    const firebaseConfig = {
     configを貼り付ける
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
    const db = firebase.firestore();
    let userId = "";

    async function post() {
      const collectionName = userId
      console.log('cllecotion userId:' + collectionName);
      const now = new Date();            //日付取得準備（必須）！！
      const month = now.getMonth() + 1;  //月+１を足す
      const date = now.getDate();        //日
      const day = now.getDay();          //曜日（数値）
      const h = now.getHours();          //時
      const m = now.getMinutes();        //分
      const s = now.getSeconds();       //秒
      //日時表示文字列の作成
      const time = month + "/" + date + " " + h + ":" + m + ":" + s;
      const dgName = $('#dgname').val();
      const dgDay = time
      const res = db.collection(collectionName).doc();
      await res.set({
              dgname : dgName,
              dgday: dgDay,
              time: now
            });
    };

    $('#post').on('click', post);
  
    $(document).ready(async function () {
        const liffId = "liffのIDをいれる";
    
        // liffの初期化
        await liff.init({ liffId: liffId });
        // これ以降にliffを利用した処理を書いていく
    
        // 開いている端末のos取得
        console.log(liff.getOS());
    
        // ログインしてなければログインを促す
        // LINE内で開いた場合は特にログイン処理なしで大丈夫
        if (!liff.isLoggedIn()) {
        liff.login();
        return;
        }
    
        // プロフィール情報取得
        const profile = await liff.getProfile();
        userId = profile.userId; //LINEのID
        console.log("userID:" + userId);

        const observer = db.collection(userId).orderBy("time", "asc").onSnapshot(snapshot => {
        $('#output').empty();
        console.log('on snapshot!');
        const collectionName = userId
        console.log('observer userId:' + userId);
        snapshot.forEach(doc => {
        console.log(doc.data().dgname);
        console.log(doc.data().dgname);
        console.log(doc.data().dgday);
        const h = '<p> 診断名：' + doc.data().dgname + '<br>診断日：' + doc.data().dgday  + '</p>';
        $('#output').append(h);
        const scrollHeight = $('#output')[0].scrollHeight;
        $('#output').scrollTop(scrollHeight);
        });
        });
      
  });
</script>
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</body>
</html>
```

### scriptの肝

```html
<script>
 $(document).ready(async function () {
        const liffId = "liffのIDをいれる";
    
        // liffの初期化
        await liff.init({ liffId: liffId });
        // これ以降にliffを利用した処理を書いていく
    
        // 開いている端末のos取得
        console.log(liff.getOS());
    
        // ログインしてなければログインを促す
        // LINE内で開いた場合は特にログイン処理なしで大丈夫
        if (!liff.isLoggedIn()) {
        liff.login();
        return;
        }
    
        // プロフィール情報取得
        const profile = await liff.getProfile();
        userId = profile.userId; //LINEのID
        console.log("userID:" + userId);

        const observer = db.collection(userId).orderBy("time", "asc").onSnapshot(snapshot => {
        $('#output').empty();
        console.log('on snapshot!');
        const collectionName = userId
        console.log('observer userId:' + userId);
        snapshot.forEach(doc => {
        console.log(doc.data().dgname);
        console.log(doc.data().dgname);
        console.log(doc.data().dgday);
        const h = '<p> 診断名：' + doc.data().dgname + '<br>診断日：' + doc.data().dgday  + '</p>';
        $('#output').append(h);
        const scrollHeight = $('#output')[0].scrollHeight;
        $('#output').scrollTop(scrollHeight);
        });
        });
      
  });
</script>
```
この部分で、LINEのuserIDを取得し、それをコレクション名に指定している。
この部分は、'ready'を使用することでページか開かれた瞬間に発動する。

## 3)Netilifyへのデプロイ
LIFF側は無料で静的サイトをデプロイできるNetlifyにデプロイします。
下記の記事を参考にデプロイしてください。
[Macにnode.js,npmのインストール方法Netlifyで静的サイトのホスティングをする](https://qiita.com/TRO555/items/57d80514e358f80f26de)