GASでlinebot

GASでlinebot

Line notify が廃止されるので代替を linebot にする必要がある

var SS = SpreadsheetApp.getActiveSpreadsheet(); //SpreadsheetのURL
var sheet = SS.getSheetByName("問い合わせ内容"); //Spreadsheetのシート名(タブ名)


var Trainingsheet= SS.getSheetByName('応答メッセージ');
var Settingsheet= SS.getSheetByName('設定');
var DBsheet= SS.getSheetByName('チャットボット用DB');
var keywordsheet= SS.getSheetByName('キーワード登録済み単語');


const CHANNEL_ACCESS_TOKEN = Settingsheet.getRange("C2").getValue();

//DBシート
var kensaku_words = DBsheet.getRange("A:A").getValues();
var kaitou_array = DBsheet.getRange("B:B").getValues();
var keyword_array = keywordsheet.getRange("A:A").getValues();

var kensaku_words1 = Array.prototype.concat.apply([], kensaku_words);
var kaitou_array1 = Array.prototype.concat.apply([], kaitou_array);
var keyword_array1 = Array.prototype.concat.apply([], keyword_array);




function doPost(request) {



 //POSTリクエストをJSONデータにパース
 const receiveJSON = JSON.parse(request.postData.contents);
 const event = receiveJSON.events[0];

var replytext =""


//検索ワードDBの何個目に送信されたメッセージが該当するか検索(ない場合は【-1】が帰ってきます。)
  j = kensaku_words1.indexOf(event.message.text) 


  if (j != -1.0){ // messageに、DBの文字列が含まれるか検索、含まれていたら
    replytext =  kaitou_array1[j];
    replyToUser(event.replyToken, replytext)  

    }else if(keyword_array1.indexOf(event.message.text)!= -1.0 ){
    replytext = "登録済応答メッセージ回答"
    }else{
    replytext =  keywordsheet.getRange(2,5).getValue();
    replyToUser(event.replyToken, replytext)  
    }

//送られてきたデータをlogシートに記載
sheet.appendRow([getCurrentTime(),event.message.text,replytext]);


}

function getCurrentTime() {
  //日付の宣言
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

の問題点として
問題点と改善点
1. CHANNEL_ACCESS_TOKEN を直接 Settingsheet から取得している
* 問題点:
* CHANNEL_ACCESS_TOKEN をスプレッドシートから取得するのは安全ではない(シートが漏洩した場合、外部から悪用される可能性)。
* 改善策:
* Google Apps Script のスクリプトプロパティ(PropertiesService)を使用する。
* Settingsheet.getRange(“C2”).getValue(); を PropertiesService.getScriptProperties().getProperty(“CHANNEL_ACCESS_TOKEN”) に変更。
2. kensaku_words1, kaitou_array1, keyword_array1 の取得方法
* 問題点:
* スプレッドシートのデータ全体を毎回読み込むのは非効率(特に行数が増えた場合、実行速度が遅くなる)。
* 改善策:
* 1回の getDataRange().getValues() で配列を取得し、flat() でフラット化する。
3. doPost のエラーハンドリングがない
* 問題点:
* 予期しないデータ(不正なJSON、nullなど)が来たときの処理がない。
* 改善策:
* try-catch を導入し、エラーが発生した場合のログ出力と適切なエラーレスポンスを返す。
4. event.message.text が undefined の場合の処理がない
* 問題点:
* event.message.text が undefined の場合、.indexOf() の実行時にエラーが発生する可能性がある。
* 改善策:
* event.message && event.message.text を確認して処理する。
5. replyToUser() の呼び出しが一部抜けている
* 問題点:
* keyword_array1.indexOf(event.message.text)!= -1.0 の場合、replyToUser() を呼んでいない。
* 改善策:
* replyToUser() を適切に呼び出す。

となっている

これを

var SS = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SS.getSheetByName("問い合わせ内容");
var Trainingsheet = SS.getSheetByName('応答メッセージ');
var Settingsheet = SS.getSheetByName('設定');
var DBsheet = SS.getSheetByName('チャットボット用DB');
var keywordsheet = SS.getSheetByName('キーワード登録済み単語');

// セキュアな方法でアクセストークンを取得
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");

// DBデータを一括取得
var kensaku_words1 = DBsheet.getDataRange().getValues().map(row => row[0]).flat();
var kaitou_array1 = DBsheet.getDataRange().getValues().map(row => row[1]).flat();
var keyword_array1 = keywordsheet.getDataRange().getValues().map(row => row[0]).flat();

function doPost(request) {
  try {
    // POSTデータをパース
    const receiveJSON = JSON.parse(request.postData.contents);

    // イベントがない、または不正な場合は処理しない
    if (!receiveJSON.events || receiveJSON.events.length === 0) {
      return ContentService.createTextOutput("No event").setMimeType(ContentService.MimeType.TEXT);
    }

    const event = receiveJSON.events[0];

    // メッセージがない、またはテキストがない場合は処理しない
    if (!event.message || !event.message.text) {
      return ContentService.createTextOutput("No message text").setMimeType(ContentService.MimeType.TEXT);
    }

    var replytext = "";

    // 検索ワードDBにメッセージがあるかチェック
    var j = kensaku_words1.indexOf(event.message.text);

    if (j !== -1) {
      // 返信メッセージを取得
      replytext = kaitou_array1[j];
    } else if (keyword_array1.includes(event.message.text)) {
      // 登録済みの応答メッセージ
      replytext = "登録済応答メッセージ回答";
    } else {
      // デフォルトメッセージを取得
      replytext = keywordsheet.getRange(2, 5).getValue();
    }

    // LINEに返信
    replyToUser(event.replyToken, replytext);

    // 送信データをスプレッドシートに記録
    sheet.appendRow([getCurrentTime(), event.message.text, replytext]);

    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);

  } catch (error) {
    Logger.log("Error in doPost: " + error.toString());
    return ContentService.createTextOutput("Error processing request").setMimeType(ContentService.MimeType.TEXT);
  }
}

