見出し画像

【GAS】時限トリガー&応答型のLINE Botを作ってみた (2)コードの内容

はじめに

前回( ..)φメモしたLINE Botのコード全体の備忘メモです。

役割ごとに4つのgsファイルに分けて書いていますが、全部同じシートに書いても動きます。

最初に04.globalのプロパティストアの設定をする必要があります。

【2022/6. 手直し後のコードに変更しました(キーワードシートはなくし、マスタシート中の6列のいずれかの文字列の部分一致からその行(に当たる配列要素)をフィルタする仕様に変更】

gsファイル「01.setInfoInMaster.gs」の中身

gmailからタイトル基準でメッセージを抽出し、各メッセージから日付とyoutubeリンクを抽出して配列に格納し、マスターシートに書き込んでいます。(MasterSheetはgsファイル「04.global.gs」内で定義しています。)

なお、gmailの各emailには通常messageという変数をあてるのが一般的かもしれませんが、後でLINE APIを使うときにもmessageが登場するので、混同しないようにemailという変数をあてています。

/**
 * Gmailの特定タイトルのメッセージ群から日付とyoutubeリンクを抽出して二次元配列で返す関数
 * @return {Array.<Array.<Object, string>>} arrDatesAndLinks - [日付,リンク」の2要素からなる一次元配列を要素とする二次元配列
 * TODO:配列の値の型が2つある(orではなくてandの関係で)場合、どう書くのか確認すること!
 */
function getInfoFromEmails() {
  const titleToSearch = 'subject:筋トレ部へのご参加、ありがとうございました';
  const threads = GmailApp.search(titleToSearch);
  const emails = threads.map(thread => thread.getMessages()).flat(); //全スレッド中のメールを取り出してから一次元化

  const arrDatesAndLinks = [];

  emails.forEach(email => {
    const emailDate = email.getDate();
    const emailBody = email.getPlainBody();

    const strToExtract = /https?:\/\/youtu[^<]*/g;
    const arrYoutubeLink = emailBody.match(strToExtract); //NOTE:メール本文から抽出したyoutubeリンクの配列(マッチがゼロ個の時は配列ではなくnull)

    if (!arrYoutubeLink) { return; } //NOTE:matchはゼロ個の時は空の配列ではなくnullを返す。空の配列はtruthyだがnullはfalsyなのでbooleanで判別可能
    if (arrYoutubeLink.length > 1) { throw 'youtubeリンクが2つ以上あります'; }

    arrDatesAndLinks.push([emailDate, arrYoutubeLink[0]]);
  });

  return arrDatesAndLinks;
}

/** Gmailから抽出した日付とyoutubeリンクの二次元配列を「マスタ」シートに出力する */
function setInfoInMaster() {
  const arrDatesAndLinks = getInfoFromEmails();
  console.log(arrDatesAndLinks);
  const dateColRange = MasterSheet.getRange('A:A');
  dateColRange.setNumberFormat("yyyy/MM/dd"); //NOTE:日付の貼り付け先の表示形式を設定

  const rangeToSetData = MasterSheet.getRange(2, 1, arrDatesAndLinks.length, arrDatesAndLinks[0].length);
  rangeToSetData.setValues(arrDatesAndLinks);
}

gsファイル「02.sentPresetMessage.gs」の中身

スプレッドシートに定めた定型メッセージをLINE APIを通じて送信します。(PresetMessageSheet、BroadcastURL、ACCESS_TOKENはgsファイル「04.global.gs」内で定義しています。)

Apps Scriptの画面の左端のエディタマークの下の時計マークから、sendPresetMessage関数に時限トリガーを設定しているので、その時間になるとLINE Botのお友達に「定期メッセージ」シートの内容が送信されます。

getPresetMessage関数は、ユーザー入力にキーワードが含まれなかった場合の返信にも利用しています。

/** 定型文(筋トレのお誘い)を「定型メッセージ」シートから取得する
 * @return {string} message
 */
function getPresetMessage() {
  const message = PresetMessageSheet.getRange(1, 1, 1, 1).getValue();
  return message;
}

/** 定型文を日次トリガーでBotのLINEのお友達に送信する */
function sendPresetMessage() {
  const url = BroadcastUrl; //NOTE:友達登録した全員に送る場合のurl。cf. PushUrlは既知のLINE IDに送る場合
  const message = getPresetMessage();
  const payload = {
    messages: [{ type: 'text', text: message }],
  };
  const params = {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + ACCESS_TOKEN },
    payload: JSON.stringify(payload),
  };
  UrlFetchApp.fetch(url, params);
}

gsファイル「03.replyWithSelectedMessage.gs」の中身

