Slackのslash command + Googleスプレッドシート + GASで貸出帳を作った

はじめまして小森です。仕事は総務系メインで、たまに情シス・人事労務やってます。

社内コミュニケーションにSlackを使っています。私自身は去年入社してから初めて使っていますが、メールに比べて圧倒的に便利ですね。Skypeも使っていたことはありますが、やっぱ個対個中心でした。Slackはチャットはもちろん頻繁に活用してますが、通知系も便利に利用しています。チームのタスク管理にTrelloを利用しているので、新規カードの追加時とかにSlackに通知がくるようにしています。とても便利。IFTTTを使って採用の通知メールを受信したら通知されるようにもしてます。

今回はSlackのslash commandを利用した貸出表を結構ちゃんと作ったのでnote化してみることにしました。Slackは使ってても、エンジニアじゃないとあんまりslash commandは使ってないんじゃないかなと思ってます。どうなんだろ。

/remind @taro 小森さんに連絡する at6pm
/calc 200*3
/unipos @taro +39 いつもありがとう!

私はこの3つしか使ってない気がする。今日も元気に働きバチ!でおなじみremindはとても便利。calcは標準じゃ入ってなかったような。結構便利。uniposは今流行りのサンクスポイントを送り合うツールです。気になる方は下記参照。

今回実現したかったこと

mobileWifiの貸出管理をおこないたかったです。そんなに台数があるわけじゃないんですが、やっぱり人数が増えてくると「この前貸したでしょ?」「いえ、私持ってませんけど」みたいなことがよく起こります。ExcelやGoogleスプレッドシートだけで管理するのが手軽ですが、記入漏れとかが頻発することが予想できます。Slackなら普段から利用しているので、多少は解消になるのではと思っています。

オリジナルのslash command基礎

貸出管理を実現する前に、slash commandの基礎を書いておきます。Slack単体ですとオリジナルのslash commandは作れません。https通信ができる環境が必要とのことです。色々選択肢はあるのですが、無料かつ程よいのはGoogleスプレッドシートとGAS(Google Apps Script)の組み合わせだと思います。今回も利用しているのですが、スプレッドシートは簡易なデータベースになるのでとても使い勝手が良いです。

ではまず下準備。Googleスプレッドシートを新規作成します。

ファイル名はなんでも大丈夫です。今回は「inout」「list」の2つのシートを使いますが詳細は後ほど。

ツール→スクリプトエディタからGASエディタを開きましょう。

エディタを開くとmyFunctionなるものが自動作成されますが、使わないので削除します。削除したら、下記をコピペします。この基礎ではslash commandを実行すると「テスト」と表示するサンプルを作ります。

function doPost(e) {
  var verificationToken = e.parameter.token;
  if (verificationToken != '********************') { // AppのVerification Tokenを入れる
     throw new Error('Invalid token');
  }
  
  var result ='テスト';
  
  var response = {text: result};
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

ここまで作ってセーブしたら一旦この画面から離れて、下記Slack APIのサイトを開きます。

この画面に遷移します。遷移できない場合、もしかしたらAdmin権限が必要かもしれませんので管理者に聞いてもらうか、お試しなのでSlackの新しいWorkspaceを作った方がいいかもしれません。

右上の「Create New App」をクリックします。

App Nameは適当で大丈夫です。このAppに複数のslash commandがひも付きます。必要事項を入力したら右下の「Create App」をクリックします。

まず「Basic Information」を選択して、少し下の方にスクロールすると「Verification Token」という項目に長い文字列があります。こちらをコピペしてGASの「*************************」となっていた部分と置き換えましょう。

置き換えたらまだGASの画面で作業します。

公開→ウェブアプリケーションとして導入、をクリックします。

これから何度かこの更新作業を行うことになりますが、必ず「新規作成」を選択して下さい。新規にしないと最新のコードを認識してくれないようです。

保存が完了したら「現在のウェブアプリケーションのURL」を全てコピーして下さい。

Slack APIに戻ります。Slash Commandsに移って「Create New Command」をクリックして下さい。

「Command」は実際のslash commandを入力します。「Request URL」は先程GAS画面で最後に取得したURLを貼り付けます。「Save」をクリックします。

後もう少しです。「Basic Information」に移って「Install App to Workspace」をクリックします。ここまでうまくいくと。。。

テスト、と表示するだけですがslash commandが完成しました!少しだけソースの説明です。

function doPost(e) {
  
}

slash commandを入力するとGASに通信が行きますが、doPostというmain関数が実行されます。これはこの名前じゃないと駄目です。

var verificationToken = e.parameter.token;
if (verificationToken != '********************') { // AppのVerification Tokenを入れる
   throw new Error('Invalid token');
}

ここはコピペして持ってきたので詳しくわかりません。不正アクセスされていないか検証してるんじゃないかと思います。

var result ='テスト';
  
var response = {text: result};
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);

一律「テスト」という文字列を表示させるのでresultに固定値を入れています。Slackへは気味の悪い拡張子でおなじみのJSON形式で引き渡されます。ここらへんはとりあえず定型文として利用できるかと思います。

貸出帳の設計

なんとかここまで作りましたが、テストって表示するだけで終わっちゃつまらないので次行きます。今回作り始める前に、予め下記を考えてから作りました。

slash commandで全端末の状況を取得する
slash commandで貸出/返却入力が出来る
最低限のチェックのみで、細かいことは考えない

ここまで考えた後に、データの持ち方とslash commandの引数を同時に考えていきました。

今回は「inout」「list」の2つのシートを使いデータ管理をしています。「inout」シートはslash commandから「貸出」「返却」が行われると一番下の行に追記されていきます。「list」シートは「inout」シートの情報を集計しています。A列以外は標準の関数で構成しています。若干関数が複雑になっていますが、やっていることは「inout」で一番下から各Noを検索して、検索に引っかかった行を「list」に反映しています。lookup関数の下から検索するバージョンがあれば楽だったのですが、無いのでこのような形になっています。