// 現在の時間を取得
function getCurrentTime() {
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

// LINEに返信
function replyToUser(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var payload = {
    "replyToken": replyToken,
    "messages": [{ "type": "text", "text": message }]
  };

  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(url, options);
  } catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
  }
}

として保存

改善点まとめ
✅ アクセストークンを PropertiesService に保存し、スプレッドシートの流出リスクを防止。
✅ スプレッドシートのデータ取得を最適化し、getDataRange().getValues() で一括取得。

✅ エラーハンドリング (try-catch) を追加し、不正なリクエストの処理を安全に。

✅ イベントデータの存在確認 (event.message && event.message.text) を追加し、エラー防止。

✅ replyToUser() のエラーハンドリングを強化し、ログ出力を追加。

✅ keyword_array1.indexOf() の処理を includes() に変更し、可読性を向上。

✅ 関数を整理し、再利用しやすく。

Google Apps Script の スクリプトプロパティ (ScriptProperties) を使うと、スプレッドシートに機密情報を保存せずに管理できます。CHANNEL_ACCESS_TOKEN のような LINEのアクセストークン を安全に保存するのに適しています

スクリプトエディタで スクリプトプロパティ を設定するには、以下の手順を実行します。
① スクリプトプロパティにアクセストークンを保存
1. Google Apps Script エディタ を開く
* スプレッドシートの 拡張機能 → Apps Script を開く
2. エディタの左側メニュー で ⚙️プロジェクトの設定 を開く
3. 下の方にある スクリプトプロパティ の + プロパティを追加 をクリック
4. プロパティ名: CHANNEL_ACCESS_TOKEN
5. 値: <あなたのLINEアクセストークン> を入力して 保存 をクリック

ということなので
プロジェクトの設定をクリック
スクリプトプロパティの追加

プロパティ名: CHANNEL_ACCESS_TOKEN
値: <あなたのLINEアクセストークン> を入力して 保存

アクセストークンの値を一度削除して

var SS = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SS.getSheetByName("問い合わせ内容");
var Trainingsheet = SS.getSheetByName('応答メッセージ');
var Settingsheet = SS.getSheetByName('設定');
var DBsheet = SS.getSheetByName('チャットボット用DB');
var keywordsheet = SS.getSheetByName('キーワード登録済み単語');

// セキュアな方法でアクセストークンを取得
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");

// DBデータを一括取得
var kensaku_words1 = DBsheet.getDataRange().getValues().map(row => row[0]).flat();
var kaitou_array1 = DBsheet.getDataRange().getValues().map(row => row[1]).flat();
var keyword_array1 = keywordsheet.getDataRange().getValues().map(row => row[0]).flat();

