狂ったお茶会のlog

後で起きる自分のためのメモ

ランチ候補をpostしてくれるslackbotを、Google Apps Scriptで作る

やりたいこと

転職したのでランチ場所の情報を蓄積したい
一回行って美味しかった場所&これから行ってみたいと思ってる場所をおすすめしてくれるやつがいい

いい感じにランチを決める Slack ボットを作った

↑こういうのがいいなぁと思った

仕様
・ 「ごはん」と呼んだら候補をslackにpostしてくれる
・ お昼の時間になったら、自動的にも投稿してくれる
スプレッドシートの情報を読んで、候補をランダムに選んでくれる


やったこと

Incoming WebHooks を設定する

https://my.slack.com/services/new/incoming-webhook
で Post to Channel でbotに投稿させたいチャンネルを選んで、
Add Incoming WebHooks Integration ボタンを押下

ここで生成された、 Webhook URL を後で使うのでメモっておく。

Outgoing WebHooks を設定する

https://my.slack.com/services/new/outgoing-webhook
で Add Outgoing WebHooks Integration ボタンを押下して追加

Channel: 発言を拾うチャンネル名を設定
Trigger Word(s): トリガーワードを設定できるが今回は使わなかった。使った方が楽
URL(s): 作成したGASのウェブアプリケーションのurlを設定してSaveする。urlについては後述

上記の設定を変更したい時はここ
https://my.slack.com/apps/manage/custom-integrations

スプレッドシート作る

f:id:dormouse666:20180510201938p:plain
こんな感じにした
店名だけだとわからんので、備考とurlも情報として持つようにする

あと、botが喋るmessageの内容もマスタっぽくシートに持たせておくことにした
f:id:dormouse666:20180510202006p:plain
こんな感じで


Google Apps Script のファイルを作成してコード書く

書いた

var SLACK_WEBHOOK = 'https://hooks.slack.com/services/~~~~~~';  // Incoming WebHooks -> Webhook URLをここに設定する
var SLACK_CHANNEL = '#lunch';
var SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/~~~~~~~~~';  // 作成したスプレッドシートのURL
var EMOJI_ICON = ':fried_shrimp:';
var BOT_NAME = 'lunchBot';
var LUNCH_HOUR = 12;
var LUNCH_MINUTE = 50;
var IS_COFFEE = false;
var IS_GOHAN = false;
var MESSAGE_TYPE_AUTO = 1;
var MESSAGE_TYPE_GOHAN = 2;
var MESSAGE_TYPE_NEMUI = 3;

// お昼になったら自動投稿するためのトリガー
function setTrigger() {
  var triggerDay = new Date();
  triggerDay.setHours(LUNCH_HOUR);
  triggerDay.setMinutes(LUNCH_MINUTE);
  ScriptApp.newTrigger("lunch").timeBased().at(triggerDay).create(); // lunch関数を動かしたいので、それを指定
}

function deleteTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == "lunch") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

var getRows = function (range) {
  return range.getValues().map(function(x) { return x[0]; }).filter(function(x) { return x });
}

Array.prototype.random = function () {
    return this[Math.floor(Math.random() * this.length)]
}

Array.prototype.randomCount = function () {
    return Math.floor(Math.random() * this.length);
}

// 投稿
function postMessage(message, hookPoint) {
  var payload = {
    "text": message,
    "icon_emoji": EMOJI_ICON,
    "username": BOT_NAME,
    "channel": SLACK_CHANNEL
  }
  var options = {
    "method" : "POST",
    "payload" : JSON.stringify(payload),
    "headers": {
      "Content-type": "application/json",
    }
  }
  var response = UrlFetchApp.fetch(hookPoint, options);

  if (response.getResponseCode() == 200) {
    return response;
  }
  return false;
}

