1. 始めに
1.1 前提
うちの文化祭で非公式のクイズBotを作りたいと発注が来まして、スクレイピングでやってた研究と文化祭のオフィシャルアプリと、Android AIと、いろいろ仕事と学業があるなか、無理やり引き継いだのがLINEbotの制作です。具体的には言えないのですが、それを作るために必要なものをかき集めてまとめました。
1.2 そもそもどうゆうの?
①GUI
LINEです。はい。
②処理
サーバーで行います。サーバーを立てれるのはいいのですけど維持費がきついし、公開めんどい。
そこで出てきたのがGoogleAppsScript(GAS)です。サーバーレスでWebアプリが作れるそうです。Googleのサービス、ツール(GoogleSpreadSheetとかDriveとか)との連携がいいのが推しポイントです。また、ブラウザ上のみで開発ができるので最強です。(ちなみにJavaScript)
③メッセージの送信
処理系統からLINEにPOSTすればいいそうです。マルチパートと同じ感じで書き方があるそうです。(JSONらしい)
④DB
DBはスプレッドシート。(個人的にはExcelのほうが好き)
⑤ユーザー情報
特にない。でも、段階的な(クリアしたら次にいく系)クイズなので、ユーザーごとに今どこまで進んでいるか、問題文、難易度、複数の答え(半角、全角、単位など答える人によっては同じ意味だけど文字的に違うかもしれないから)をDBに入れないといけない。クリアした問題は左に"A"
を付けます。
<ユーザー情報の準備・生成>
アカウント名:User
とすると
- 問題、答えなど基本情報のあるスプレッドシート
Master
はURLとして残し、ネットに公開します。 - それをコピーして
User
という名前のスプレッドシートを複製します。 - 今どの問題を解いているか
User
にアクセスします。 - Aが付いていない行の問題を
User
から取得します。 - 問題を出します。
- ユーザーが答えます。
- 正解します。
-
User
の正解した問題にAを付けます。
こうすることでオリジナルのMaster
ファイルは破損、編集されないので友達になればいくらでも生成します。(生成されたファイルは制作者あなたのDriveに入るので個人情報は抜き取らないで)
今回はこのスプレッドシートを削除できるようにします。
2. LINEアカウントの生成
- https://account.line.biz/login にアクセスして今使っているLINEアカウントでログイン。
- プロバイダーを生成(名前は適当)。
- 新しいチャンネルをMessaging APIから作ります。この時、プランをTrialとかいうやつにしてください。
- 初期設定いろいろしたらおkです。
こんな感じ。そしたら下のアクセストークン(ロングターム)
を再発行してもらいます。失効は0時間。これで再発行されたトークンをメモ。
次に、Webhook送信
を利用するにしてください。
ここら辺は忘れたので違かったらググってどうぞ。
3. オウム返し
GoogleAppsScriptにアクセスして、ログインしたら。新しいプロジェクトを生成して開きます。そして上書き。
var CHANNEL_ACCESS_TOKEN = '/*さっきメモったやつ*/';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
//ポストで送られてくるので、ポストデータ取得
//JSONをパースする
function doPost(e) {
var json = JSON.parse(e.postData.contents);
//返信するためのトークン取得
var reply_token= json.events[0].replyToken;
var user_message = json.events[0].message.text;
reply_message(reply_token, user_message);
}
/**文字を送る場合
*
* @param String reply_token : アクセストークン
* @param String[] message : 本文
*
*/
function reply_message(reply_token, message) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : "" + message
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
さっきメモったやつをCHANNEL_ACCESS_TOKENに入れてください。
そしたらGoogleAppsScript(以後、GAS)の公開>Webアプリケーションとして公開
と進んで、
バージョンはNew、
次のユーザーとしてアプリケーションを実行は自分(メアド)、
アプリケーションにアクセスできるユーザーは全員(匿名...)、
それで公開します。出てきたURLをコピーして、LINE DeveloperのWebhook URL ※SSLのみ対応
にペーストして前のhttps://
を消して適用させてあげます。チェックマークが出てきて、接続確認が成功したら大丈夫です。
次またソースを変えて実行したくなったら公開の作業だけをしてください。生成されるURLは変わらないはずなので。Webhook送信
をオンにしてないことが多いので気を付けて。
これでオウム返しは大丈夫なはずです。
<解説みたいなの>
CHANNEL_ACCESS_TOKEN
はそのままです。
line_endpoint
に入ってるリンクはReply送信のAPIが提供されているURLです。基本的に不変です。
doPost
の引数、e
には送信者のデータが詰まってます。それをJSONにパースして中にある(送ってきた人を判別する)トークンとメッセージを取ります。それを作ったreply_message
関数でJSONの中に組み入れ、UrlFetchApp.fetch(line_endpoint, options);
でLINEに送信!あとはJSONを解釈してくれて、メッセージを飛ばしてくれるという感じです。
4. コマンドに反応する。
送るメッセージを変えるだけ。
var CHANNEL_ACCESS_TOKEN = '/**/';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
//ポストで送られてくるので、ポストデータ取得
//JSONをパースする
function doPost(e) {
var json = JSON.parse(e.postData.contents);
//返信するためのトークン取得
var reply_token= json.events[0].replyToken;
var user_message = json.events[0].message.text;
//返信する内容を作成
var reply_messages; // 配列
switch (user_message) {
case 'ド':
reply_messages = ['はドーナツのド'];
break;
case 'レ':
reply_messages = ['はレモンのレ'];
break;
case 'ミ':
reply_messages = ['はみんなのミ'];
break;
case 'ドレミ':
reply_messages = ['の歌'];
break;
default:
reply_messages = [user_message]
break;
}
reply_message(reply_token, reply_messages);
}
/**文字を送る場合
*
* @param String reply_token : アクセストークン
* @param String[] message : 本文
*
*/
function reply_message(reply_token, message) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : "" + message[0]
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
<解説もどき>
switch
文で分岐。それ以外の場合はオウム返し。break;
を忘れずに。reply_messages
変数は後程、いろいろなデータを詰め込みたいので配列。だから、reply_message
関数内の"text" : "" + message[0]
では配列message
の一つ目を取るようにしてます。
5. 画像も送れるようにする
switch
文内で画像を送るのかどうかtrue of false
boolean
型で表記しとく。
画像には注意も...
//CHANNEL_ACCESS_TOKENを設LINE developerで登録をした、自分のCHANNEL_ACCESS_TOKENを入れて下さい
var CHANNEL_ACCESS_TOKEN = '';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
//ポストで送られてくるので、ポストデータ取得
//JSONをパースする
function doPost(e) {
var json = JSON.parse(e.postData.contents);
//返信するためのトークン取得
var reply_token= json.events[0].replyToken;
var user_message = json.events[0].message.text;
// 画像を送るかどうか
var is_image = false;
//返信する内容を作成
var reply_messages; // 配列
switch (user_message) {
case 'ド':
reply_messages = ['はドーナツのド'];
is_image = false;
break;
case 'レ':
reply_messages = ['はレモンのレ'];
is_image = false;
break;
case 'ミ':
reply_messages = ['はみんなのミ'];
is_image = false;
break;
case 'ドレミ':
reply_messages = ['の歌'];
is_image = false;
break;
case '画像':
reply_message = ["", "https://cdn-ak.f.st-hatena.com/images/fotolife/C/CyberHacnoshuke/20190220/20190220203854.jpg", "https://cdn-ak.f.st-hatena.com/images/fotolife/C/CyberHacnoshuke/20190220/20190220205005.jpg"];
is_image = true;
default:
reply_messages = [user_message]
is_image = false;
break;
}
if (is_image) {
// 画像
reply_message_image(reply_token, reply_messages[0], reply_messages[1], reply_messages[2]);
} else {
// 画像ではない
reply_message(reply_token, reply_messages);
}
}
/**文字を送る場合
*
* @param String reply_token : アクセストークン
* @param String[] message : 本文
*
*/
function reply_message(reply_token, message) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : "" + message[0]
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
/** 画像を送る場合
*
* @param String reply_token : アクセストークン
* @param String message : 画像と一緒に送るメッセージ
* @param String originalContentUrl : オリジナル画質の画像のURL
* @param String previewImageUrl : プレビュー画質の画像のURL
*
*/
function reply_message_image(reply_token, message, originalContentUrl, previewImageUrl) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : message
},
{
"type" : "image",
"originalContentUrl": originalContentUrl,
"previewImageUrl": previewImageUrl
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
送る画像には制限があります。
まず、必ずhttps://
のURLにしてください。hatena blogとかでアップロードしてURLを取得できます。(URLは開いたときこんな感じになるものにしてください。はてなブログの教えてくれるURLではなく右クリックしてアドレスを取得してくださいね。)
次に容量とサイズの制限です。
メイン画像 | プレビュー画像 | |
---|---|---|
最大画像サイズ | 1024×1024 | 240×240 |
最大ファイルサイズ | 1MB | 1MB |
こんなかんじのめやすを付けといてください。 | ||
**必ずプレビュー画像を付けてください。**片方がないとダメみたいです。 |
6. データベースをユーザーごとに用意する。
スプレッドシートでMaster
のファイル(コピー元)を書き終わったらWeb上に公開してください。そのURLはhttps://docs.google.com/spreadsheets/d/XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx/edit
みたいな感じになります。
データベース関連のソース
// プロファイル取得用のAPIのURL(Replyと同じ感じ)
var line_endpoint_profile = 'https://api.line.me/v2/bot/profile';
/** ユーザーのスプレッドシートを取得。なかったらcreateSpreadSheetを呼んで生成。
*
* @param user_id ユーザーのID
*
* @return スプレッドシート
*/
function getSpreadSheet(user_id) {
var sid = PropertiesService.getScriptProperties().getProperty(user_id);
if (sid == null) {
return createSpreadSheet(user_id);
} else {
try {
return SpreadsheetApp.openById(sid);
} catch(e) {
return createSpreadSheet(user_id);
}
}
}
/** ユーザーのスプレッドシートを生成。名前はユーザー名。権限なしアクセスを許可して共有。
*
* @param user_id ユーザーのID
*
* @return スプレッドシート
*/
function createSpreadSheet(user_id) {
var ori_sheet = SpreadsheetApp.openById('XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx');
var spreadSheet = ori_sheet.copy(getUserDisplayName(user_id));
PropertiesService.getScriptProperties().setProperty(user_id, spreadSheet.getId());
var file = DriveApp.getFileById(spreadSheet.getId());
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return spreadSheet;
}
/** ユーザーのアカウント名を取得。
*
* @param user_id ユーザーのID
*
* @return アカウント名
*/
function getUserDisplayName(user_id) {
var res = UrlFetchApp.fetch(line_endpoint_profile + '/' + user_id, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'get',
});
return JSON.parse(res).displayName;
}
// 自分でスプレッドシート関連の関数作るときに参考にしてください。
//
// var SpreadSheet = getSpreadSheet(user_id);
// var sheet = SpreadSheet.getSheetByName('シート1');
// これでシートを取得
// sheet.getRange(/*縦の数字*/, /*横の数字*/).getValue();
// sheet.getRange(1, 2).getValue();
// これでB1の値を取得
//
// sheet.getRange("B1").getValue();
// これも同じ意味
/** 特定のユーザーのスプレッドシートを削除(ゴミ箱だとまだアクセスできるので、ゴミ箱からも消します。)
*
* @param user_id ユーザーのID
*/
function deleteData(user_id) {
var SpreadSheet = getSpreadSheet(user_id);
//ゴミ箱に入れずに削除
Drive.Files.remove(SpreadSheet.getId());
}
関数ごとの説明は上記の通り。createSpreadSheet
関数内のvar ori_sheet = SpreadsheetApp.openById('XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx');
ではオリジナルシート(Master
)を取得しています。
XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx
にはスプレッドシートを公開したときのhttps://docs.google.com/spreadsheets/d/XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx/edit
の該当部分を入れてください。
7. 全部
var CHANNEL_ACCESS_TOKEN = '';
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';
var line_endpoint_profile = 'https://api.line.me/v2/bot/profile';
//ポストで送られてくるので、ポストデータ取得
//JSONをパースする
function doPost(e) {
var json = JSON.parse(e.postData.contents);
//返信するためのトークン取得
var reply_token= json.events[0].replyToken;
//user_idはこうやってとる
var user_id = json.events[0].source.userId;
var user_message = json.events[0].message.text;
// 画像を送るかどうか
var is_image = false;
//返信する内容を作成
var reply_messages; // 配列
switch (user_message) {
case 'ド':
reply_messages = ['はドーナツのド'];
is_image = false;
break;
case 'レ':
reply_messages = ['はレモンのレ'];
is_image = false;
break;
case 'ミ':
reply_messages = ['はみんなのミ'];
is_image = false;
break;
case 'ドレミ':
reply_messages = ['の歌'];
is_image = false;
break;
case '画像':
reply_message = ["", "https://cdn-ak.f.st-hatena.com/images/fotolife/C/CyberHacnoshuke/20190220/20190220203854.jpg", "https://cdn-ak.f.st-hatena.com/images/fotolife/C/CyberHacnoshuke/20190220/20190220205005.jpg"];
is_image = true;
break;
case 'スプレッドシート':
//リンクを取得する
var SpreadSheet = getSpreadSheet(user_id);
reply_messages = [SpreadSheet.getUrl()];
is_image = false;
break;
default:
reply_messages = [user_message]
is_image = false;
break;
}
if (is_image) {
reply_message_image(reply_token, reply_messages[0], reply_messages[1], reply_messages[2]);
} else {
reply_message(reply_token, reply_messages);
}
}
/**文字を送る場合
*
* @param String reply_token : アクセストークン
* @param String[] message : 本文
*
*/
function reply_message(reply_token, message) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : "" + message[0]
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
/** 画像を送る場合
*
* @param String reply_token : アクセストークン
* @param String message : 画像と一緒に送るメッセージ
* @param String originalContentUrl : オリジナル画質の画像のURL
* @param String previewImageUrl : プレビュー画質の画像のURL
*
*/
function reply_message_image(reply_token, message, originalContentUrl, previewImageUrl) {
var postData = {
"replyToken": reply_token,
"messages": [{
"type" : "text",
"text" : message
},
{
"type" : "image",
"originalContentUrl": originalContentUrl,
"previewImageUrl": previewImageUrl
}]
};
var options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(line_endpoint, options);
}
/** ユーザーのスプレッドシートを取得。なかったらcreateSpreadSheetを呼んで生成。
*
* @param user_id ユーザーのID
*
* @return スプレッドシート
*/
function getSpreadSheet(user_id) {
var sid = PropertiesService.getScriptProperties().getProperty(user_id);
if (sid == null) {
return createSpreadSheet(user_id);
} else {
try {
return SpreadsheetApp.openById(sid);
} catch(e) {
return createSpreadSheet(user_id);
}
}
}
/** ユーザーのスプレッドシートを生成。名前はユーザー名。権限なしアクセスを許可して共有。
*
* @param user_id ユーザーのID
*
* @return スプレッドシート
*/
function createSpreadSheet(user_id) {
var ori_sheet = SpreadsheetApp.openById('XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx');
var spreadSheet = ori_sheet.copy(getUserDisplayName(user_id));
PropertiesService.getScriptProperties().setProperty(user_id, spreadSheet.getId());
var file = DriveApp.getFileById(spreadSheet.getId());
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return spreadSheet;
}
/** ユーザーのアカウント名を取得。
*
* @param user_id ユーザーのID
*
* @return アカウント名
*/
function getUserDisplayName(user_id) {
var res = UrlFetchApp.fetch(line_endpoint_profile + '/' + user_id, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'get',
});
return JSON.parse(res).displayName;
}
// 自分でスプレッドシート関連の関数作るときに参考にしてください。
//
// var SpreadSheet = getSpreadSheet(user_id);
// var sheet = SpreadSheet.getSheetByName('シート1');
// これでシートを取得
// sheet.getRange(/*縦の数字*/, /*横の数字*/).getValue();
// sheet.getRange(1, 2).getValue();
// これでB1の値を取得
//
// sheet.getRange("B1").getValue();
// これも同じ意味
/** 特定のユーザーのスプレッドシートを削除(ゴミ箱だとまだアクセスできるので、ゴミ箱からも消します。)
*
* @param user_id ユーザーのID
*/
function deleteData(user_id) {
var SpreadSheet = getSpreadSheet(user_id);
//ゴミ箱に入れずに削除
Drive.Files.remove(SpreadSheet.getId());
}
こんな感じです。書き込みなどは自由で。
8. まとめ
わずか一日という短期間で仕上げたのでこの記事に間違いがあるかもしれませんが、指摘していただけるとありがたいです。いまはやりのLINE。そのBotアカウント、作ってみませんか。
Twitter: https://twitter.com/Cyber_Hacnosuke (フォローしてくださいお願いします。)
Github: https://github.com/CyberHacnoshuke