122
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

保育園・小中学校での感染症流行状況がわかる Line Botの作成

Last updated at Posted at 2019-10-13

##概要
プログラムの勉強を始めて3か月ほどになる開業医です。今回自分のクリニックがある地域の保育園や小中学校の感染症の流行状況がわかるLINEのFAQボットを作ってみようと思いました。
管轄保健所の感染症サーベイランスをAPIで取得できなかったので、毎週保健所からメールで送られてくる感染症情報をGoogle Spread Sheetに手入力して作成しました。

##実装内容
・LINE上で質問するとその回答が送られてくるFAQBOT。
・質問や回答となる感染症の情報はGoogle Spread Sheetで更新可能。
・質問者とその内容をGoogle Spread Sheetに表示。

##概念図##
バックエンドとしてGoogle Spread Sheetを利用し医療情報を管理。Google App Script(GAS)でLINE botと連携しました。「LINE Bot+APIで表現してアウトプット」 概念図.png

##作成方法
1. LINE Messaging APIの新規チャンネルを作成 ~ channelAccessToken発行

こちらの記事を参考にさせていただきました。

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

こちらの記事の**” 1. Botアカウントを作成する ”**までを行い 、LINE Messaging APIの新規チャンネルを作成します。

channelAccessToken発行の仕方は**" 2. Node.jsでBot開発>Channel Secretとアクセストークンの確認 "**に記載されています。

2. Google Spread Sheet上での作業

これ以降はこちらの記事の通りに作業を進めました。

【30分でやる】Google Spread Sheetで管理するLINE BOTの作り方

・「faq」のシートを作成します。質問と回答は自由に作成できますが、今回は質問に感染症の病名を中心にA列に記載し、回答に感染症の発生状況を中心にB列に記載していきました。

LINE Bot Sheet.png

3.スクリプトエディタでGASを編集

これ以降は先ほどの参考記事の通りに行えば10分ほどで公開までいけます。

開いたスクリプトエディタ上に、先ほどの記事に掲載されていた次のソースコードをコピペします。

// 利用しているシート
var SHEET_ID = '(①スプレッドシートのIDをコピペ)';
// 利用しているSSのシート名(※変えるとみえなくなる)
var SHEET_NAME = 'faq';
// 利用しているもしかしてSSのシート名(※変えるとみえなくなる)
var SHEET_NAME_MAYBE = 'maybe';

// LINE Message API アクセストークン
var ACCESS_TOKEN = '(②LINEのdevelopersアカウント上で取得したTOKENをコピペ)';
// 通知URL
var PUSH = "https://api.line.me/v2/bot/message/push";
// リプライ時URL
var REPLY = "https://api.line.me/v2/bot/message/reply";
// プロフィール取得URL
var PROFILE = "https://api.line.me/v2/profile";

/**
 * doPOST
 * POSTリクエストのハンドリング
 */
function doPost(e) {
  var json = JSON.parse(e.postData.contents);
  reply(json);
}

/** 
 * doGet
 * GETリクエストのハンドリング
 */
function doGet(e) {
    return ContentService.createTextOutput("SUCCESS");
}

/** 
 * reply
 * ユーザからのアクションに返信する
 */
function reply(data) {
  // POST情報から必要データを抽出
  var lineUserId = data.events[0].source.userId;
  var postMsg    = data.events[0].message.text;
  var replyToken = data.events[0].replyToken;
  var action    = data.events[0].message.action;
  // 記録用に検索語とuserIdを記録
//  debug(postMsg, lineUserId);
  debug(action, lineUserId);

  // 検索語に対しての回答をSSから取得
  var answers = findResponseArray(postMsg);

  // 回答メッセージを作成
  var replyText = '' + postMsg + '」ですね。かしこまりました。以下、回答です。';
  // 回答の有無に応じて分岐
  if (answers.length === 0) {
    // 「類似の検索キーワード」がないかチェック
    var mayBeWord = findMaybe(postMsg);
    if (typeof mayBeWord === "undefined") {
      // 回答がない場合の定型文
      sendMessage(replyToken, '答えが見つかりませんでした。別のキーワードで質問してみてください。');        
    } else {
      sendMayBe(replyToken, mayBeWord);
    }
  } else {
    // 回答がある場合のメッセージ生成
    answers.forEach(function(answer) {
      replyText = replyText + "\n\n=============\n\nQ:" + answer.key + "\n\nA:" + answer.value;
    });

    // 1000文字を超える場合は途中で切る
    if (replyText.length > 1000) {
      replyText = replyText.slice(0,1000) + "……\n\n=============\n\n回答文字数オーバーです。詳細に検索キーワードを絞ってください。";
    }
    // メッセージAPI送信
    sendMessage(replyToken, replyText);
  }
}

