見出し画像

LINEのBOTで実験の声かけしてみる(2)

前回のふりかえりと方針変更

作りたいBOTの動きはこんな感じ。実験開始から1時間の間に5回の声がけ。

実際にLINEでBOTを作ってみたが、Google Apps Script の制限で5分以上の待つことができないことが判明した。まめ1号では、「開始」をトリガーにLINEから呼び出される→返事する→待つ→1回目の声がけする→待つ…(繰り返し)…5回目の声がけする→待つ→終了のお知らせ→終了、だった。

web の仕組みは「呼び出されたら速攻で返事をして処理を終了させる」のがキホン。まめ2号では、「開始」をトリガーにLINEから呼び出される→返事する→1回目の声がけをn分後の予約→終了、とし速攻で終了するようにする。それ以降の声がけは予約したタイマーをトリガーにプログラムが実行される。n分後にタイマーをトリガーに起動され、1回目の声がけをする→2回目の声がけをn分後の予約→終了、これも速攻で終了。この処理を繰り返す。タイマーをトリガーに5回目の声がけをする→終了のお知らせをn分後の予約→終了。最後に、終了のお知らせをする→終了。図解すると次の通り。まめ1号に比べてちょっと複雑になったが同じ処理の繰り返し。



新しいチャネルを作る

LINE Developers コンソールを開きます。前回は[今すぐはじめよう]ボタンから開始しましたが、右の方に[ログイン]からでも開始できます。

[新規チャネル作成]をクリックして項目を埋めていきます。

前回のまめ1号と同様にチャネルの設定です。まめ2号にしたのと、プロバイダーは前回作った ismcafe にしました。プロフ写真は黒っぽい深煎りの豆に。まめ1号との違いはビミョーです。
■■■■
新規チャネルを作らなくても、まめ1号を再利用する方が面倒な設定を再度やる必要がないのでオススメです。

種類:Messaging API
プロバイダー:ismcafe
会社・事業者の所在国・地域:日本
チャネル名:beans01チャネル
説明:まめ2号
大業種:保育・学校
小業種:大学

前回のまめ1号ではデフォルトの応答メッセージでしたが、LINE Developers コンソールの [Message API設定] タブの下の方に[応答メッセージ]の[編集]でも、[あいさつメッセージ]の[編集]でも同じページに遷移します。
■■■■
まめ1号を再利用した場合でもこのあいさつメッセージはカスタマイズしておくと良いでしょう。

LINE Official Account Managerコンソールが開きます。[応答設定]の[あいさつメッセージ設定]をクリックします。

友達追加の時のデフォルトのメッセージはこんな感じになっています。これを変更していきます。右にプレビューが出てるのは嬉しいです。絵文字を使わなければプレビューはなくても平気です。

メッセージ部分だけ拡大するとこんな感じ。今回は実験に参加してくる人へのあいさつと実験の流れを説明し作り替えていきます。

デフォルトのともだち追加メッセージ

{Nickname}さん
今回は実験にご協力いただきありがとうございます。
実験をお手伝いするロボットの{AccountName}です。
このアカウントはロボット(=プログラム)が自動的に返事をしており人が応答しているわけではありません。
・実験を開始するときは「開始します」とトークしてください
・実験の期間(約1時間)の間に何度か私からトークしますが返信は不要
もしも不具合がございましたらxxxxxxxにご連絡ください。

最後に画面下の[変更を保存]を押すのを忘れずに!

新しくGoogleスプレッドシートを作る

まめ1号で作ったGoogleスプレッドシートをコピーしてもOKです。プログラム(=Apps Script)までコピーされるようです。
■■■■
まめ1号を再利用する場合は、新しく作る必要はありません。

新しいシートを作る

queue という新しいシートを作ります。queue(キュー)とは待ち行列のこと。処理の順番=待ち行列=queue のことです。新しくシートを作った理由は後ほど。今はまず動かすことを優先します。
■■■■
まめ1号を再利用した場合でも queue というシートは必ず作ります。

Google Apps Script (=プログラム)はこちら、まめ1号に比べてだいぶ大きく(=長く)なっています。
■■■■
まめ1号を再利用する場合は、このGoogle Apps Script でそっくり置き換えます。

function setTriggerAfterZ(functionName, min) {
  ScriptApp.newTrigger(functionName).
    timeBased().
    after(min * 60 * 1000).
    create();
}

