#1行概要
Messaging API、GAS、Googleカレンダーの組み合わせで、予定の共有と匿名で投稿ができるBotを実現した。
#はじめに
LINEのグループチャット、特にそこまで親しくなかったり、知らない人も入っていたり、大人数だったりするとラインするのに躊躇しますよね。
だったら「Botにラインさせよう」ってことで、LINE Messaging APIとGoogle Apps Scriptを使ってグループチャットに匿名で投稿+予定の共有ができるLINE Botを開発しました。
ちなみに、私は4月から勤務している会社の同期グループ向けに作りました。そのため、社名や氏名など画像の一部に隠しを入れています。
#完成例
まず、実際の動作の様子を機能別に紹介します。
###機能1.予定の追加
グループで共有したい予定を追加します。
①LINE Bot(以下、ボット)と1対1の個人チャット(以下、個チャ)の画面は次の通り。「予定の追加」と「匿名で投稿」というメニューが表示されています。
②「予定の追加」をタップすると、予定の日付、開始時刻、終了時刻、名前と入力案内がされます。最後に予定の確認がされ、「はい」と入力すると予定の追加が完了します。
入力案内のいずれかのフェーズで「やめる」もしくは予定の確認で「いいえ」と入力すると、最初からやり直すようになります。なお、ボットが参加しているグループチャット(以下、グルチャ)ではメニューは表示されませんが、「予定の追加」と手入力すれば同様の手順で予定の追加を行うことはできます。
###機能2.予定の通知
グループで共有したい予定の通知をします。
機能1で追加した予定が通知されています。通知のタイミングは任意で設定ができ、このボットの場合は、予定の前日20~21時に通知するようにしています。予定がない場合は通知しません。
###機能3.匿名で投稿
グルチャに匿名で投稿ができます。
①「匿名で投稿」をタップすると、投稿内容を入力する案内が表示されます。投稿内容が確認され、「はい」と入力すると予定の追加が完了します。
投稿内容を入力するフェーズで「やめる」もしくは投稿内容の確認で「いいえ」と入力すると、最初からやり直すようになります。なお、ボットが参加しているグルチャではメニューは表示されませんが、「匿名で投稿」と手入力すれば同様の手順で匿名で投稿を行うことはできます。もはや匿名ではありませんが。
②投稿した内容は「匿名投稿:(投稿内容)」として、グルチャにボットが代わりに投稿してくれます。
#初心者に向けて
次に、本記事のボット実現のために使ったものを初心者に向けてできるだけ噛み砕いて説明します。
実際は私自身が立ち返って復習をできることが目的です。
###LINE Messaging API
ボットを実現する方法はいくつかありますが、基本的にはMessaging APIを使うことになると思います。
「機能2.予定の通知」に限れば、LINE Notifyを使うことでシンプルに実現することができます。が、ボットとやり取りをしたり、グルチャに投稿したりと、より自由度や拡張性が高いのはMessaging APIです。個人的にはボットに任意の名前やアイコンを付けたかったという理由もありますが。
基本的な処理の流れは次の通り。
ちなみに、Webhookとは、WebコールバックやHTTPプッシュAPI、リバースAPIとも呼ばれ、Webアプリケーション(ここで言う、LINEプラットフォーム)でイベントが実行されたときに外部サービス(ここで言う、ボットサーバー)とHTTPで通信する仕組みです。
ちなみにちなみに、API(Application Programming Interface)とは、プログラムの機能やデータなどを外部のプログラムから呼び出して利用するための手順やデータ形式などを定めたもののことです。
1.ユーザーが、LINE公式アカウント(=ボット)にメッセージを送信します。
2.LINEプラットフォームからボットサーバーのWebhook URLに、Webhookイベントが送信されます。
3.Webhookイベントに応じて、ボットサーバーからユーザーにLINEプラットフォームを介して応答します。
Messaging APIで主にできることは次の通り。
本記事のボットでは「応答メッセージを送る」「プッシュメッセージを送る」「ユーザープロフィールを取得する」を使っています。
・応答メッセージを送る
・プッシュメッセージを送る
・さまざまなタイプのメッセージを送る
・テキストメッセージ
・スタンプメッセージ
・画像メッセージ
・動画メッセージ
・音声メッセージ
・位置情報メッセージ
・イメージマップメッセージ
・テンプレートメッセージ
・Flex Message
・ユーザーが送ったコンテンツを取得する
・ユーザープロフィールを取得する
・グループチャットに参加する
・リッチメニューを使う
Tips
###Google Apps Script
APIの次は、Webhookを受け取るボットサーバーと処理を記述するプラットフォームを決める必要があります。
Messaging APIではJava、PHP、Pythonなど様々なプラットフォームで処理を記述することができますが、本記事ではGoogle Apps Script(以下、GAS)を利用します。
GASはその名の通りGoogleが提供するスクリプト開発プラットフォームで、本記事でGASを採用する理由は大きく2つあります。1つ目は予定の追加・管理・取得をするGoogleカレンダーと容易に連携ができるため、2つ目は別にサーバーを用意する必要がないためです。
Tips
###簡易プログラムの作成
ボット開発初期段階の慣例となりつつある、「オウム返しボット」の作成手順を解説します。
手順1.チャネルの作成
まずはチャネル(=ボットの外枠)の作成です。
①LINE Bussines IDから「LINEアカウントでログイン」をします。
LINEビジネスIDとは、LINEが提供するビジネス向けまたは開発者向けの各種管理画面にログインができる共通認証システムで、通常のLINEアカウントのログイン情報(IDとパスワード)でログインが可能です。
②「プロバイダー」(ボットの開発・提供者の意)を作成します。
③「チャネル設定」の中から「Messaging API」を選択しチャネルを作成します。
「チャネル名」(=ボット名)「チャネル説明」「大業種」「小業種」「メールアドレス」を入力し、各種利用規約に同意します。
手順2.GASプロジェクトの新規作成
処理を記述するプロジェクトを作成します。
①Googleドライブにアクセスします。
このときGoogleアカウントにログインする必要があり、アカウントを所持していない人は新たに作成する必要があります。
②ログイン後、Googleドライブ画面左上の「新規」から、「その他」>「Google Apps Script」を選択します。
手順3.処理の記述
実際にボットの処理を記述していきます。
①手順2で作成したGASプロジェクトにもともと記述されているコード(myFunction)は削除し、次のコードを入力してください。以下、プロジェクト名「TEST」、GASファイル名「test.gs」となっています。
コードの細かい中身はこの際気にしなくても大丈夫です。これがボットの基本型になります。
ちなみに、JSON(JavaScript Object Notation)とは文字ベースのデータ送受信用フォーマットです。ボットのメッセージやメッセージ送信のためのフォーマットとして利用しています。
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = "【※】";
/*
* ボットにイベントが発生したときの(メイン)処理
* (例)メッセージの受信、フォローされた、アンフォローされた
*/
function doPost(e) {
const events = JSON.parse(e.postData.contents).events;
events.forEach((event) => {
// イベントがメッセージの受信だったとき
if(event.type == "message") {
reply(event);
}
});
}
/*
* オウム返しをする処理
*/
function reply(e) {
// 受信したメッセージをそのまま送信
const message = {
"replyToken": e.replyToken,
"messages": [
{
"type": "text",
"text": e.message.text
}
]
};
// 送信のための諸準備
const replyData = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
},
"payload": JSON.stringify(message)
};
// JSON形式でAPIにポスト
UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", replyData);
}
②手順1で作成したチャネルの「Messaging API設定」の最下部にある「チャネルアクセストークン」をコピーし、①のコード中【※】と置き換えます。
チャネルアクセストークンとは、作成したチャネルのMessaging APIを呼び出すときに必要なキーワードのようなものです。他の人に見られないように注意してください。
手順4.処理の公開
GASプロジェクトを公開し、記述した処理がボットサーバーの役割を果たすようにします。
①処理を記述したプロジェクトの「公開」から「ウェブアプリケーションとして導入...」を選択します。
②「Deply as web app」の小ウィンドウが表示されたら、「Project version:」に「New」を、「Who has access to the app:」に「Anyone, even anonymous」を選択してください。
これらを正しく選択しないと、プロジェクトの更新がされなかったり、ボットからボットサーバーにアクセスできなかったりします。
③プロジェクトを初めて公開しようとする際には確認の小ウィンドウが表示されます。「許可を確認」で承認してください。
④環境によっては下のように「このアプリは確認されていません」という画面が表示されるかもしれません。
問題ありませんので、最下部の「【プロジェクト名】(安全ではないページ)に移動」を選択します。
⑤「【プロジェクト名】がGoogleアカウントへのアクセスをリクエストしています」と表示されますので、「許可」を選択してください。
⑥次のように「Deploy as web app」として、「Current web app URL:」が表示されれば公開完了です。
手順5.チャネルのWebhook指定
チャネルのWebhookに、作成したGASプロジェクトを指定します。
①手順1で作成したチャネルの「Messaging API設定」の中央「Webhook設定」の「Webhook URL」に手順4⑥で表示された「Current web app URL:」のURLを入力し、「Webhookの利用」をオンにします。
②「Webhook URL」の「検証」を行い、「成功」と表示されればWebhookの指定完了です。
私見ですが、Webhookの反映の多少のラグがあるときがあるように思えます。「成功」以外のエラーメッセージが出た場合は、焦らずここまでの手順を再度確認してみてください。
手順6.ボットの動作確認
最後にボットの動作を確認してみましょう。
①手順1で作成したチャネルの「Messaging API設定」の「QRコード」から友達登録をします。
②適当なメッセージを送信し、同じメッセージが返ってくれば成功です。画像では「こんにちは!」とラインして、「こんにちは!」と返って来ているのが分かります。
このとき、フォローしたときの「あいさつメッセージ」とラインする度にくる「応答メッセージ」が表示されていますが、別で任意に設定・解除ができます。
Tips
ここまでが、本記事のボット実現のために使ったものの基本的な説明になります。
#実装
ようやく本題です。~~自分でも長くなってしまったと反省しています。~~実際に実装したボットについて説明していきます。
なお、コードの大部分はLINE BOTからGoogleカレンダーの予定の取得・追加を行うを参考にさせていただいています。
###前準備
処理を記述する前に次の準備をする必要があります。
①新規チャネル、GASプロジェクトの作成
新規にボットを作るため、チャネルとGASプロジェクトを作成します。細かな手順は前述した通りです。
②グルチャへの参加許可
チャネルを作成したら、「Messagin API設定」中の「グループ・複数人チャットへの参加を許可する」を「有効」にします。
③カレンダーIDの取得
予定の追加・管理・取得を行う任意のGoogleカレンダーを作成します。(既存のものでも構いません。)
用意したカレンダーIDをコピーしておきます。
###処理の記述
次のコードが実際のボットの処理の中身です。
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = "【※1】";
// 予定の追加・管理・取得をするカレンダーID
const CALENDER_ID = "【※2】";
// グルチャのグループID
const GROUP_ID = "【※3】";
const dateExp = /(\d{2})\/(\d{2})\s(\d{2}):(\d{2})/;
const dayExp = /(\d+)[\/月](\d+)/;
const hourMinExp = /(\d+)[:時](\d+)*/;
/*
* ボットにイベントが発生したときの(メイン)処理
*/
function doPost(e) {
let replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
let lineType = JSON.parse(e.postData.contents).events[0].type
let userMessage = JSON.parse(e.postData.contents).events[0].message.text;
// フォロー、アンフォローイベントは今回無視
if(typeof replyToken === "undefined" || lineType === "follow" || lineType === "unfollow") {
return;
}
// ボットの状態遷移をtypeという名のキャッシュで管理
let cache = CacheService.getScriptCache();
let type = cache.get("type");
// 状態なし
if(type === null) {
// 「予定の追加」メッセージを受け取ったとき
if(userMessage === "予定の追加") {
cache.put("type", 1);
reply(replyToken, "予定の日付を教えてください!\n形式指定:『1/23』『1月23日』\nキャンセル:『やめる』と入力");
// 「匿名で投稿」メッセージを受け取ったとき
} else if(userMessage === "匿名で投稿") {
cache.put("type", 10);
reply(replyToken, "グルチャに匿名でラインします!投稿内容を教えてください!\nキャンセル:『やめる』と入力");
// メッセージの投稿に必要なグループIDの取得(後準備で説明)
} else if(userMessage === "getGroupId") {
// reply(replyToken, JSON.parse(e.postData.contents).events[0].source.groupId);
}
// 状態あり
} else {
if(userMessage === "やめる") {
cache.remove("type");
reply(replyToken, "キャンセルしました");
return;
}
// 状態1~5は予定の追加、状態10~11は匿名で投稿
switch(type) {
// 予定の日付
case "1":
let [matched, month, day] = userMessage.match(dayExp);
cache.put("type", 2);
cache.put("month", month);
cache.put("day", day);
reply(replyToken, "次に開始時刻を教えてください!\n形式指定:『1:23』『12時』『12時34分』\nキャンセル:『やめる』と入力");
break;
// 予定の開始時刻
case "2":
let [matched, startHour, startMin] = userMessage.match(hourMinExp);
cache.put("type", 3);
cache.put("start_hour", startHour);
if (startMin == null) startMin = "00";
cache.put("start_min", startMin);
reply(replyToken, "次に終了時刻を教えてください!\n形式指定:『1:23』『12時』『12時34分』\n\キャンセル:『やめる』と入力");
break;
// 予定の終了時刻
case "3":
let [matched, endHour, endMin] = userMessage.match(hourMinExp);
cache.put("type", 4);
cache.put("end_hour", endHour);
if (endMin == null) endMin = "00";
cache.put("end_min", endMin);
reply(replyToken, "最後に予定の名前を教えてください!\nキャンセル:『やめる』と入力");
break;
// 予定の名前
case "4":
cache.put("type", 5);
cache.put("title", userMessage);
let [title, startDate, endDate] = createEventData(cache);
reply(replyToken, toEventFormat(title, startDate, endDate) + "\n\nで間違いないでしょうか?よろしければ『はい』を、やり直す場合は『いいえ』と入力してください!");
break;
// 予定の確認
case "5":
cache.remove("type");
if (userMessage === "はい") {
let [title, startDate, endDate] = createEventData(cache);
CalendarApp.getCalendarById(CALENDER_ID).createEvent(title, startDate, endDate);
reply(replyToken, "予定を追加しました!");
} else {
reply(replyToken, "お手数ですが最初からやり直してください");
}
break;
// 匿名で投稿する内容
case "10":
cache.put("type", 11);
cache.put("post", userMessage);
let message = createPost(cache);
reply(replyToken, message + "\n\nで間違いないでしょうか?よろしければ『はい』を、やり直す場合は『いいえ』と入力してください!\nいたずらや誹謗中傷は絶対にやめてください");
break;
// 投稿する内容の確認
case "11":
cache.remove("type");
if (userMessage === "はい") {
let message = createPost(cache);
pushPost("匿名投稿:\n" + message);
} else {
reply(replyToken, "お手数ですが最初からやり直してください");
}
cache.remove("post");
break;
}
}
}
/*
* 追加する予定の日付、開始時刻、終了時刻、名前の作成・保管
*/
function createEventData(cache) {
const year = new Date().getFullYear();
const title = cache.get("title");
const startDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("start_hour"), cache.get("start_min"));
const endDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("end_hour"), cache.get("end_min"));
return [title, startDate, endDate];
}
/*
* 追加する予定の確認のためのフォーマット作成
*/
function toEventFormat(title, startDate, endDate) {
const start = Utilities.formatDate(startDate, "JST", "MM/dd HH:mm");
const end = Utilities.formatDate(endDate, "JST", "MM/dd HH:mm");
const str = title + ": " + start + " ~ " + end;
return str;
}
/*
* 匿名で投稿する内容の作成・保管
*/
function createPost(cache){
const post = cache.get("post");
return post;
}
/*
* ボットのメッセージ応答
*/
function reply(replyToken, message) {
const url = "https://api.line.me/v2/bot/message/reply";
UrlFetchApp.fetch(url, {
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
},
"method": "post",
"payload": JSON.stringify({
"replyToken": replyToken,
"messages": [{
"type": "text",
"text": message,
}],
}),
});
return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}
/*
* ボットからのポスト処理
*/
function pushPost(body){
const url = "https://api.line.me/v2/bot/message/push";
// 指定のグルチャにPOSTする
UrlFetchApp.fetch(url, {
"headers": {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
},
"method": "post",
"payload": JSON.stringify({
"to": GROUP_ID,
"messages":[{
"type": "text",
"text": body,
}]
})
})
}
/*
* 通知する予定の取得
*/
function getEvents() {
let date = new Date();
date.setDate(date.getDate() + 1);
const events = CalendarApp.getCalendarById(CALENDER_ID).getEventsForDay(date);
if (events.length !== 0) {
let body = "明日の予定は\n";
events.forEach(function(event) {
const title = event.getTitle();
const start = toHHmm(event.getStartTime());
const end = toHHmm(event.getEndTime());
body += "*" + title + ": " + start + " ~ " + end + "\n";
});
body += "です!";
pushPost(body);
}
}
/*
* 時刻フォーマットの作成
*/
function toHHmm(date){
return Utilities.formatDate(date, "JST", "HH:mm");
}
###機能別の処理の詳細
冒頭で紹介した機能別の処理の中身を説明していきます。
機能1.予定の追加
本記事のボットは特定のメッセージを受け取ったことをトリガーに、状態遷移を管理するtype
という名のキャッシュの中身を変化させて案内をします。
機能1の場合は、その中身の通り「予定の追加」というキーワードをトリガーにしています。トリガーが発動すると案内の進み具合に応じてtype
を1~5の順に変化させ、案内の課程で入力させた予定の日付、開始時刻、終了時刻、名前をもとにCalendarApp.getCalendarById(CALENDER_ID).createEvent(title, startDate, endDate)
で指定のGoogleカレンダーに予定を追加します。
機能2.予定の通知
対して機能2は独立のトリガーで動作します。
GASには任意のタイミングで任意の関数を実行させる機能が付いています。本記事のボットでは、追加された予定の前日20~21時の間にグルチャに予定の通知をすることになっていました。具体的には、後述する「日付ベースのタイマー」設定でgetEvents
関数を実行することで予定の通知を行います。
機能3.匿名で投稿
機能1に同じで、「匿名で投稿」というキーワードをトリガーにしています。トリガー発動後はtype
が10~11の順に変化し、最終的にpushPost
関数でグルチャにボットが匿名で投稿します。
###後準備
処理を記述したら、細かい後準備をしていきます。
①チャネルアクセストークン【※1】の入力
例の如く。
②カレンダーID【※2】の入力
前準備で取得したカレンダーIDを【※2】と置き換えます。
③グループID【※3】の取得・入力
ここが少しややこしいです。
メッセージに応答をするのではなく、ボットから直接グルチャに投稿をするためには投稿するグルチャのグループIDが必要になります。
グループIDを取得する方法はいくつかありますが、本記事ではボットがグルチャに参加後、特定のキーワードをトリガーにグループIDを応答するという方法でグループIDを取得します。具体的には、コード中の次の内容が該当します。まず、ボットをグルチャに参加させ、reply
以下のコメントアウトを外した後getGroupId
というキーワードでグループIDを応答させます。グループID取得後、reply
以下は再びコメントアウトすることで後にgetGroupId
というキーワードでトリガーは発生しなくなります。もう少しましな方法がある気はしますが。
if (userMessage === "getGroupId") {
// reply(replyToken, JSON.parse(e.postData.contents).events[0].source.groupId);
}
④日付ベースのタイマートリガーの設定
最後に機能2のトリガーを設定します。
GASプロジェクトのトリガー設定から、getEvents
を任意のタイミングで発動させるように設定します。本記事のボットでは「日付ベースのタイマー」で「午後8時~9時」に発動させるようにトリガーを設定しました。
以上で処理の記述および諸準備は終わりです。
#UIの設定
最後にボットの見た目クオリティを上げます。
ここからは完全に好みの問題になるので、かいつまんで必要な箇所だけ説明します。
①リッチメニュー
ボットとの個チャに表示されるメニューで、LINE Official Account Managerから設定が可能です。
あくまで任意ですが、本記事のボットは特定のキーワードをトリガーに各機能を実行することができるので、メニューを選択するとキーワードを送信するメニューを作成すると良いかもしれません。
②応答メッセージの無効化
重ね重ねになりますが、本記事のボットは特定のキーワードをトリガーに機能を実行します。したがって、受け取ったメッセージ一つひとつに対して応答をする必要はないかと思います。~~いちいち応答されたらウザいですし。~~設定から無効化することをおすすめします。
#おわりに
本記事では、LINE Messaging APIとGoogle Apps Scriptを使ってグループチャットに匿名で投稿+予定の共有ができるLINE Botを開発しました。
時間にして3時間程度での実現だったので比較的容易に実装できたのではないかと思います。改めてGASは有能だと気づかされました。次はGoogle AppsではなくDBにデータを蓄積・取得していく形式のボットの開発を検討中です。