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つの機能を変えることにする