ブログ名

LINE Bot開発

概要

・サーバレスでLINE botを作りたい ・費用をかけずに作りたい ということで、Google Apps ScriptでLINE BOTを開発してみました。 この記事では、Line側の設定、LineBot本体の開発、外部公開までの手順を記載しています。

使用するサービス

  • GoogleAppsScript
  • LINE Messaging API
    • Lineアカウントが必要

開発の流れ

Line側の設定(Channel作成)

LINE Developer コンソールにアクセスし、ログインボタンをクリックして自身のLINEアカウントでログインします

f:id:iTD_GRP:20190719163926p:plain

はじめてログインしたときには開発者登録を求められますので必要事項を入力してください。(下記が入力済の様子)

f:id:iTD_GRP:20190719164239p:plain

次にプロバイダーを作成します。プロバイダーとはこれから作成するBotの提供元として表示される情報です。

f:id:iTD_GRP:20190719164306p:plain

必要情報を入力して進んでください。

f:id:iTD_GRP:20190719173440p:plain

次にChannelを作成します。ChannelにはLINEログインのほか三つが存在します。今回作成するのはBotなのでMessaging APIを選択してください。

f:id:iTD_GRP:20190719173507p:plain

こちらの画面で必要情報を入力していきます。

  • アプリ名:任意アプリ名
  • アプリ説明:任意の説明
  • 大業種:任意選択
  • 小業種:任意選択
  • メールアドレス:任意のメールアドレス

f:id:iTD_GRP:20190719173532p:plain

f:id:iTD_GRP:20190719173556p:plain

これでChannelが作成されますがまだもう少し設定が必要です。「TutorialBot」をクリックします。

f:id:iTD_GRP:20190719173631p:plain

メッセージ送受信設定のセクションにあるアクセストークンの「再発行」ボタンをクリックします。

f:id:iTD_GRP:20190719173809p:plain

f:id:iTD_GRP:20190719173839p:plain

これでアクセストークンが発行されます。このトークンはMessaging APIの呼び出し時に必要になります。

あと残りの項目を下記の通り設定します。

  • Webhook送信:利用する
  • Botグループトーク参加:利用する
  • 自動応答メッセージ:利用する
  • 友だち追加時あいさつ:任意

これでChannelの設定はほぼ完了です。Bot本体をクラウドにデプロイしてからWebhook URLの設定では、後述します。 また、これからおこなうBot本体の開発でChannel Secretとアクセストークンが必要になりますので、メモしておいた方が良いです。

LineBot本体の開発

Googleドライブにログイン後、Googleスプレッドシートを選択。

f:id:iTD_GRP:20190719173938p:plain

スプレッドシートを開いたらスクリプトエディタを選択。

f:id:iTD_GRP:20190719173953p:plain

そしたらエディタが開くのでここにbotの中身をガリガリ書いていきましょう。

f:id:iTD_GRP:20190719174015p:plain

下記より、今回開発したソースのコア部分を抜粋して掲載しています。

doPost関数 

ユーザから発信したメッセージを受けて、処理を分岐させる役割を担う(コントロールに相当)

[do.Post.gs]

