【全部GAS】社内で作ったGoogleドキュメント製マニュアル検索Slack BotをDrive APIの全文検索機能を使って実装した話
作ることになった経緯
- 社内でGoogleドキュメントでマニュアルを整備することになった
- Googleドキュメントのマニュアルを特定のフォルダを開かないと見れないのは利便性が悪い
- Slack Bot × Google系のAPIで何とかしてみようと思った
やったこと(TL:DR;)
- GASのWebデプロイでSlack Event Subscription のエンドポイントを作成
- Slack Bot をワークスペースに導入
- Drive API で対象フォルダ内のファイル検索
- SlackでBotに検索ワード付きでメンションすると、検索結果を質問者あてにメンションつきで送ってくれる。
開発のポイント
- エンドポイントはverifyの
challenge
適切に捌こう - GCP連携してログ書き出ししている場合は、GCP側でもAPIを有効化しよう(これで筆者は30分詰まりました)
- 検索のクエリで
mimeType
を使ってファイルタイプを指定、全文検索はfullText
で。
この記事で書いてないこと
- Slack Botの立て方
- GASでSlack Botが動くようになるまでの流れ(コード読んだら分かるけど、細かく記述しないよ)
このあたりは、以下記事参照して作りました。感謝感激。Special Thanks
https://auto-worker.com/blog/?p=2904 (基本的なGAS×Slack Bot について)
https://arrown-blog.com/gas-slack-bot-mention-collect/#1-2_Event_Subscriptions ( Slack Event Subscriptionのエンドポイント設定周り )
注意点
実際のコード
基本の処理
doPost(e)
関数がGASをウェブ上でデプロイする際(ウェブ上でデプロイすると設定次第ですが、パブリックなサーバーを立てられます)のメイン関数です。
引数e
でリクエストを受け取り、パースしています。
console.log
はGCP経由でログ表示させています。
paramsはSlackから送られてくるJSONをパースしたもので、必ず付与されているtype
メンバをswitchで切り分け、url_verification
が指定された場合はchallenge
を返し、Event Subscriptionの場合はevent_callback
が指定され独自のdoEvent(event)
関数を呼び出します。
末尾のreturn ContentService.createTextOutput()
はSlack API側に200を返すための記述です。
// シンタックスハイライトはJavascriptのものを利用しています。
function doPost(e) {
try {
const params = JSON.parse(e.postData.getDataAsString()); // PostのJSONデータをObjectにパース
console.log(params)
const type = params.type;
switch (type) {
case "url_verification":
return ContentService.createTextOutput(params.challenge);
case "event_callback":
doEvent(params.event)
return ContentService.createTextOutput();
}
} catch (err) {
console.log(err)
}
}
Drive API を利用した検索機能
DriveApp.getFolderById
で検索対象のフォルダを指定。
サブフォルダまでは検索しないので、必要であればループさせる必要ありです。
searchFolder.searchFiles
がメインの検索を行う関数。
返り値はFileIterator
です。
searchFiles関数の引数について
詳細は全てこちらの公式ドキュメントに...
まとめると、引数となるparams
には検索用のクエリを入れ込んだ、文字列を渡すことになります。
クエリの書式は以下の通りです。
検索条件 条件式 値
検索条件はname
やmodifiedTime
,writers
など、検索の条件絞り込みが可能です。
条件式は一般的な!=
,=
だけでなく<
やcontains
などがありますが、検索条件ごとに使える条件式が違うため、詳しくは公式ドキュメント(英語のみ)を読んでください。
値は文字列を指定する場合はシングルクォーテーションで囲むなど指示がありますが、それも検索条件ごとに変わります。
書式は空白を開けて複数重ねることが可能です。
ちなみに検索条件のmimeType
でGoogleドキュメントのみを指定しています。他にもいろいろ指定可能。公式ドキュメント(英語のみ)を読んでね
例)編集日が2023年1月1日0:00(GMT+9)のファイルタイトルに「経費精算」を含むファイル
modifiedDate >= 2022-12-31T15:00:00:00-00:00 name contains '経費精算'
function SearchGoogleDocs(query) {
// query引数は検索キーワードです。
const driveId = PropertiesService.getScriptProperties().getProperty("MANUAL_INDEX_FOLDER")//GASのPropertiesServiceという環境変数を扱える機能経由でマニュアルが含まれたフォルダのIDを指定しています。
const searchFolder = DriveApp.getFolderById(driveId)
let files = searchFolder.searchFiles("mimeType = 'application/vnd.google-apps.document' and fullText contains '" + query + "'");
//ここでの条件は、ファイルタイプがGoogleドキュメントで、タイトル、本文対象の全文検索でqueryワードを含むファイル
return files
}
メンションされたワードをもとに検索結果を返す機能
const type = event.type
const text = event.text
switch (type) {
case "app_mention":
event
もtype
があるため、switch
で切り分けしています。
const token = PropertiesService.getScriptProperties().getProperty("SLACK_ACCESS_TOKEN")
const slackApp = SlackApp.create(token)
tokenをもとにSlackAppのクライアントを作成。
Slack API ラッパーについて
今回はGASのSlackApp(GitHub)を利用しています。ドキュメントも整備されていて素敵なライブラリ。
あんまりリポジトリに対してリンク貼られてなかったので貼っておきます。
メンションされたテキストから検索ワードを取り出す
let query = text.replace(/<.*>/g, "").replace("\n", "");
Slackでメンションを受け取ると、<@userID> hogefuga
のように、メンションの文字が含まれるので正規表現で削除。ついでに改行もエラーの元になるので消しておきましょう。
これでメンション+検索ワードから検索ワードのみを取り出せます。
Drive API File Iteratorを回す
FileIteratorはイテレーター(解説してくれているQiita記事)なので、配列(じゃないけど)の次の対象があるかチェックして、あれば次へなければ終了という流れでwhile
を回していきます。
message
変数に名前とURLと改行コード\n
を渡して終了
let files = SearchGoogleDocs(query) // さっきの検索関数です。queryは検索ワードです。
while (files.hasNext()) {
var file = files.next()
message += `\n${file.getName()}(${file.getUrl()})`
}
Slack API のメッセージで@mention
var message = `<@${event.user}> 「${query}」のワードを含むマニュアルは見つかりませんでした。検索ワードを変えてください。`
送りたいメッセージに<@userID>
を含めるだけ。
メッセージ送信はslackApp.postMessage
特にこれといったこともないですが、event
オブジェクトのchannel
メンバは、メンションされた(正確に言うとイベントが発生した)チャンネルのIDを含んでいるのでこれでチャネル指定してメッセージ送信します。
message
も普通の文字列で大丈夫です。
slackApp.postMessage(event.channel, message);
コード全文
function doEvent(event) {
const type = event.type
const text = event.text
switch (type) {
case "app_mention":
const token = PropertiesService.getScriptProperties().getProperty("SLACK_ACCESS_TOKEN")
const slackApp = SlackApp.create(token)
let query = text.replace(/<.*>/g, "").replace("\n", "");
let files = SearchGoogleDocs(query)
if (!files.hasNext()) {
var message = `<@${event.user}> 「${query}」のワードを含むマニュアルは見つかりませんでした。検索ワードを変えてください。`
slackApp.postMessage(event.channel, message);
} else {
var message = `<@${event.user}> 「${query}」のワードを含むマニュアルの検索結果です。`
message += "\n------------"
while (files.hasNext()) {
var file = files.next()
message += `\n${file.getName()}(${file.getUrl()})`
}
slackApp.postMessage(event.channel, message);
}
}
}
以上を組み合わせてデプロイすれば完成!
最後に
年明け3日の朝にぶっ続けでコードと記事書いて腕がしびれています。
このあとはGITADORAやるぞ!
読了アリガトウゴザイマス!
Discussion