今回作ったもの
ラボで映画を定期的に見る会が立ったので、各人が見たい映画を各々追加し、またその映画リストの中からランダムな映画を推薦してくれるslack botを作成しました。
こんな感じで動きます。
手順については大まかにしか記していませんが、GASのコードを残しておきますので、誰かの助けになればと思います。
機能:
- cinema add <title>で映画を追加
- cinema remove <title>で削除
- cinema list で現在のリストを確認
- cinema next で次に見る映画をランダムにレコメンド
構成:
Google スプレッドシート + GAS (Google Apps Script) + slack api
参考:
GASでSlackのinteractive messageの作成 ① Webhookアプリ編 #Slack - Qiita
SlackBotへのメンションをトリガーにメール送信する方法 #Slack - Qiita
Slack Appの作り方を丁寧に残す【BotとEvent APIの設定編】
作成手順:
- https://api.slack.com/ からYour Appsを作成
- Create New App→From Scratch
- 適当に名前とアプリを入れたいワークスペースを追加します
- Basic InformationのAdd featurs and functionalityからIncoming webhhooksを追加
- 一番下のAdd New Webhook to Workspaceから、ワークスペースの中の投稿したいチャンネルを選択
(ここまでで、メッセージをチャンネルに投稿できるようになる。Sample curl requestをターミナルで実行すれば、"Hello, world!"のメッセージがチャンネルに投稿されるはず。) アプリの名前とアイコンはBasic Informationの下の方から追加できます。
- Google スプレッドシートを開いて、webアプリを作成する
- スプレッドシートを作成
今回は3列目をデータとして用いる。テスト用に適当なタイトルを入力しておく。 - 拡張機能のタブからApps Scriptを追加
- 以下の関数を作成
function doPost(e) {// testMessage();var response = {};if (params.type === 'url_verification') {return ContentService.createTextOutput(params.challenge);} else if (params.type === 'event_callback') {if (params.event.type == 'message') {response = eventHandler(params.event);}}return {};// return ContentService.createTextOutput(response).setMimeType(ContentService.MimeType.JSON);}function eventHandler(event) {return {};} - アプリをデプロイする。アクセスできるユーザーの範囲を全員に広げておく(本当はもっと狭めるべきかもしれない)。
- デプロイしたアプリのurlをコピーしておく。
- スプレッドシートを作成
- スプシとslack apiを連携させる
- slackのアプリを作るwebサイトの方に戻って、Event Subscriptionを開く
- URLのところに先ほどのGASのwebアプリのurlを貼り、Verifiedされたら成功。失敗した場合はエラーメッセージをググる。
- Subscribe to events on behalf of usersの所から、message.channelsに対するサブスクライブを行うように設定する。
これでslack botがチャンネル内のメッセージを読み、GASに送信できるようになる。読むたびにdoPost()関数が実行される。
function testMessage() {//Webhook URLを以下に入力const postUrl = "<your app url>";const sendMessage = "test";const jsonData = {"text": sendMessage};const options = {"method": "post","payload": payload};UrlFetchApp.fetch(postUrl, options);}
- GASのスクリプトに処理を追加する。
- 最後に、読んだメッセージに合わせて映画リストを編集したりslackに投稿したりする機能を作成する。スクリプトは以下。
function sendMessage(text) {//Webhook URLを以下に入力const postUrl = "<>";// メッセージを加工const formattedMessage = text + "\n ```" + createMessage()+ "```"; // 引用枠で囲まれたテキストとcreateMessage()の内容を結合
const jsonData = {"text": formattedMessage};const options = {"method": "post","payload": payload};UrlFetchApp.fetch(postUrl, options);}
function testMessage() {//Webhook URLを以下に入力const postUrl = "<>";const sendMessage = "test";const jsonData = {"text": sendMessage};const options = {"method": "post","payload": payload};UrlFetchApp.fetch(postUrl, options);}
function chosenMessage(text) {//Webhook URLを以下に入力const postUrl = "<>";// メッセージを加工//最後の文字を幾つかのパターンからランダムに変える "お楽しみに!"などconst lastMessages = ["お楽しみに!", "ポップコーンをお忘れなく!","いってらっしゃい!","研究ネタが見つかるかも!?","コーラをお忘れなく!","ピザでも頼む?","初めて見る?","アメージング!!","予定は空いてる?","CHILL OUT!","見たことあるかな?","これは教養だね!","これは必修だよね!"];const random = Math.floor(Math.random() * lastMessages.length);const lastMessage = lastMessages[random];const sendMessage = "次回の上映は *" + text + "* ですよ!\n" + lastMessage;const jsonData = {"text": sendMessage};const options = {"method": "post","payload": payload};UrlFetchApp.fetch(postUrl, options);}
function createMessage() {const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();const sheet = spreadsheet.getSheetByName("schedule");
const data = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();const messages = ;
for (const row of data) {if (row[0]) { // 空でないセルのみを処理messages.push(row[0]);}}
return messages.join('\n');}
function doPost(e) {// testMessage();var response = {};if (params.type === 'url_verification') {return ContentService.createTextOutput(params.challenge);} else if (params.type === 'event_callback') {if (params.event.type == 'message') {response = eventHandler(params.event);}}return {};// return ContentService.createTextOutput(response).setMimeType(ContentService.MimeType.JSON);}
function eventHandler(event) {if (event.text.startsWith("cinema add")) {const valueToAdd = event.text.replace("cinema add", "").trim(); // メンション部分を削除// スプレッドシートにデータを追加addValueToSpreadsheet(valueToAdd);return sendMessage("追加したよ!");}
if (event.text.startsWith("cinema list")) {return sendMessage("今後の上映予定は以下の通りだよ!\n" + createMessage());}
if (event.text.startsWith("cinema remove")) {const valueToRemove = event.text.replace("cinema remove", "").trim(); // メンション部分を削除// スプレッドシートからデータを削除removeValueFromSpreadsheet(valueToRemove);return sendMessage("削除したよ!");}
if (event.text.startsWith("cinema next")) { //ある映画リストから一つだけランダムに抜き出してそれをslackに返す、抜き出した後は削除return nextTitleFromSpreadsheet();}
return {};}
function nextTitleFromSpreadsheet(){const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();const sheet = spreadsheet.getSheetByName("schedule");const data = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();const messages = ;for (const row of data) {if (row[0]) { // 空でないセルのみを処理messages.push(row[0]);}}const random = Math.floor(Math.random() * messages.length);const randomMessage = messages[random];removeValueFromSpreadsheet(randomMessage);chosenMessage(randomMessage);}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();const sheet = spreadsheet.getSheetByName("schedule");// スプレッドシートから既存のデータを取得const data = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();// ↑ getRange 関数によって、3列目(C列)のセルからデータを取得し、// 2行目から始めて、最終行までのデータを取得します。// 値を削除for (let i = 0; i < data.length; i++) {// 値が一致した場合、そのセルを空白に設定sheet.getRange(i + 2, 3).setValue("");// ↑ i + 2 はスプレッドシートの行インデックス。i は配列のインデックスなので、+2 して行インデックスに変換します。break;}}}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();const sheet = spreadsheet.getSheetByName("schedule");
// スプレッドシートから既存のデータを取得const data = sheet.getRange(2, 3, sheet.getLastRow() - 1, 1).getValues();// ↑ getRange 関数によって、3列目(C列)のセルからデータを取得し、// 2行目から始めて、最終行までのデータを取得します。
let foundEmptyCell = false;
// 最初の空白のセルに値を追加for (let i = 0; i < data.length; i++) {if (!data[i][0]) {// 空白のセルが見つかった場合、そのセルに値を設定// ↑ i + 2 はスプレッドシートの行インデックス。i は配列のインデックスなので、+2 して行インデックスに変換します。foundEmptyCell = true;break;}}
if (!foundEmptyCell) {// 空白のセルが見つからなかった場合、新しい行に値を追加}}
- 最後に、読んだメッセージに合わせて映画リストを編集したりslackに投稿したりする機能を作成する。スクリプトは以下。
最後に
今回は特定の文字列に対して反応するようにしたが、メンションに対して反応する方が正しい気がする。まあ最低限動いたので、一旦はこれで良しとしよう。