function postMessageStepZ(userId, stepNum) {
  if ( stepNum == 0 ) {
    postMessageZ( userId, "では実験を始めてください。");
  }
  else if ( stepNum == 1 ) {
    postMessageZ( userId, "順調ですか?");
  }
  else if ( stepNum == 2 ) {
    postMessageZ( userId, "その調子です。");
  }
  else if ( stepNum == 3 ) {
    postMessageZ( userId, "良いペースです。");
  }
  else if ( stepNum == 4 ) {
    postMessageZ( userId, "もう少しです。");
  }
  else if ( stepNum == 5 ) {
    postMessageZ( userId, "頑張ってください。");
  }
  else if ( stepNum == 6 ) {
    postMessageZ( userId, "お疲れ様でした。");
  }
  else {
    postMessageZ( userId, "hogehoge"); // ここには到達しないはず。ステップ0未満か7以上はない
  }
}

function mkIntervalZ() { 
  var rrr;
  rrr = Math.round(7 + Math.random() * 4); // 7分 + ランダムで0,1,2,3,4分
  Logger.log(rrr);
  return rrr;
}

function setNextSchedule( userId, stepNum, progressTime) {
  const functionName = 'execQueueTopZ';
  var iii;
  if ( stepNum < 6) {  // stepNum 1,2,3,4,5 の時はランダムな待ち時間にする
    iii = mkIntervalZ();
  }
  else {               // stepNum 6 の時だけ残り時間を待ち時間にする
    iii = 60 - progressTime;
  }
  var ddd = new Date();
  ddd.setMinutes(ddd.getMinutes()+iii);
  var timestamp = Utilities.formatDate( ddd,'Asia/Tokyo', 'HH:mm:ss');
  setTriggerAfterZ(functionName, iii);
  addQueueZ( userId, timestamp, stepNum, progressTime + iii);
  sortQueueZ();
}

function execQueueTopZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,1,4);
  var vvv = range.getValues();
  Logger.log(vvv);
  var userId = vvv[0][0];
  var fireTime = vvv[0][1];
  var stepNum = vvv[0][2];
  var progressTime = vvv[0][3];
  delQueueTopZ();
  postMessageStepZ( userId, stepNum);
  if ( stepNum < 6) {
    setNextSchedule( userId, stepNum+1, progressTime);
  }
}

function sortQueueZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,10,4);
  range.sort(2); // range.sort({column: 2, ascending: true})
}

function delQueueTopZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,1,4);
  range.deleteCells(SpreadsheetApp.Dimension.ROWS);
}

function delQueueAllZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  var range = sheet.getRange(1,1,10,4);
  range.deleteCells(SpreadsheetApp.Dimension.ROWS);
}

function addQueueZ( userId, nextTime, stepNum, progressTime) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("queue");
  sheet.appendRow([userId, nextTime, stepNum, progressTime]);
}


function getTokenZ() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("secret");
  var token = sheet.getRange(1, 2).getValue();
  Logger.log( token);
  return token;
}

function logZ( uuu, xxx) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("log");
  var timestamp = Utilities.formatDate( new Date(),'Asia/Tokyo', 'HH:mm:ss');
  sheet.appendRow([timestamp, uuu, xxx]);
}

function postMessageZ( ttt, mmm) {
  logZ( ttt, mmm)
  const url = 'https://api.line.me/v2/bot/message/push';
  const payload = {
    to: ttt, 
    messages: [{
      type: 'text',
      text: mmm 
    }]
  };
  const params = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + getTokenZ()
    },
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch(url, params);
}

function randomSleepZ() {
  Utilities.sleep( Math.random()*10000); // ミリ秒、10000ミリ秒=10秒、0〜10秒未満のランダム値
}

function doPost(e) {
  let token = getTokenZ(); 
  let json = JSON.parse(e.postData.contents);
  let userId = json.events[0].source.userId;
  let userMessage = json.events[0].message.text;
  //postMessageZ( userId, "000");
 
  logZ( userId, userMessage);
  if ( userMessage.match(/開始/)) {
      postMessageStepZ( userId, 0);
      setNextSchedule( userId, 1, 0);
  }
  else {
    //logZ( userId, "ignore");
    postMessageZ( userId, "何?");   
  }
} 

諸々の設定

webhook のURLの設定や応答メッセージの設定、デプロイなどはまめ1号と同じことを設定しておきます。ここでは割愛します。
■■■■
まめ1号を再利用する場合は不要です。というかこの設定が意外に面倒で何度やっても1、2個設定し忘れしまい「動かない!」と慌てます。プログラムを変更しているので[デプロイを管理]で[新しいバージョン]はお忘れなく。