function doPost(request) {
  try {
    // POSTデータをパース
    const receiveJSON = JSON.parse(request.postData.contents);

    // イベントがない、または不正な場合は処理しない
    if (!receiveJSON.events || receiveJSON.events.length === 0) {
      return ContentService.createTextOutput("No event").setMimeType(ContentService.MimeType.TEXT);
    }

    const event = receiveJSON.events[0];

    // メッセージがない、またはテキストがない場合は処理しない
    if (!event.message || !event.message.text) {
      return ContentService.createTextOutput("No message text").setMimeType(ContentService.MimeType.TEXT);
    }

    var replytext = "";

    // 検索ワードDBにメッセージがあるかチェック
    var j = kensaku_words1.indexOf(event.message.text);

    if (j !== -1) {
      // 返信メッセージを取得
      replytext = kaitou_array1[j];
    } else if (keyword_array1.includes(event.message.text)) {
      // 登録済みの応答メッセージ
      replytext = "登録済応答メッセージ回答";
    } else {
      // デフォルトメッセージを取得
      replytext = keywordsheet.getRange(2, 5).getValue();
    }

    // LINEに返信
    replyToUser(event.replyToken, replytext);

    // 送信データをスプレッドシートに記録
    sheet.appendRow([getCurrentTime(), event.message.text, replytext]);

    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);

  } catch (error) {
    Logger.log("Error in doPost: " + error.toString());
    return ContentService.createTextOutput("Error processing request").setMimeType(ContentService.MimeType.TEXT);
  }
}

// 現在の時間を取得
function getCurrentTime() {
  return Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

// LINEに返信
function replyToUser(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var payload = {
    "replyToken": replyToken,
    "messages": [{ "type": "text", "text": message }]
  };

  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    "payload": JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(url, options);
  } catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
  }
}

とコード変更

比べたらやはりURLが変わっている

この変更後のURLをlinebot の web hook に再度設定すれば動作する

次はDB関連の変更

function training() {
  
  var lastrow = Trainingsheet.getLastRow();
  var lastcolumn = Trainingsheet.getLastColumn();
  
  //Trainingシートのタイトル行除く全てのデータを取得
  // var Alldata  = Trainingsheet.getRange(2,2,lastrow-1,lastcolumn-1).getValues();
  var Alldata  = Trainingsheet.getRange(2,1,lastrow,lastcolumn).getValues();
  
  var DB_lastrow = DBsheet.getLastRow();
  var DB_lastcolumn = DBsheet.getLastColumn();
  
  //DBシートのデータ消去
  if(DB_lastrow!=2){
    DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn).clear();
  }
  
  var count = 0;
  
  for(var i = 0; i < Alldata.length; i++) {
    
    for(var j = 0; j < Alldata[i].length-1; j++){
      
      if(Alldata[i][j+1] != ""){
        DBsheet.getRange(3+count,1).setValue(Alldata[i][j+1]);//応答メッセージ
        
        DBsheet.getRange(3+count,2).setValue(Alldata[i][0]);//キーワード
        count++
          
      }
    }
  }
  
}

のコードレビュー

この training() 関数は、Trainingsheet のデータを DBsheet に転記する処理を行っていますが、以下の点で改善

### **コードレビュー & 改善点**

この `training()` 関数は、`Trainingsheet` のデータを `DBsheet` に転記する処理を行っていますが、以下の点で改善できます。

## **🔹 改善点と修正方法**
### **1. `DBsheet` のデータ消去処理のバグ**
#### **🔹 問題点**

if(DB_lastrow!=2){
  DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn).clear();
}

– **`DB_lastrow != 2` のチェックが不適切**:
– `DBsheet` が空の場合 (`lastRow == 0` など)、`DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn)` の範囲指定でエラーが発生する可能性がある。
– `lastRow < 3` の場合 (`DBsheet` に2行未満のデータしかない場合)、`DBsheet.getRange(3,1,DB_lastrow-2,DB_lastcolumn)` の範囲が **負の値になりエラー** になる。 #### **✅ 改善策** [shell] if(DB_lastrow > 2){ DBsheet.getRange(3, 1, DB_lastrow - 2, DB_lastcolumn).clear(); } [/shell] - **`DB_lastrow > 2` の場合のみクリアする** ことで **エラー防止**。

### **2. `getValues()` を `getValues()` + `setValues()` に最適化**
#### **🔹 問題点**

DBsheet.getRange(3+count,1).setValue(Alldata[i][j+1]);//応答メッセージ
DBsheet.getRange(3+count,2).setValue(Alldata[i][0]);//キーワード