function doPost(e) {
  var json = e.postData.contents
  var events = JSON.parse(json).events;
  
  events.forEach(function(event) {
    if(event.type == "follow") {
      follow(event);
    } 
    else if(event.type == "message") {
      var text = event.message.text;
      var userId = event.source.userId;
      var groupId = event.source.groupId;
      
      if (groupId !== '' && groupId !== undefined) {
        writeLog('groupid', groupId);
        return;
      }
      writeLog('userId', userId);
      writeLog('text', text); 
      var userDataRow = -1
      switch (text) {
        case '予約':
          // 削除用sheet内容クリア(キャンセルが終わらせなくても、予約の場合、キャンセルをやり直すようにする
          cancelsheet.getDataRange().clear();  
          replyText = '予約ですね!いつですか? 例:「6月12日 13:00ー14:00」を教えて下さいね!';
          userDataRow = searchUserDataRow(userId);
          if (userDataRow === -1) {
            appendToSheet(userId);
          }
          
          // ユーザ毎絞り込みフラグが立てば削除する
          stopForRefineSch(userId);
          break;
        case '確認':
          reply_message(event);
          
          // ユーザ毎絞り込みフラグが立てば削除する
          stopForRefineSch(userId);
          break;
        case 'キャンセル':
          userDataRow = searchUserDataRow(userId);
          if (userDataRow !==-1) {
            replyText = cancel(userDataRow);
          } else {
            reply_message(event);
          }
          
          // ユーザ毎絞り込みフラグが立てば削除する
          stopForRefineSch(userId);
          break;  
        case '日付で確認':
          reply_message(event);
          break;
        case '日付でキャンセル':
          reply_message(event);
          break;
        case '絞り込み条件検索_確認':
          reply_message(event);
          setUserDataForRefineSch(userId, '確認');
          break;
        case '絞り込み条件検索_キャンセル':
          reply_message(event);
          setUserDataForRefineSch(userId, 'キャンセル');
          break;
        case 'やめる': 
          if (stopForRefineSch(userId)) {
            replyText = '絞り込み検索をやめました。';
          } else {
            replyText = '絞り込み検索が始まりませんよ。「確認」で絞り込み検索を行えます。';
          }
          break;
        case '検索': 
          if (isRefineSchFromUserId(userId)) {
            
            if (isEmptyForKikan(getRefineSchCell(userId, 2).getValue(), getRefineSchCell(userId, 3).getValue())) {
              replyText = '検索「期間」を設定してください。(必須)';
            } else {
              getRefineSchCell(userId, 2).getValue()
              replyText = doSearch(userId);
              // ユーザ毎絞り込みフラグが立てば削除する
              stopForRefineSch(userId);
            } 
          } else {
            replyText = '検索条件が設定されません。「確認」から行って下さい。';
          }
          break;
        case '使い方':
          break;
        default:
          var iscancel = getCancelCell(0).getValue() === 'cancel' ? true : false;
          var isRefineSch = isRefineSchFromUserId(userId);
          // 絞り込み検索
          if (isRefineSch) {
            replyText = getRefineSchFromInput(text, userId);
            break;
          }
          // 予約キャンセル
          if (iscancel) {
            replyText = eventCancel(text, userId);
            break;
          }
          
          userDataRow = searchUserDataRow(userId);
          if (userDataRow === -1) {
            replyText = getMenuInfoMessage();
          } else if (userDataRow === -2) {
            replyText = '申し訳ございません。\n予約操作を始めたが、一定時間登録が出来なかったため、再度「予約」から始めて下さい。';
            
          } else {
              var todoDate = getDateStartCell(userDataRow).getValue();
              var location = getLocationCell(userDataRow).getValue();
              var registUser = getRegistUserCell(userDataRow).getValue();
              if (todoDate === '') {
                replyText = getDateFromInput(text, userDataRow);
                // sendLineMessageFromReplyToken(event, replyText, false);
              } else if (location === ''){
                replyText = getLocationFromInput(text, userDataRow);
                
              } else if (registUser === ''){
                replyText = getRegistUserFromInput(text, userDataRow, event);
              } 
            
            break;
          }
      }
      
      if (userDataRow === -1 || userDataRow === -2) {
        sendLineMessageFromReplyToken(event, replyText, false);
        return;
      }
      
      if (getRegistUserCell(userDataRow).getValue() !== '') {
        // 登録者指定までできていれば、カレンダーに登録
        createEvents(userDataRow);
        
        // trueならば、”予約済み”の旨を伝える
        sendLineMessageFromReplyToken(event, replyText, true);
        if (isPushToGroup) {
          // 予約をグループに通知
          sendLineMessageFromGroupId(getEventContextByTime(userDataRow));
        }
        return;
      }
      sendLineMessageFromReplyToken(event, replyText, false);
    } 
    else if (event.type == "postback") {
    // 日付を選択後、コールバック関数で値を受け取る
      post_back(event);
    }
  });
}

Botからの返答処理

ユーザ発信メッセージに応じた返答メッセージを返す

[sendLineMessageFromReplyToken.gs]

function sendLineMessageFromReplyToken(e, msg) {
  var message;
  message = {
      "replyToken" : e.replyToken,
      "messages" : [{"type": "text", "text" : msg}]
      };
  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(message)
  };
  UrlFetchApp.fetch(url, options);
}

上記インスタンス変数「headers」、「url」が下記ように定義してある

[instance.gs]

// channel_token:Line設定側の「アクセストークン」を設定
var headers = {
  "Content-Type": "application/json; charset=UTF-8",
  "Authorization": "Bearer " + channel_token
};
// 応答API
var url = "https://api.line.me/v2/bot/message/reply"