動作確認

ともだち登録すると自動応答のメッセージが返ってきます。LINEのデフォルトではなく実験用のメッセージになっていました。

ツンデレちゃんも健在です。

実験を開始すると

声がけと声がけの間の待ち時間について

このプログラムでは9分±2分にしています。

function mkIntervalZ() { 
  var rrr;
  rrr = Math.round(7 + Math.random() * 4); // 7分 + ランダムで0,1,2,3,4分
  Logger.log(rrr);
  return rrr;
}

7分にランダムで0〜4分を足すというプログラムにしています。Math.random()は0.0000〜0.9999を返す関数です。それを4倍すると0.0000〜3.9999になります。7を足すと 7.0000〜10.9999になります。それをMath.round() 関数で四捨五入すると、7〜11のランダムな値の出来上がりです。

なぜ9±2分にしたのか?

LINEからの「開始」のトリガーでスタートするのを +0分とすると、途中の5回の声がけの後、終了のお知らせで1時間の実験にしたいので、声がけの平均待ち時間は10分です。10±2分にしたらどうなるか?ランダムが最長の12分が偶然連続したとすると12分、12分、12分、12分、12分、12分、12分、で合計72分になってしまい1時間を大きくオーバーしてしまいます。
+0分  「では実験を始めてください」
+12分 1回目の声かけ「順調ですか?」
+24分 2回目の声かけ「その調子です。」
+36分 3回目の声かけ「よいペースです。」
+48分 4回目の声かけ「もう少しです。」
+60分 5回目の声かけ「頑張ってください。」
+72分 終了のお知らせ「お疲れ様でした。」

9±2分にしたらどうなるでしょう?最長の11分が偶然連続したとすると、11分、11分、11分、11分、11分、11分、11分、で合計66分になってしまいますが、最後の終了のお知らせまでの待ち時間を工夫し60分ちょうどにすることで辻褄を合わせればなんとかなりそうです。
+0分  「では実験を始めてください」
+11分 1回目の声かけ「順調ですか?」
+22分 2回目の声かけ「その調子です。」
+33分 3回目の声かけ「よいペースです。」
+44分 4回目の声かけ「もう少しです。」
+55分 5回目の声かけ「頑張ってください。」
+60分 終了のお知らせ「お疲れ様でした。」

逆にランダムが最短の7分が偶然にも連続したとすると、5回目の声がけから終了のお知らせまで25分も空いてしまうというカッコ悪さがありますが見なかったことにします。
+0分  「では実験を始めてください」
+7分 1回目の声かけ「順調ですか?」
+14分 2回目の声かけ「その調子です。」
+21分 3回目の声かけ「よいペースです。」
+28分 4回目の声かけ「もう少しです。」
+35分 5回目の声かけ「頑張ってください。」
+60分 終了のお知らせ「お疲れ様でした。」

もう少し工夫の余地はありそうですが、プログラムを複雑にしないためにこのくらいにしておきます。終了のお知らせで辻褄を合わせるというのはプログラムではこのようになっています。stepNumが6未満の時、つまり、5回目の声がけまでは mkIntervalZ() で待ち時間をランダムに生成し、stepNumが6のとき(プログラム上では else )は 60 - progressTime にしています。progressTime には今までの待ち時間の合計が入っています。

function setNextSchedule( userId, stepNum, progressTime) {
  const functionName = 'execQueueTopZ';
  var iii;
  if ( stepNum < 6) {  // stepNum 1,2,3,4,5 の時はランダムな待ち時間にする
    iii = mkIntervalZ();
  }
  else {               // stepNum 6 の時だけ残り時間を待ち時間にする
    iii = 60 - progressTime;
  }
  var ddd = new Date();
  ddd.setMinutes(ddd.getMinutes()+iii);
  var timestamp = Utilities.formatDate( ddd,'Asia/Tokyo', 'HH:mm:ss');
  setTriggerAfterZ(functionName, iii);
  addQueueZ( userId, timestamp, stepNum, progressTime + iii);
  sortQueueZ();
}

課題

「ちょうど1時間」にならないのはタイマーによるトリガーでプログラムを実行する時のタイムラグがあり、積み重なったからのようです。今回は約1時間なのでそこまで精度に拘らない事にします。

この記事が気に入ったらサポートをしてみませんか?