LINE BOTアプリのユーザーから入力があると、マスタシート内を検索し、該当列の情報を返信します。検索対象が6列あり、そのどの列でも、部分一致があればヒットさせ(「腹筋」と入力があったとすると、6列のいずれかに「足上げ腹筋」や「V字腹筋」などがある行をすべてfilterする)、その中からランダムで1行分の情報を返信します。

/**
 * LINE Messaging APIからユーザー反応のPOSTを受けると起動し、パースしたユーザー反応をreplyWithSelectedMessage関数に渡す
 * @param {Object} e - Content service TextOutput object * 
 */
function doPost(e) {
  if (typeof e === 'undefined') { sendPresetMessage(); } //NOTE:eがundefinedになるのはサーバーやネットの不具合の場合
  const json = JSON.parse(e.postData.contents);
  replyWithSelectedMessage(json);
}

/**
 * ユーザー入力文字列をスプシ内を検索し、部分一致があったレコードからランダムで1要素をLINEで返信する
 * @param {Object} json - parse済みのe.postData.contents
 */
function replyWithSelectedMessage(json) {
  const userInput = json.events[0].message.text;

  const replyMessageRange = MasterSheet.getRange(2, 2, MasterSheet.getLastRow(), MasterSheet.getLastColumn());
  const allRecs = replyMessageRange.getValues();

  //↓各レコード内のセルの値(想定としてはC列~H列の文字列)とユーザー入力が1つでも部分一致したら
  //↓そのレコードを配列recsInclUserInputに追加。例:ユーザー入力が「腹筋」の場合、「V字腹筋」や「脚上げ腹筋」が含まれるレコードがfilterされる

  const recsInclUserInput = allRecs.filter(record => record.some(cellVal => cellVal.includes(userInput)));

  if (recsInclUserInput.length === 0) {

    const message = getPresetMessage();

    reply(json, message);
  }

  const selectedIndex = Math.floor(Math.random() * (recsInclUserInput.length)); //NOTE:ランダムにインデックスを選定
  const valsToSend = recsInclUserInput[selectedIndex];

  const message = `あなたにオススメの動画はこちら!\r${valsToSend.join('\r')}`;

  reply(json, message);
}

/**
 * 引数を元にbotから返信する
 * @param {Object} json - parse済みのe.postData.contents
 * @param {string} message
 */
function reply(json, message) {
  const replyUrl = ReplyUrl;
  const replyToken = json.events[0].replyToken; //NOTE:WebHookで受信する応答用Tokenなので設定不要
  const headers = {
    'Content-Type': 'application/json; charset=UTF-8',
    'Authorization': 'Bearer ' + ACCESS_TOKEN,
  };
  const postData = {
    'replyToken': replyToken,
    'messages': [{ 'type': 'text', 'text': message }],
  };
  const options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(postData),
  };
  UrlFetchApp.fetch(replyUrl, options);
}

gsファイル「04.global.gs」の中身

全体で使用する変数・定数をまとめています。

/** LINE Channel Access Token */
const ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_CHANNEL_ACCESS_TOKEN_KINTOREKUNver3');

/** LINE User ID */
const LINE_USER_ID = PropertiesService.getUserProperties().getProperty('LINE_USER_ID');

/** LINE Broadcast Url */
const BroadcastUrl = 'https://api.line.me/v2/bot/message/broadcast';

/** LINE Push Url このアプリでは不使用 特定のLINE_IDに送信する時に使用 */ 
const PushUrl = 'https://api.line.me/v2/bot/message/push'

/** LINE ReplyUrl */
const ReplyUrl = 'https://api.line.me/v2/bot/message/reply'

/** スプレッドシート(アクティブ=コンテナ) */
const SS = SpreadsheetApp.getActiveSpreadsheet();

/** 「マスタ」シート */
const MasterSheet = SS.getSheetByName('マスタ');

/** 「定型メッセージ」シート */
const PresetMessageSheet = SS.getSheetByName('定型メッセージ');

なお、はじめにも書いたように、事前に以下のsetUserProperty関数を実行して、ACCESS_TOKENをスクリプトプロパティに、また、LINE_USER_IDをユーザープロパティに格納しています。個人情報をコード内にベタ書きしてある状態より、コピペなどで流出するリスクが低くなります。

function setUsertProperty() {
  PropertiesService.getScriptProperties().setProperty('LINE_CHANNEL_ACCESS_TOKEN_KINTOREKUNver3', 'ここにLINEチャンネルアクセストークンを入力');
    PropertiesService.getUserProperties().setProperty('LINE_USER_ID', 'ここにLINE USER IDを入力');

}

プロパティストアについてはこちらを参考にさせていただきました。ありがとうございました。


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