// SSからデータを取得
function getData() {
  var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
  var data = sheet.getDataRange().getValues();

  return data.map(function(row) { return {key: row[0], value: row[1], type: row[2]}; });
}

// SSから「もしかして」データを取得
function getMayBeData() {
  var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME_MAYBE);
  var data = sheet.getDataRange().getValues();
  return data.map(function(row) { return {key: row[0], value: row[1], type: row[2]}; });
}

// 単語が一致したセルの回答を配列で返す
function findResponseArray(word) {
  // スペース検索用のスペースを半角に統一
  word = word.replace(' ',' ');
  // 単語ごとに配列に分割
  var wordArray = word.split(' ');
  return getData().reduce(function(memo, row) {
    // 値が入っているか
    if (row.value) {
      // AND検索ですべての単語を含んでいるか
      var matchCnt = 0;
      wordArray.forEach(function(wordUnit) {
        // 単語を含んでいればtrue
        if (row.key.indexOf(wordUnit) > -1) {
          matchCnt = matchCnt + 1;
        }
      });
      if (wordArray.length === matchCnt) {
        memo.push(row);
      }
    }
    return memo;
  }, []) || [];
}

// 単語が一致したセルの回答を「もしかして」を返す
function findMaybe(word) {
  return getMayBeData().reduce(function(memo, row) { return memo || (row.key === word && row.value); }, false) || undefined;
}

// 画像形式でAPI送信
function sendMessageImage(replyToken, imageUrl) {
  // replyするメッセージの定義
  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        "type": "image",
        "originalContentUrl": imageUrl
      }
    ]
  };
  return postMessage(postData);
}

// LINE messaging apiにJSON形式でデータをPOST
function sendMessage(replyToken, replyText) {  
  // replyするメッセージの定義
  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        "type" : "text",
        "text" : replyText
      }
    ]
  };
  return postMessage(postData);
}

// LINE messaging apiにJSON形式で確認をPOST
function sendMayBe(replyToken, mayBeWord) {  
  // replyするメッセージの定義
  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        "type" : "template",
        "altText" : "もしかして検索キーワードは「" + mayBeWord + "」ですか?",
        "template": {
          "type": "confirm",
          "actions": [
            {
                "type":"postback",
                "label":"はい",
                "data":"action=detail",
            },
            {
                "type": "message",
                "label": "いいえ",
                "text": "いいえ、違います。"
            }
          ],
          "text": "答えが見つかりませんでした。もしかして検索キーワードは「" + mayBeWord + "」ですか?"
        }

      }
    ]
  };
  return postMessage(postData);
}

// LINE messaging apiにJSON形式でデータをPOST
function postMessage(postData) {  
  // リクエストヘッダ
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    "Authorization" : "Bearer " + ACCESS_TOKEN
  };
  // POSTオプション作成
  var options = {
    "method" : "POST",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
  return UrlFetchApp.fetch(REPLY, options);      
}

/** ユーザーのアカウント名を取得
 */
function getUserDisplayName(userId) {
  var url = 'https://api.line.me/v2/bot/profile/' + userId;
  var userProfile = UrlFetchApp.fetch(url,{
    'headers': {
      'Authorization' :  'Bearer ' + ACCESS_TOKEN,
    },
  })
  return JSON.parse(userProfile).displayName;
}

// userIdシートに記載
function lineUserId(userId) {
  var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName('userId');
  sheet.appendRow([userId]);
}

// debugシートに値を記載
function debug(text, userId) {
  var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName('debug');
  var date = new Date();
  var userName = getUserDisplayName(userId);
  sheet.appendRow([userId, userName, text, Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss')]);
}

コピペしたコードの中に記載されている(①スプレッドシートのIDをコピペ)の部分にスプレッドシートのID(スプレッドシート画面を開いた時のURLの/d/と/edit#git=0の間の部分)をコピペします。
また、(②LINEのdevelopersアカウント上で取得したTOKENをコピペ)の部分には1. LINE Messaging APIの新規チャンネルを作成 ~ channelAccessToken発行で確認したアクセストークンをコピペします。

4.アプリケーションの公開~LINEアカウントでWebhookの設定
上記の作業も記事の通りに進めていきます。

5.動作確認
ちゃんと質問に回答しています。

IMG-0682.PNG IMG-0683.PNG IMG-0684.PNG

動画です。

##考察
Google Spread Sheet × Google Apps Script × LINE の組み合わせで驚くほど速く実装することができました。他にも色々できそうなので、体重を入れると薬の量が回答されるようなBotも作成してみようかなと思いました。
毎週感染症情報をGoogle Spread Sheetに入力することはすこし面倒なので、何か方法を考えたいと思います。

122
61
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
122
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?