1行目の関数を参考まで
A2:固定値
B2:=if(F2="in","貸出可","貸出中")
C2:=INDEX(inout!$B:$B,MAX(INDEX((inout!$A:$A=A2)*ROW(inout!$A:$A),)))
D2:=if(F2="in","",G2)
E2:=if(F2="in",G2,"")
F2:=INDEX(inout!$C:$C,MAX(INDEX((inout!$A:$A=A2)*ROW(inout!$A:$A),)))
G2:=INDEX(inout!$D:$D,MAX(INDEX((inout!$A:$A=A2)*ROW(inout!$A:$A),)))

データ構成を考えると同時に、slash commandの引数も考えていきました。

/wifi list
/wifi 端末ID 名前 in | out

別々のslash commandにしても良かったのですが、なんとなく1つで統一しました。

GASを書いてみる

function doPost(e) {
  var verificationToken = e.parameter.token;
  if (verificationToken != '***********************') { // AppのVerification Tokenを入れる
     throw new Error('Invalid token');
  }
  
  var command = e.parameter.text.split(' ');
  
  var result ='';
  var listStartRow = 1;
  var listStartColumn = 1;
  var listEndRow = 6;
  var listEndColumn = 5;
  var wifiList = getListRange(listStartRow, listStartColumn, listEndRow, listEndColumn).getValues();
  
  if(command[0] == 'list') {
    result = getList(listStartRow, listStartColumn, listEndRow, listEndColumn);
    
  } else if(isEnableWifiName(command, 0, 0, wifiList, listEndRow) && isInOrOut(command, 2)) {
    var spreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('inout');
    var lastRow = spreadsheet.getLastRow() + 1;
    var today = new Date();
    
    spreadsheet.getRange(lastRow, 1).setValue(command[0]);
    spreadsheet.getRange(lastRow, 2).setValue(command[1]);
    spreadsheet.getRange(lastRow, 3).setValue(command[2]);
    spreadsheet.getRange(lastRow, 4).setValue(today);
    
    result = "受け付けました。";
    
  } else {
    result = 'usage:\n/wifi list\nwifi一覧を表示します。\n\n/wifi wifi端末ID 名前 in|out\nout(貸出)in(返却)を登録します。';
  }
  
  var response = {text: result};
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

function isInOrOut(command, column) {
  var result = false;
  
  if(command[column] == 'in' || command[column] == 'out'){
     result = true;
  }
  
  return result;
}

function isEnableWifiName(command, commandColumn, listColumn, wifiList, listEndRow) {
  var result = false;
  
  for(var i=0; i<listEndRow; i++){
      if(command[commandColumn] == wifiList[i][listColumn]){
      result = true;
      }
  }

  return result;
}

function getList(listStartRow, listStartColumn, listEndRow, listEndColumn){
  var result = '';
  var range;
  
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list');
  range = spreadsheet.getRange(listStartRow, listStartColumn, listEndRow, listEndColumn);
  
  for(var i=0; i<listEndRow; i++){
    for(var j=0; j<listEndColumn; j++){
      result = result + range.getValues()[i][j] + ' | ';
    }
    result = result + '\n';
  }
  
  return result;
}

function getListRange(listStartRow, listStartColumn, listEndRow, listEndColumn){
  var result;
  var range;
  
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('list');
  range = spreadsheet.getRange(listStartRow, listStartColumn, listEndRow, listEndColumn);
  
  return range;
}

全部を解説すると終わらなそうなので、大切なところとハマったところを中心に記載します。コードの中身を省いて、各関数が何をしているのか書きます。

function doPost(e) {
  //メイン
}

function isInOrOut(command, column) {
  //inかout以外の文字列だったらエラーにする関数
}

function isEnableWifiName(command, commandColumn, listColumn, wifiList, listEndRow) {
  //端末IDが「list」シートに存在しない時にエラーにする関数
}

function getList(listStartRow, listStartColumn, listEndRow, listEndColumn){
  //端末リストを表示する関数
}

function getListRange(listStartRow, listStartColumn, listEndRow, listEndColumn){
  //「list」シートの使う部分を取得する関数。
}

あらためて見ると結構とっちらかってるので、もっと効率化できそうな気がしてきましたが、とりあえず動いているのでそのままにしておきますw

var command = e.parameter.text.split(' ');

slash commandから取得した引数(パラメタ)です。スペース区切りでcommandという配列に格納しています。

result = 'usage:\n/wifi list\nwifi一覧を表示します。\n\n/wifi wifi端末ID 名前 in|out\nout(貸出)in(返却)を登録します。';

改行コードにハマったのでピックアップ。\nで改行です。なんか最初コピペした時うまく改行されなかったのですが、理由不明。

for(var i=0; i<listEndRow; i++){
    for(var j=0; j<listEndColumn; j++){
      result = result + range.getValues()[i][j] + ' | ';
    }
    result = result + '\n';
  }

for文とか2次元配列とか久しぶりに書いたので、どっちがどっちだかよくわからなくなりました。

var listEndRow = 6;

この数字重要でした。端末数に応じて修正して下さい。やり方次第ではこの値も動的に取得できると思うので、余力があったら試してみて下さい。

実行結果

基礎編と同じく、こちらを保存すると(新規作成での保存をお忘れなく!)slash commandが使えるようになります。

良い感じです!

おわり

いかがだったでしょうか?まずはコピペしてみて動かしてもらって、動きがわかったらカスタマイズしてみるといいと思います!
















この記事が気に入ったらサポートをしてみませんか?