グループへの発信処理

[sendLineMessageFromGroupId.gs]

function sendLineMessageFromGroupId(text) {
  // push APIを利用
  var url = "https://api.line.me/v2/bot/message/push";
 // groupIdを予め取得しておかないと
  var postData = {
    "to": groupID,
    "messages": [{
      "type": "text",
      "text": text
    }]
  };
  var options = {
    "method": "POST",
    "headers": headers,
    "payload": JSON.stringify(postData)
  };
  return UrlFetchApp.fetch(url, options);
}

Googleカレンダーへの登録処理

(ポイントのみ記載)

[createEvents.gs]

function createEvents(userDataRow) {
  try {    
    var option = {
      description: registUser + '\n' + eventId,
      location: getLocationCell(userDataRow).getValue()
    }
    // googleカレンダーAPI取得
    var calendar = CalendarApp.getDefaultCalendar();
    calendar.createEvent(title, startTime, endTime, option);
    
    writeLog('createEvents', '成功');
  } catch(e) {
    writeLog('createEvents', '失敗 ' + e);  
  }
}

定時アナウンストリガー設定方法

「現在のプロジェクトのトリガー」を選択

f:id:iTD_GRP:20190719174131p:plain

下図の通り、現在、日々9時に「setTriggerByDaily」関数を実行させるようなトリガーになっていますが、新規設定するには、「トリガー追加」をクリックします。

f:id:iTD_GRP:20190719174203p:plain

トリガー設定ダイアログ画面

f:id:iTD_GRP:20190719174229p:plain

上記トリガー設定することで、時間になったら、「setTriggerByDaily」関数が呼ばれるが、具体的に何時何分にメッセージをプッシュするか、という2ステップのトリガー設定が必要。何故ならば、上記「トリガー設定ダイアログ画面」にて、トリガー発火時間帯のみ選択できるためです。今回では[7時-8時]を設定しました。

[setTriggerByDaily.gs]

function setTriggerByDaily() {
  var triggerDay = new Date();
// sendEventToGroupByDailyを8:59分に実行させる
  triggerDay.setHours(8);
  triggerDay.setMinutes(50);
  ScriptApp.newTrigger("sendEventToGroupByDaily").timeBased().at(triggerDay).create();
  writeLog('setTriggerByDaily', '成功');
}

Botに教えた登録内容をどこに記録するか

ウェブアプリ開発とは異なり、入力した内容を全てformというものに乗っけるため、コミット後、登録内容を一気に取得できます。しかし、Bot開発では、自然対話の中、正確と判断した内容をどこに記録しておかないと、登録まで(カレンダー登録)至りません。そのため、下記イメージ通り、GoogleScriptSheetというのを利用しています。

f:id:iTD_GRP:20190719174317p:plain

現在運用中Botでは、登録際に下記ような内容が書き込まれています。

f:id:iTD_GRP:20190719174338p:plain

その他

開発に関してのポイントが、上記述べたようになりますが、ニーズに応じてブラッシュアップしなければいかないのもありましが一旦割愛させて頂きます。

アプリケーション公開・利用方法

公開手順

スクリプトエディタのメニューから「公開」→「ウェブアプリケーションとして導入」を選択します。

f:id:iTD_GRP:20190719174407p:plain

すると、「ウェブアプリケーションとして導入」ウィンドウが開きます。

f:id:iTD_GRP:20190719174426p:plain

プロジェクトバージョン は、このウェブアプリケーションのバージョン管理をするためのものです。今回は最初ですから「新規作成」で入力内容は「最初のバージョン」などとしておけばよいです。

アプリケーションにアクセスできるユーザー で公開範囲を決めます。以下の通りですので、用途に合わせて使い分けて下さい。

  • 自分だけ:自分のアカウントでログインしていればアクセスできます
  • 全員:Googleアカウントでログインしていればアクセスできます
  • 全員(匿名ユーザー含む):ログインせずにアクセスできます

最後に「導入」とすると、公開が完了します。ウェブアプリケーションのURLが生成されます。

f:id:iTD_GRP:20190719174458p:plain

LineBotとの繋ぎ込み

ウェブアプリケーションURLをコピーし、Line設定ページ「Webhook URL」に張り付けます。

f:id:iTD_GRP:20190719174529p:plain

これで、LineBotが利用可能になります。

参考文献

以上です。


目次に戻る