@sandoraki (John)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

LINEで使えるアラームbotに関する質問

解決したいこと

LINEbotの作成に際して様々な資料を自分で読みながら自分で進めていたのですが、
行き詰ったところがあり、他の方の意見を聞いてみたいと思いました。

どういうbotなのかを簡単に言うと、
・LINEからコマンドメッセージを受け取る。
・そのコマンドから送り主が指定した「年月月日時分、メッセージ」情報を抜き出して、
その時間にそのメッセージを送るという処理が実行されるトリガーを作成する。
というものです。
例として、
「$2022$05$14$17$30$会議」というコマンドメッセージを受け取ると、
「2022年の5月14日17時30分に『会議』というメッセージをコマンドメッセージが送られたトークに送る」というトリガーを設定してくれるということです。
※実際のコマンドでは「$」は半角ですが、Qiitaに書き込む都合上全角にしてます。

発生している問題・エラー

コード自体は試行錯誤を重ねながら大体書き終わったのですが、実際にLINEからメッセージを送っても何も反応がないという状況です。

全体ソースコード

//LINEからメッセージを取得時に実行
function doPost(e) {

  const LINE_TOKEN = '--------------'; 
  const LINE_URL = 'https://api.line.me/v2/bot/message/reply';

  //LINEからのメッセージ、送信元のトークンを取得
  var json = JSON.parse(e.postData.contents);
  var reply_token = json.events[0].reply_token;
  if (typeof reply_token === 'undefined') {
    return;
  }
  var alarm_usermessage = json.events[0].message.text;
  var body = 'アラーム設定が完了しました。';

  //年月日時、用件情報を各変数に格納(コマンドは”$”で区切る)
  for(var i = 0 ; i < 4 ; i++){
      switch(i){

        //「年」の格納
        case 0:
        if(alarm_usermessage.match(/^\$\d/)){
          var alarm_year = alarm_usermessage.substr(1, 4);
          alarm_usermessage = alarm_usermessage.substr(5);
        }else{
          body = 'コマンド形式が正しくありません。';
        }
        break;

        //「月」の格納
        case 1:
        if(alarm_usermessage.match(/^\$\d/)){
          var alarm_month = alarm_usermessage.substr(1 ,2)
          alarm_usermessage = alarm_usermessage.substr(3);
        }else{
          body = 'コマンド形式が正しくありません。';
        }
        break;

        //「日」の格納
        case 2:
        if(alarm_usermessage.match(/^\$\d/)){
          var alarm_day = alarm_usermessage.substr(1 ,2)
          alarm_usermessage = alarm_usermessage.substr(3);
        }else{
          body = 'コマンド形式が正しくありません。';
        }
        break;

        //「時」の格納
        case 3:
        if(alarm_usermessage.match(/^\$\d/)){
          var alarm_hours = alarm_usermessage.substr(1 ,2)
          alarm_usermessage = alarm_usermessage.substr(3);
        }else{
          body = 'コマンド形式が正しくありません。';
        }
        break;

        //「分」の格納
        case 4:
        if(alarm_usermessage.match(/^\$\d/)){
          var alarm_minutes = alarm_usermessage.substr(1 ,2)
          alarm_usermessage = alarm_usermessage.substr(4);
        }else{
          body = 'コマンド形式が正しくありません。';
        }
        break;
      }
    }

    //確認メッセージの送信
    sendmessage(reply_token, body);

    //トリガーセット時に使う「at(○○)」部分
    var alarm_time = new Date();
    alarm_time.setFullYear(alarm_year);
    alarm_time.setMonth(alarm_month - 1);
    alarm_time.setDate(alarm_day);
    alarm_time.setHours(alarm_hours);
    alarm_time.setMinutes(alarm_minutes);
    
    //トリガーセット
    if(body === 'アラーム設定が完了しました。'){
      ScriptApp.newTrigger('sendmessage(reply_token, alarm_usermessage)').timeBased().at(alarm_time).create(); 
    }else{
      return;
    }
}