// メイン
function lunch() {  

    if(!IS_GOHAN && !IS_COFFEE) {
      deleteTrigger(); //自動投稿トリガーを消す
    }
  
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
  var sheet0 = spreadsheet.getSheets()[0]; //一番左のシート: 店羅列用
  var sheet1 = spreadsheet.getSheets()[1]; //次のシート: mesasge用
  
  // message 該当タイプをランダムで
  var messageAll = sheet1.getRange(3, 2, sheet1.getLastRow(), sheet1.getLastColumn()).getValues();
  var messageList = [];
  var message = "";
  var type = MESSAGE_TYPE_AUTO;
  
  if(IS_GOHAN) {
    type = MESSAGE_TYPE_GOHAN;
  }
  
  if(IS_COFFEE) {
    type = MESSAGE_TYPE_NEMUI;
  }
  
  for (var i in messageAll) {
    var mtype = messageAll[i][1]; //1つなら数値、カンマ区切りならstring判断になる
    var typeList = [];
    if(mtype == null) {
      break;
    }
    if(isNaN(mtype)) { 
      typeList = mtype.split(','); //文字列。カンマ区切りで配列に変換
    } else {
      typeList.push(mtype); //数値なのでそのままぶち込む
    }

    for (var ii in typeList) {
      if (typeList[ii] == type) {
        messageList.push(messageAll[i][0]); //textだけ格納
      }
    }   
  }
  
  message += messageList.random();
  if(!IS_GOHAN && !IS_COFFEE) {
    message += " <!channel>"; //自動投稿の時だけメンション飛ばす
  }
  
  // 3行目〜
  // B列: ご飯
  // C列: 備考
  // D列: url
  var bs = getRows(sheet0.getRange(3, 2, sheet0.getLastRow()));
  var cs = getRows(sheet0.getRange(3, 3, sheet0.getLastRow()));
  var ds = getRows(sheet0.getRange(3, 4, sheet0.getLastRow()));
  var cnt1 = bs.randomCount();
  var b = bs[cnt1];
  var c = cs[cnt1];
  var d = ds[cnt1];
  
  // F列: 軽食
  // G列: 備考
  // H列: url
  var fs = getRows(sheet0.getRange(3, 6, sheet0.getLastRow()));
  var gs = getRows(sheet0.getRange(3, 7, sheet0.getLastRow()));
  var hs = getRows(sheet0.getRange(3, 8, sheet0.getLastRow()));
  var cnt2 = fs.randomCount();
  var f = fs[cnt2];
  var g = gs[cnt2];
  var h = hs[cnt2];
  
  // J列: 遠出
  // K列: 備考
  // L列: url
  var js = getRows(sheet0.getRange(3, 10, sheet0.getLastRow()));
  var ks = getRows(sheet0.getRange(3, 11, sheet0.getLastRow()));
  var ls = getRows(sheet0.getRange(3, 12, sheet0.getLastRow()));
  var cnt3 = js.randomCount();
  var j = js[cnt3];
  var k = ks[cnt3];
  var l = ls[cnt3];
  
  if(!IS_COFFEE){
      message += "\n\n\n:fried_shrimp: " + "*" + b + "*" + " :fried_shrimp:";
      message += "\n:speech_balloon: " + c + "\n" + d;
  }
  message += "\n\n\n:coffee: " + "*" + f + "*" + " :coffee:";
  message += "\n:speech_balloon: " + g + "\n" + h;
  if(!IS_COFFEE){
      message += "\n\n\n:mushroom: " + "*" + j + "*" + " :mushroom: [遠出枠]";
      message += "\n:speech_balloon: " + k + "\n" + l;
  }
  message += "\n\n\n候補を編集する → <" + SPREADSHEET_URL + "|:memo:>";

  postMessage(message, SLACK_WEBHOOK);
}

// Outgoing WebHooks を設定すると、ユーザからのpostがあると走る処理
function doPost(e) {
  // 自分自身ははじく
  if(e.parameter.user_id == "USLACKBOT"){
    return;
  }
  
  // Outgoing WebHooks の Trigger Word(s) でも設定できるんだけど
  if(e.parameter.text.indexOf("ごはん") > -1){
    IS_GOHAN = true;
    lunch();
  }
  
  // おまけ
  if(e.parameter.text.indexOf("ねむい") > -1){
    IS_COFFEE = true;
    lunch();
  }
}

postMessage
doPost
はGASが持ってる関数。

途中でやらかしたのが、botの返信メッセージに「ごはん」が含まれるとそれに自分で反応して無限ループする。
ので、自分自身には反応しないようにする。
doPost で得ている e.parameter の中身、botからの送信だと

user_id: USLACKBOT
user_name: slackbot

で来ているっぽい。それを弾くようにしておく。

e.parameter.text.indexOf("ごはん") > -1
↑これは、部分一致。「ごはん」という言葉が含まれていれば反応する。


書いたコードを実行するために

GASのメニュー -> 公開 -> Webアプリケーションとして導入 -> 新しいバージョンを保存 -> 導入

で、ここで URLが生成されるので、それを先述した Outgoing WebHooks の URL(s) に設定する。

注意点として、上記手順後にコードを変更したら、
GASのメニュー -> 公開 -> Webアプリケーションとして導入 -> プロジェクト バージョン -> 新規作成 -> 更新 ボタン押下
を毎回しないとbotに反映されない。
ちょっとめんどくさい。


自動投稿するならトリガーを設定する

月水金だけお昼の時間になったら自動で投稿してほしいので、そういった設定をする。

var LUNCH_HOUR = 12;
var LUNCH_MINUTE = 50;

↑の、自動投稿してほしい時間以前に、 setTrigger が走るようにしておけばOK
f:id:dormouse666:20180510202134p:plain

こんな感じ。


こうなる

自動投稿時
この時だけメンションをくれる
f:id:dormouse666:20180510202201p:plain


「ごはん」で呼んだ時
f:id:dormouse666:20180510202224p:plain


ねむい時はコーヒーを勧めてくる
f:id:dormouse666:20180510202246p:plain


以上


参考URL

いい感じにランチを決める Slack ボットを作った
SlackのIncoming Webhooksを使い倒す <-Incoming WebHooks
SlackのOutgoing Webhooksを使って投稿に反応するbotを作る <-Outgoing WebHooks
Google Apps Scriptの日毎のトリガーで時間をもっと細かく設定する <-トリガー
GoogleAppsScriptメモ:トリガーの利用 <-トリガー
Google Apps Script 実践メモ(スプレッドシート) <-スプレッドシートへのアクセス
SlackのIncomingWebhooksとOutgoingWebhooksを使って電子工作と連携させてみよう