はじめに
公式lineを作って、追加したユーザごとのスプレッドシートを提供したいと思った時に1つずつ作るのは大変なので、自動でそれぞれのユーザごとのスプレッドシートを作ってもらうためのものです
この記事について
この記事ではLineBot + Gasで説明します。
この記事で説明すること、説明しないことは以下の通りです。
説明すること
- ユーザごとのスプレッドシートの作り方
説明しないこと(簡単には説明します)
- 公式lineの作り方
- gasとの連携方法
手順
- 公式Lineの作成(詳細省略)
- Gasにデプロイ(詳細省略)
- マスターファイルの作成
- コードの記述
⚠️公式Lineの作成、Gasとの連携については詳細は他の記事を参考にしてください⚠️
1. 公式Lineの作成(詳細省略)
詳細は他の記事を参考にしてください。 簡潔に書きます。
line Developerにログインし、新しい公式のアカウントを作成します。
Messaging API設定の部分のWebhook設定の「Webhook URL」と「チャネルアクセストークン」を使用します。
- Webhook URL: デプロイ時に発行されるURLを登録することで、line とgasを繋げる
- チャンネルアクセストークン: gasからlineへの送信等を行う場合の認証トークン
2. Gasにデプロイ(詳細省略)
詳細は他の記事を参考にしてください。 簡潔に書きます。
初めてのデプロイ方法
右上の「デプロイ」=>「新しいデプロイ」=>「ウェブアプリ」 => アクセスできるユーザ「全員」=>デプロイ
※ここで全員にしないとline側から見ることができません。
2回目以降のデプロイ方法
※内容を変更した際には再度デプロイする必要があります。
「デプロイ」=>「デプロイを管理」=>「🖊️」=> バージョンの「新バージョン」=>「デプロイ」
3. マスターファイルの作成
まず、ユーザごとに作成するスプレッドシートの元となるファイルを作成します。
もし毎回何も記載されていないスプレッドシートで構わない場合には飛ばしてOKです!
※コードで、「マスターファイルをコピー」を「新規作成」にそれぞれ変えてください。
4. コードの記述
先にコードの全容です。
受信を受け取るmain.gsとスプレッドシート関連のspreadSheet.gsに分かれています。
事前に用意するスクリプトプロパティとして以下が必要です。
- CHANNEL_ACCESS_TOKEN
- MASTER_SPREADSHEET
gasの⚙️のマークの設定からスクリプトプロパティを追加できます。
スクリプトプロパティは.envの役割を果たすので秘密にする必要のある情報は入れておきましょう。
main.gs
// LineのDevelopersのアクセストークン
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");
/**
* データがポストで送られてくるので、データを取得する
* @param e ポストで送られたデータ
*/
function doPost(e) {
/** パースされた受信データ */
const json = JSON.parse(e.postData.contents);
const replyToken = json.events[0].replyToken;
const userMessage = json.events[0].message.text;
const userId = json.events[0].source.userId;
/** スプレッドシートがない場合には作成する*/
if (!existSpreadSheet(userId)) {
const spreadSheet = createUserSpreadSheet(userId);
// メッセージを保存する
addMessage(spreadSheet, userMessage);
return reply_message(
replyToken,
`新しくスプレッドシートを作成しました。\nURLはこちらです\n${getUrl(spreadSheetId)}`
);
}
/**メッセージを保存する */
const spreadSheet = getSpreadSheet(userId);
addMessage(spreadSheet, userMessage);
return reply_message(
replyToken,
`送信内容を保存しました\nURLはこちらです\n${getUrl(spreadSheetId)}`
);
}
/**文字を送る場合
* 返信を行う
* @param reply_token : アクセストークン
* @param message : 本文
*/
function reply_message(reply_token, message) {
/** 返信用 グローバル変数とした際にmessageがないとエラーが発生したのでこの場所に記載 */
const LINE_ENDPOINT = "https://api.line.me/v2/bot/message/reply";
/** 返信用のデータ */
const postData = {
replyToken: reply_token,
messages: [
{
type: "text",
text: "" + message,
},
],
};
// 返信する
UrlFetchApp.fetch(LINE_ENDPOINT, {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
},
payload: JSON.stringify(postData),
});
}
spreadSheet.gs
↓ここからがスプレッドシート関連の内容
/**
* userIdからスプレッドシートのidを取得する
* @param userId : ユーザID
* @returns : スプレッドシートID
*/
function getSpreadSheetId(userId) {
return PropertiesService.getScriptProperties().getProperty(userId);
}
/**
* マスターのスプレッドシートのidを取得する
* @returns : スプレッドシートID
*/
function getMasterSpreadSheetId() {
return PropertiesService.getScriptProperties().getProperty("MASTER_SPREADSHEET");
}
/**
* userIdとSpreadSheetIdを用いてスクリプトプロパティに保存する
* @param userId : ユーザID
* @param spreadSheetId スプレッドシートID
*/
function saveUserIdAndSpreadSheetId(userId, spreadSheetId) {
// ユーザーIDとスプレッドシートのIDを保存する
PropertiesService.getScriptProperties().setProperty(userId, spreadSheetId);
}
/**
* スプレッドシートが既に存在しているかどうか
* @param userId : ユーザID
* @returns {boolean} : 存在している(true)/存在していない(false)
*/
function existSpreadSheet(userId) {
return getSpreadSheetId(userId) !== null;
}
/**
* ユーザーのスプレッドシートを取得。
* @param ユーザーのID
* @return スプレッドシート
*/
function getSpreadSheet(userId) {
const spreadSheetId = getSpreadSheetId(userId);
if (existSpreadSheet(userId)) return SpreadsheetApp.openById(spreadSheetId);
return null;
}
/**
* ユーザーのスプレッドシートを生成。名前はユーザー名。URLを知っている全員に閲覧者として共有する
* @param userId ユーザーのID
* @return スプレッドシート
*/
function createUserSpreadSheet(userId) {
// コピー元のスプレッドシートのID
const masterSpreadSheetId = getMasterSpreadSheetId();
if (masterSpreadSheetId === null) {
throw ReferenceError("コピー元のスプレッドシートにアクセスできません。\n");
}
const masterSpreadSheet = SpreadsheetApp.openById(masterSpreadSheetId);
// スプレッドシートをコピーする。ファイル名にはユーザのLineのプロフィール名を用いる
const createdSpreadSheet = masterSpreadSheet.copy(
"メッセージ記録BOT_" + getUserNameFromUserId(userId)
);
// スクリプトプロバティにスプレッドシートの情報を保存する
const createdSpreadSheetId = createdSpreadSheet.getId();
saveUserIdAndSpreadSheetId(userId, createdSpreadSheetId);
// 設定の変更
setShareMode(createdSpreadSheetId);
return createdSpreadSheet;
}
/**
* スプレッドシートの編集権限をURLを知っている人にする
* @param SpreadSheetId スプレッドシートのID
*/
function setShareMode(SpreadSheetId) {
// アクセス権限の付与
const file = DriveApp.getFileById(SpreadSheetId);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.EDIT);
}
/**
* メッセージをスプレッドシートに記載する
* @param spreadSheet スプレッドシート
* @param message メッセージ
* @returns null
*/
function addMessage(spreadSheet, message) {
const sheet = spreadSheet.getSheetByName("シート1");
if (sheet === null) return; // sheetが存在するか
// 最終行に記載する
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow, 1).setValue(new Date());
sheet.getRange(lastRow, 2).setValue(message);
}
/**
* スプレッドシートのidからスプレッドシートのURLを返す
* @params spreadSheetId スプレッドシートのID
* @returns スプレッドシートのURL
*/
function getUrl(spreadSheetId){
return `https://docs.google.com/spreadsheets/d/${spreadSheetId}`
}
/**
* ユーザーIDからユーザー名を取得
* @param userId {str} ユーザーのID
* @return {str} ユーザー名
*/
function getUserNameFromUserId(userId) {
// プロファイル取得用のAPIのURL
const url ="https://api.line.me/v2/bot/profile"
/**ユーザー名を取得するための情報*/
const response = UrlFetchApp.fetch(url + "/" + userId, {
headers: {
"Content-Type": "application/json; charset=UTF-8",
Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
},
method: "get",
});
return JSON.parse(response.toString()).displayName;
}
コード詳細(main.gs)
詳細を見ていきましょう。まず、main.gsからです。(重要な部分のみです。)
doPost
// doPost(e)の中身
/** パースされた受信データ */
const json = JSON.parse(e.postData.contents);
const replyToken = json.events[0].replyToken;
const userMessage = json.events[0].message.text;
const userId = json.events[0].source.userId;
doPost(e)とすることでlineサーバーからのpostメソッドを受け取ることができます。
そのdataの内容をeに含めます(e = envent)
詳細はこちらを確認ください。
https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects
この内容によって、返信用のトークン、ユーザが送信した内容、ユーザのIDを取得します。
※ユーザのIDは、ユーザごとに1つであり、公式アカウントごとに1つではないので、管理に気をつけましょう。
// doPost(e)内の内容です。
/** スプレッドシートがない場合には作成する*/
if (!existSpreadSheet(userId)) {
const spreadSheet = createUserSpreadSheet(userId);
// メッセージを保存する
addMessage(spreadSheet, userMessage);
return reply_message(
replyToken,
`新しくスプレッドシートを作成しました。\nURLはこちらです\n${getUrl(spreadSheetId)}`
);
}
/**メッセージを保存する */
const spreadSheet = getSpreadSheet(userId);
addMessage(spreadSheet, userMessage);
return reply_message(
replyToken,
`送信内容を保存しました\nURLはこちらです\n${getUrl(spreadSheetId)}`
);
}
ここでスプレッドシートの存在を確認し、その結果によって動作を変えています。
存在していない場合
- 作成→メッセージの記録→新規作成したことを返信
存在している場合
- メッセージの記録→追加したことを返信
詳細についてはspreadSheet.gsで記載しています。
reply_message
// reply_message(reply_token, message)の内容です。
/** 返信用 グローバル変数とした際にmessageがないとエラーが発生したのでこの場所に記載 */
const LINE_ENDPOINT = "https://api.line.me/v2/bot/message/reply";
/** 返信用のデータ */
const postData = {
replyToken: reply_token,
messages: [
{
type: "text",
text: "" + message,
},
],
};
// 返信する
UrlFetchApp.fetch(LINE_ENDPOINT, {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
},
payload: JSON.stringify(postData),
});
ここでユーザに返す内容を指定して返信を送ります。
注意点としては、replyは1つのメッセージに対して1度しか使用できないため、複数メッセージを返信したい場合には、replyとpushMessageを組み合わせる必要があります。
詳細について以下のURLを確認してください。
https://developers.line.biz/en/reference/messaging-api/#send-reply-message
コード詳細(spreadSheet.gs)
ここからspreadSheet.gsの内容についてです。
getSpreadSheetId
function getSpreadSheetId(userId) {
return PropertiesService.getScriptProperties().getProperty(userId);
}
ここで、userIdを受け取り、スクリプトプロパティに登録してあるユーザごとのスプレッドシートのIDを取得します。
スクリプトプロパティへのスプレッドシートのIDの登録はスプレッドシートの作成時に自動で行います。
ちなみに、スプレッドシートのIDはurlの/d/以降の部分です。
xxxスプレッドシートのIDです。
https://docs.google.com/spreadsheets/d/xxx/edit?
saveUserIdAndSpreadSheetId
function saveUserIdAndSpreadSheetId(userId, spreadSheetId) {
PropertiesService.getScriptProperties().setProperty(userId, spreadSheetId);
}
ここでユーザIDとスプレッドシートIDをスクリプトプロパティに登録します。
getSpreadSheet
function getSpreadSheet(userId) {
const spreadSheetId = getSpreadSheetId(userId);
if (existSpreadSheet(userId)) return SpreadsheetApp.openById(spreadSheetId);
return null;
}
ここではスプレッドシートのIDを用いて、スプレッドシートの開きます。
スプレッドシートのID指定でできることとスプレッドシートを開いたオブジェクトでできることが異なるため注意しましょう。
スプレッドシートのオブジェクト
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet?hl=ja
createUserSpreadSheet
function createUserSpreadSheet(userId) {
// コピー元のスプレッドシートのID
const masterSpreadSheetId = getMasterSpreadSheetId();
if (masterSpreadSheetId === null) {
throw ReferenceError("コピー元のスプレッドシートにアクセスできません。\n");
}
const masterSpreadSheet = SpreadsheetApp.openById(masterSpreadSheetId);
// スプレッドシートをコピーする。ファイル名にはユーザのLineのプロフィール名を用いる
const createdSpreadSheet = masterSpreadSheet.copy(
"メッセージ記録BOT_" + getUserNameFromUserId(userId)
);
// スクリプトプロバティにスプレッドシートの情報を保存する
const createdSpreadSheetId = createdSpreadSheet.getId();
saveUserIdAndSpreadSheetId(userId, createdSpreadSheetId);
// 設定の変更
setShareMode(createdSpreadSheetId);
return createdSpreadSheet;
}
ここがメイン部分となります。
ここでマスターのスプレッドシートをコピーしてスプレッドシートを新規に作成します。
この時にファイル名はユーザのLineの名前を用いて表現します。
また、公開設定を変えないと自分しか見られないため公開設定を変更します。
ここで、作成者が自分であるため、ユーザではない点に注意です。
これにより作成者はいつでもスプレッドシートの内容を閲覧することができてしまいます。
もしユーザにしか見られないスプレッドシートを作りたければ、あらかじめユーザに作成してもらうか、作成後に画面操作で権限を渡して、ユーザに承認を受ける必要があります。
setShareMode
function setShareMode(SpreadSheetId) {
// アクセス権限の付与
const file = DriveApp.getFileById(SpreadSheetId);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.EDIT);
}
ここでアクセス権限を変えます。
ここでは、スプレッドシートのIDではなく、スプレッドシートのファイルIDで操作するため混乱しないようにしましょう。
ANYONE_WITH_LINKでURLを知っている人全員にXXXするという設定です。
DriveApp.Permission.EDITこのeditで今回は編集権限を与えています。
addMessage
function addMessage(spreadSheet, message) {
const sheet = spreadSheet.getSheetByName("シート1");
if (sheet === null) return; // sheetが存在するか
// 最終行に記載する
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow, 1).setValue(new Date());
sheet.getRange(lastRow, 2).setValue(message);
}
ここでは内容をスプレッドシートに記載しています。
sheet.getLastRow() + 1
このコードで最終行を取得し、+1することで未入力の行を取得します。
sheet.getRange(lastRow, 1).setValue(new Date());
sheet.getRange(lastRow, 2).setValue(message);
これにより、A列に日付を入力、B列に内容を入力しています。
getUserNameFromUserId
function getUserNameFromUserId(userId) {
// プロファイル取得用のAPIのURL
const url ="https://api.line.me/v2/bot/profile"
/**ユーザー名を取得するための情報*/
const response = UrlFetchApp.fetch(url + "/" + userId, {
headers: {
"Content-Type": "application/json; charset=UTF-8",
Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
},
method: "get",
});
return JSON.parse(response.toString()).displayName;
}
ここでは、ユーザのIDからユーザのLineのプロフィールの名前を取得します。
ここは本内容と逸れるので、あまり触れませんが、ユーザの名前や他の情報も取得可能です。
以上でコードの説明は終了です。
あとはgas側でデプロイをし、そのURLをline developerの方に貼り付けることでlineからの実行が可能になります。
注意点
上記の説明でも出てきましたが注意点まとめておきます。
- スプレッドシートは常に作成者が見えるようになっています。なので、ユーザ個人にしか見せない設定にするには別途機能が必要です。
- lineのユーザIDの管理には慎重に。ユーザIDは一意であり、紐づいているアカウントごとに変わるものではないので、取り扱いには注意しましょう。
おまけ
これを使うことで、例えば、100人の友達全員のスプレッドシートを作りたいというものできたりします。
また、何かエラーがあった際には、gasではそのエラーの内容を見ることができません。
そこで対策として、スプレッドシートにそのエラー内容を入力することが必要です。
以下のようのコードを書くことでエラーをスプレッドシートに表示し、デバッグに使えます。
今回は、SPREAD_SHEET_IDのスプレッドシートに「ログ」というシートがあることを前提にしています。
// エラーをスプレッドシートに記録するための準備 //
const SHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREAD_SHEET_ID');
const SHEET_NAME_LOG = "ログ";
/**
* エラーが発生した時にエラーメッセージをスプレッドシートに記録する
* @params e エラーメッセージ
*/
function logger(e) {
const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME_LOG);
if(sheet === null) return; // sheetが存在するか
const lastRow = sheet.getLastRow() + 1;
sheet.getRange(lastRow,1).setValue(new Date());
sheet.getRange(lastRow,2).setValue(e);
sheet.getRange(lastRow,3).setValue(e.stack);
}