//メッセージ送信関数
function sendmessage(token, message) {

   //メッセージを返信
  UrlFetchApp.fetch(LINE_URL, {
    'headers':{
      'Content-Type':'application/json; charset=UTF-8',
      'Authorization':'Bearer '+ LINE_TOKEN,
    },
    'method':'post',
    'payload':JSON.stringify({
      'replyToken':token,
      'messages':[{
        'type':'text',
        'text':message,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content':'post ok'})).setMimeType(ContentService.MimeType.JSON);


}

処理の流れ

・LINEからメッセージを取得すると、「doPost」関数が実行される。
・定数にbotのチャネルアクセストークンとLINEURLを各々格納する。
・変数に取得したメッセージを格納する。

・for文とswitch文とif文を使って、時間関連の各変数にコマンドからの情報を格納していく。
※この時コマンド形式がおかしかったのなら適宜処理を行う。
・for文の処理が終わったら、アラーム設定が完了したかもしくはコマンドエラーが発生したかどうかの確認メッセージを送信する。
→定義しておいた「sendmessage」関数に、「宛先を指定する引数」と「メッセージ内容を指定する引数」を渡して実行する。確認メッセージではfor文の途中でエラーが発生しない限りは「アラーム設定が完了しました。」となっていて、エラーが発生していた場合は「コマンド形式が正しくありません。」に置き換わっている。

・トリガーをセットする際に「at(○○)」の部分に使う変数を「new Date();」で定義して、先のfor×switch×ifで手に入った時間関連情報を各々セットしていく。

・if文で「もしエラーが発生していないのなら」という条件で、trueの場合にトリガーをセットするというようにする。

確信が持てていない点

・トリガーをセットする「ScriptApp.newTrigger...」の際に、参考にしていたサイトなどではここまで引数を使用していなかったためnewTriggerの後の()の中にコードのような感じに引数も含めて書いていいのか。

・参考にしたサイトの中に「関数として定義したsetTriggerを、トリガーに組み込みたい処理を書く関数(私のコードで言うならsendmessage関数)の一番上の部分に書いておく」と記述されたものがあったので、もしかしたらそれが出来ない原因?と思いつつも、その場合引数の渡しが複雑になりそうだからどうなのかな。

最後に

コード自体のエラーはないと思(って)います。
ただこんないろんなことを詰め込んだものは探しても出てこなかったり、LINEbotの記事自体が古いものが多かったりと色んな資料を読み漁っているもののわからないことだらけという状況です。
周りにこんな細かいことを聞けるような人もおらず、この場に投げかけてみました。
ここまで読んでくださった方は、長文なのにも関わらず読んでいただきありがとうございました。
何か知っていること、気になったこと、間違っていることなどありましたらコメントよろしくお願いします。

0 likes

1Answer

linebotに関しては知らないのですが、GASは自分も勉強中なのでトリガー部分のみ書いてみました。10秒後にパラメータ付きでsendmessageを起動するサンプルです。新しいプロジェクトにコピペし実行してみて下さい。動作状況は左メニューの「実行数」項目から見る事ができます。

トリガーのサンプル
test.gs
// スクリプトプロパティサービスを利用する
const ScriptProps = PropertiesService.getScriptProperties();

function doPost(e) {
  // テストパラメータ
  const message = 'テストメッセージ';
  const reply_token = 'token_' + Math.random();
  
  // トリガー生成 10秒後に起動
  const trigger = ScriptApp.newTrigger('sendmessage').timeBased().after(10000).create();
  const uid = trigger.getUniqueId();
  
  // スクリプトプロパティにパラメータを格納
  const data = { token: reply_token, message };
  ScriptProps.setProperty(uid, JSON.stringify(data));

  console.log(`トリガー(${uid})が設定されました`, data);
  return ContentService.createTextOutput(JSON.stringify({'content':'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

//メッセージ送信関数
function sendmessage(tevent) {
  // トリガーID
  const uid = tevent.triggerUid;
  // スクリプトプロパティからパラメータ取得
  const data = JSON.parse(ScriptProps.getProperty(uid) || '{}');

  console.log(`トリガー(${uid})からsendmessageが起動されました`, data);

  // パラメータ削除
  ScriptProps.deleteProperty(uid);
  // トリガーを削除
  for( const trg of ScriptApp.getProjectTriggers() ) {
    if( trg.getUniqueId() === uid ) {
      ScriptApp.deleteTrigger(trg);
      break;
    }
  }

  // メッセージ送信処理...
}



()の中にコードのような感じに引数も含めて書いていいのか。

恐らく、ダメです。そのように書くと関数の呼び出しに失敗したというエラーが出ているはずです。調べた限りトリガーに引数を渡す方法は用意されていないようなので、渡したいデータがある場合はどこかに保持しておく必要があります。検索すると出てくる最も簡単な方法は大きく以下の二つで、

  • スクリプトプロパティに記録する
  • 適当なスプレッドシートを用意してデータベース代わりにする

このコードでは一番手軽な前者のスクリプトプロパティを利用しています(本来こういう使いかたをするためのものではないっぽいですが)


※追記:

LINE Botのサンプル
test.gs
// LINE情報
const LINE_TOKEN = '-'; 
const LINE_REPLY = 'https://api.line.me/v2/bot/message/reply';
const LINE_PUSH = 'https://api.line.me/v2/bot/message/push';

// スクリプトプロパティサービスを利用する
const ScriptProps = PropertiesService.getScriptProperties();

function doPost(e) {
  const json = JSON.parse(e?.postData?.contents || '{}');
  const dat = json?.events?.[0];
  //console.log(dat);

  const reply_token = dat?.replyToken;
  if (typeof reply_token === 'undefined') {
    console.log('reply_tokenがありません');
    return;
  }
  const groupId = dat?.source?.groupId;
  const userId = dat?.source?.userId;
  if( !userId ) {
    console.log('userIdがありません');
    return;
  }

  // 日付とメッセージ展開
  const alarm_usermessage = dat?.message?.text;
  const match = /^\$(\d+)\$(\d+)\$(\d+)\$(\d+)\$(\d+)\$(.+)/.exec(alarm_usermessage);
  if( !match ) {
    console.log('不明なメッセージです', alarm_usermessage);
    return;
  }
  const [,y, m, d, hour, min, message] = match;
  const alarm_time = new Date(y, m - 1, d, hour, min);
  
  // トリガー生成
  const trigger = ScriptApp.newTrigger('sendmessage').timeBased().at(alarm_time).create();
  const uid = trigger.getUniqueId();
  
  // スクリプトプロパティにパラメータを格納
  const data = { to: groupId || userId, token: reply_token, message };
  ScriptProps.setProperty(uid, JSON.stringify(data));

  console.log('トリガー設定', alarm_time, data);

  // 設定完了メッセージ
  UrlFetchApp.fetch(LINE_REPLY, {
    'headers':{
      'Content-Type':'application/json; charset=UTF-8',
      'Authorization':'Bearer '+ LINE_TOKEN,
    },
    'method':'post',
    'payload':JSON.stringify({
      'replyToken': reply_token,
      'messages':[{
        'type': 'text',
        'text': `[${y+"/"+m+"/"+d+" "+hour+":"+min}]トリガーが設定されました`,
      }],
    }),
  });
}

//メッセージ送信関数
function sendmessage(tevent) {
  // トリガーID
  const uid = tevent.triggerUid;
  // スクリプトプロパティからパラメータ取得
  const data = JSON.parse(ScriptProps.getProperty(uid) || '{}');

  console.log(`トリガー(${uid})からsendmessageが起動されました`, data);

  // パラメータ削除
  ScriptProps.deleteProperty(uid);
  // トリガーを削除
  for( const trg of ScriptApp.getProjectTriggers() ) {
    if( trg.getUniqueId() === uid ) {
      ScriptApp.deleteTrigger(trg);
      break;
    }
  }

  // メッセージ送信処理...
  UrlFetchApp.fetch(LINE_PUSH, {
    'headers':{
      'Content-Type':'application/json; charset=UTF-8',
      'Authorization':'Bearer '+ LINE_TOKEN,
    },
    'method':'post',
    'payload':JSON.stringify({
      'to': data.to,
      'messages':[{
        'type': 'text',
        'text': data.message,
      }],
    }),
  });
}

0Like

Comments

  1. @sandoraki

    Questioner

    返信ありがとうございます。
    「var json = JSON.parse(e.postData.contents);」をdoPost関数内の一番上に書いたうえで、「const reply_token = 'token_' + Math.random();」の=の右側を「 json.events[0].reply_token;」にして実際にLINEからメッセージを送ってみたのですがトリガーは作成されたもののメッセージは帰ってきませんでした。
    このコードにはチャネルアクセストークンを書いている場所がないのですが、そういうのがいらないコードなんでしょうか?
    それとトリガーは作成されるのにメッセージは帰ってこず、その上トリガーが続けて作成されていってます。
    私自身スクリプトプロパティは使ったことがないのでまだわかってない部分が多いので自分じゃわからないことだらけなので良ければ教えていただけると幸いです。
  2. 解り難くてすみません、これは引数付きのトリガーを生成するだけのサンプルコードなのでLINEに関係するコードは省いてあります。

    LINEに返信する場合は doPost の「// テストパラメータ」部分を正しくリクエストから取得するように編集し、sendmessage 内の「// メッセージ送信処理」以降に"UrlFetchApp.fetch(LINE_URL..."の処理を追加すればよいのではないかと思います。token と message はそれぞれ data.token, data.message で取得できるようになってるはずなので該当部分を書き換えて下さい。
  3. @sandoraki

    Questioner

    そうなんですね、こちらこそ分からず申し訳ないです。
    それでもう一度試してみます!
  4. 実際に私も作ってみたものを追記したのでご覧ください。
  5. @sandoraki

    Questioner

    拝見しました!
    非常に助かります。
    こちらのコードは実際に上手くいったということでしょうか?
  6. すいません少し修正しました。とりあえずテストした限りは動いています。
    疑問点があれば言ってください。
  7. @sandoraki

    Questioner

    こちらでGASにコピペして、アクセストークンを変えてやってみたのですが、
    「$YYYY$MM$DD$HH$MM$----」の形式で送信してみたところ、「トリガーが設定されました」というメッセージは帰ってきました。ただ適当なメッセージを送ったときに「不明なメッセージです」が帰って来ることはありませんでした。
    それとトリガーは作成されていたものの定時になってもメッセージは届きませんでした。
    テストしたというのが僕と同じ条件下なのであれば僕のやり方が間違っていると思うので、何か間違っているとこがあれば教えていただきたいです。
  8. すみません再度修正しました><
    こちらはbotをグループに入れた状態で試していたのですが、一対一の場合だと動作してませんでした。"to"に入れたユーザーIDの値が配列なのがダメだったようです。
  9. 「不明なメッセージです」などは console.log でサーバのログへ出力しているため、LINEには表示されません。これは単にトリガー生成が成功した時のみ返事を返すようにしたからですが、もし必ず返事を返すなら

    if( !match ) { 失敗時の処理(returnはしない) } else { 成功時の処理(トリガ生成等) }

    // 設定完了メッセージ
    UrlFetchApp.fetch(LINE_REPLY, {
    ...
    'text': '適切な返事',
    ...
    })

    のように書けば良いと思います
  10. @sandoraki

    Questioner

    試してみたところちゃんと定時にきました!
    聞きたいことがいくつかあるのですが、
    ・このコードでconsole.logはどういう役割なのか?
    ・コマンドが正しくない場合のメッセージが返ってこないのはなぜなのか?
    ・ここまでのGAS,スクリプトプロパティについてどこで学んだのか、参考になるような書籍があるのか
    聞いてばっかりですみません><
    でも自分でずっと行き詰まってて実際に一つの結果として成功したのを見てほんとにすごいと思ったので、出来れば教えていただきたいです…!
  11. @sandoraki

    Questioner

    すれ違いになってしまいました💦
    やってみます!
  12. GASの console.log の出力は、ダッシュボードから直接実行するかサイドメニューの「実行数」の項目をクリックするなどで見る事ができます。デバッグのためのものなので、あってもなくても動作は変わりません。

    自分はGASは初心者ですしLINE Botを触るのも初めてでしたので、まず検索してヒットした初心者向け情報を参考に、APIの仕様を詳しく知る必要がある部分についてはリファレンスで確認するという事を繰り返して書きました。

    GASのリファレンス
    https://developers.google.com/apps-script/reference/script/script-app
    LINE Messaging APIのリファレンス
    https://developers.line.biz/ja/reference/messaging-api/#messages

    ただリファレンスというものは使用言語についてある程度知識がないと読みづらいものなので、まずJavaScript自体をじっくり学んで慣れるのがいいかも知れないです。
  13. @sandoraki

    Questioner

    ありがとうございます!
    丁寧な対応で本当に助かりました。
  14. @sandoraki

    Questioner

    聞きたいことができたのでまたコメントさせていただきました。

    sendmessage関数の引数tevevtがあるのですが、
    どの部分でsendmessage関数に引数を渡してるのでしょうか?

    あと
    // スクリプトプロパティからパラメータ取得
    const data = JSON.parse(ScriptProps.getProperty(uid) || '{}');
    //console.log(`トリガー(${uid})からsendmessageが起動されました`, data);
    の部分はトリガー設定時に結び付けておいたスクリプトプロパティをsendmessage関数内のこの部分で呼び出すことによって、dataにパラメータが渡されてdata.○○でデータを呼び出せるようになったということで大丈夫なんでしょうか?
  15. >sendmessage関数の引数tevevt
    これはトリガー起動時に自動的にシステムから渡されるイベントオブジェクトです。これについて自分はどこかのサンプルコードで見てそのまま流用したのですが、ドキュメントを確認するとちゃんと書いてありますね。
    https://developers.google.com/apps-script/guides/triggers/events


    >スクリプトプロパティ
    その通りで大丈夫です。

Your answer might help someone💌