– **`setValue()` をループ内で何回も呼んでいる**(パフォーマンスが悪い)。
– **Google Apps Script は I/O 操作が遅い** ため、`getValues()` と `setValues()` を使って **一括処理** する方が高速。

#### **✅ 改善策**
– **データを `配列` に格納して、最後に `setValues()` で一括書き込み** する。

### **3. `Alldata[i][j+1]` の条件チェックを厳密に**
#### **🔹 問題点**

if(Alldata[i][j+1] != ""){

– `Alldata[i][j+1]` が `undefined` の場合 **比較が正しく機能しない可能性がある**。
– `null` や `空白スペース (” “)` のみのデータも無視した方がよい。

#### **✅ 改善策**

if (Alldata[i][j+1] && Alldata[i][j+1].trim() !== "") {

– `&& Alldata[i][j+1].trim() !== “”` を追加して、 **空白のみの値も無視** する。

## **🔹 改善後のコード**

function training() {
  var lastrow = Trainingsheet.getLastRow();
  var lastcolumn = Trainingsheet.getLastColumn();
  
  // Trainingシートのタイトル行を除く全データ取得
  var Alldata = Trainingsheet.getRange(2, 1, lastrow - 1, lastcolumn).getValues();
  
  var DB_lastrow = DBsheet.getLastRow();
  var DB_lastcolumn = DBsheet.getLastColumn();

  // DBシートのデータをクリア(タイトル行以降を削除)
  if (DB_lastrow > 2) {
    DBsheet.getRange(3, 1, DB_lastrow - 2, DB_lastcolumn).clear();
  }
  
  var dataToWrite = [];
  
  for (var i = 0; i < Alldata.length; i++) {
    for (var j = 0; j < Alldata[i].length - 1; j++) {
      if (Alldata[i][j+1] && Alldata[i][j+1].trim() !== "") {
        // データを配列に追加
        dataToWrite.push([Alldata[i][j+1], Alldata[i][0]]);
      }
    }
  }
  
  // まとめてDBシートに書き込み
  if (dataToWrite.length > 0) {
    DBsheet.getRange(3, 1, dataToWrite.length, 2).setValues(dataToWrite);
  }
}

## **🔹 改善点まとめ**
✅ **エラー防止:** `DB_lastrow > 2` のチェックを修正し、**負の範囲エラーを回避**
✅ **高速化:** `setValue()` の代わりに **配列にデータを溜めて `setValues()` で一括書き込み**
✅ **データチェックの厳密化:** `Alldata[i][j+1] && Alldata[i][j+1].trim() !== “”` で **空白や `undefined` を無視**
✅ **リファクタリング:** 変数名やロジックを **シンプルかつ分かりやすく** 変更

この修正により、処理速度と信頼性が向上し、エラーのリスクを減らせます!🚀

これを実行し再度デプロイ
URLが変更されるのでwebhookへ再度設定する

次に
replytouser.gs のコードレビュー

/**
 * 該当ユーザーへの
 * @param {*} replyToken 
 * @param {*} text 
 */
function replyToUser(replyToken, text) {

    const replyText = {
        "replyToken": replyToken,
        "messages": [{
            "type": "text",
            "text": text,
        }]
    }

    const options = {
        "method": "post",
        "headers":
        {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
        },
        "payload": JSON.stringify(replyText)
    };

    // Line該当ユーザーに応答を返している
    UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
}

この replyToUser() 関数は、LINE Bot を使って特定のユーザーに応答メッセージを送る処理を行っています。
基本的な構造は問題ありませんが、以下の改善点があり
### **コードレビュー & 改善点**

この `replyToUser()` 関数は、LINE Bot を使って特定のユーザーに応答メッセージを送る処理を行っています。
基本的な構造は問題ありませんが、以下の改善点があります。

## **🔹 改善点と修正方法**

### **1. `CHANNEL_ACCESS_TOKEN` の存在チェック**
#### **🔹 問題点**

"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,

– **`CHANNEL_ACCESS_TOKEN` が `null` または `undefined` の場合、エラーになる可能性がある**。
– もし `CHANNEL_ACCESS_TOKEN` の取得に失敗した場合、LINE API へのリクエストが **無駄に送信されてしまう**。

#### **✅ 改善策**
– `CHANNEL_ACCESS_TOKEN` の **存在を事前にチェック** し、エラーが発生する前に処理を停止する。

if (!CHANNEL_ACCESS_TOKEN) {
    throw new Error("CHANNEL_ACCESS_TOKEN が設定されていません");
}

### **2. `UrlFetchApp.fetch()` のエラーハンドリング**
#### **🔹 問題点**
– `UrlFetchApp.fetch()` で **LINE API へのリクエストが失敗してもエラーログが記録されない**。
– 例えば、ネットワークエラーや無効な `replyToken` の場合、原因が分からない。

#### **✅ 改善策**
– `try-catch` を使用してエラー発生時に `Logger.log()` でエラーメッセージを記録する。

try {
    const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
    Logger.log("LINE API Response: " + response.getContentText());
} catch (error) {
    Logger.log("Error in replyToUser: " + error.toString());
}

### **3. LINE API のレスポンスをチェック**
#### **🔹 問題点**
– `UrlFetchApp.fetch()` のレスポンスを **無視している**。
– **エラーメッセージやステータスコードを確認しないと、何が問題か分からない**。

#### **✅ 改善策**
– `response.getResponseCode()` を使って **200(成功)以外の場合にログを記録する**。

const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
const responseCode = response.getResponseCode();

if (responseCode !== 200) {
    Logger.log("LINE API Error: " + response.getContentText());
}

### **4. コメントの改善**
#### **🔹 問題点**
– 関数の説明コメントが **途中で途切れている**。
– `@param` の説明が不十分(`replyToken` の説明がない)。
– **関数が何をするのか明確に記述すべき**。

#### **✅ 改善策**
– **関数の説明を具体化** し、`@param` も明確に。

/**
 * LINE Bot で特定のユーザーにメッセージを送信する関数
 *
 * @param {string} replyToken - LINEから受け取った返信用トークン
 * @param {string} text - 送信するメッセージ内容
 */

## **🔹 改善後のコード**

/**
 * LINE Bot で特定のユーザーにメッセージを送信する関数
 *
 * @param {string} replyToken - LINEから受け取った返信用トークン
 * @param {string} text - 送信するメッセージ内容
 */
function replyToUser(replyToken, text) {
    // CHANNEL_ACCESS_TOKEN が設定されているか確認
    if (!CHANNEL_ACCESS_TOKEN) {
        throw new Error("CHANNEL_ACCESS_TOKEN が設定されていません");
    }

    const replyText = {
        "replyToken": replyToken,
        "messages": [{
            "type": "text",
            "text": text,
        }]
    };

    const options = {
        "method": "post",
        "headers": {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
        },
        "payload": JSON.stringify(replyText)
    };

    try {
        // LINE API へリクエストを送信
        const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
        const responseCode = response.getResponseCode();

        // レスポンスをログに記録
        if (responseCode === 200) {
            Logger.log("LINE API Response: " + response.getContentText());
        } else {
            Logger.log("LINE API Error: " + response.getContentText());
        }
    } catch (error) {
        // エラーハンドリング
        Logger.log("Error in replyToUser: " + error.toString());
    }
}

## **🔹 改善点まとめ**
✅ **`CHANNEL_ACCESS_TOKEN` が `null` の場合にエラーをスロー**(事前チェック)
✅ **`try-catch` を追加し、`UrlFetchApp.fetch()` のエラーハンドリングを強化**
✅ **`response.getResponseCode()` をチェックし、API のレスポンスを記録**
✅ **コメント (`@param` の説明) を改善し、関数の目的を明確化**

この修正により、**エラーハンドリングが強化され、問題が発生したときにすぐ原因を特定できるようになります!** 🚀

再度デプロイして
webhookのURL更新

これでlinebot の動作確認できたので
過去に通知に line notify を使っていたものを調べて
Linebot に変える

リポジトリは
https://github.com/Snowpooll
にあるので

https://github.com/Snowpooll/store_adversting_lists
gmailで件名を指定し、未読の最新のメールを取得後にURLを抽出、
抽出したURLを元にブラウザを開き画像をダウンロード
ダウンロード画像へcloud vision api を実行し
購入リストにマッチしたものを LINE norifyで買い物リストが送信

https://github.com/Snowpooll/inventory_count
yoov8と学習したモデルで 指定した画像から物体検出をして在庫数が1以下になったときに
LINE norifyで買い物リストが送信
検出時の結果はsqlite3データベースに保存

とりあえずこの2つの機能を変えることにする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です