Slack×GASで作った勤怠Botの話#4

UG Advent Calendar 2020 の4日目の記事です。

どうもbarusuです。知らない方は初めまして。
昨日の続きです。
フィクションと思って読んでいただければ幸いです。

Slack×GASで作った勤怠Botの話 #4

2020年5月。
良くも悪くもコロナ禍による新しい生活様式が定着しはじめた頃のお話。

―――――――ちょっとさ、勤怠システム作れない?

すべては法務部長の一言から始まった…

全10話分の4話目となります。
 2020/12/01:導入,初期の要望
 2020/12/02:設計
 2020/12/03:構築その1 :SlackBotを作る
 2020/12/04:構築その2 :GASを書く(基本メソッド編)← 今日はココ
 2020/12/05:構築その3 :GASを書く(ユーティリティ編)
 2020/12/06:構築その4 :GASを書く(機能実装編:打刻処理)
 2020/12/07:構築その5 :GASを書く(機能実装編:リマインド機能)
 2020/12/07:構築その6 :色々書く(機能実装編:ユーザー管理機能,勤怠修正機能)
 2020/12/08:構築その7 :関数とか色々(書ききれなかったやつまとめて)
 2020/12/09:単体,ユーザーテスト
 2020/12/10:そして伝説へ...

Barusu
「さーて今日も頑張るぞい」

Barusu
「前回はSlackBotの作成とGASをAPI公開して、BotへのDMをきっかけにGAS発火するとこまでやったんだっけ。

Barusu
「一旦、要件定義したやつを元に実装するものをまとめておこう(≒簡易設計)」

▼要件リスト
要望
 ・業務委託の勤怠を把握したい
 ・リアルタイムで残業量を把握し、予測を立てたい
 ・ライセンス契約はNG(ここ交渉したけどだめだった)
要件
 ・インターフェースはSlackを用いること
 ・CSVまたはExcel,スプレッドシートで出力ができること
 ・位置情報を取得できること(ユーザーの事前承認必須)
 ・打刻忘れリマインド機能があること(土日祝日に対応すること)
 ・0:00またぎ(日付またぎ)の打刻は考慮しない(発生頻度が高い場合は対応」検討する)
 ・ユーザーの打刻修正は都度申請とすること
 ・管理監督者が勤怠を承認できること
 ・勤務時間予測を立て、しきい値を超える場合はアラートを管理者、経理、法務にメール通知する
 ・契約に応じた時間丸めを設定する
 ・中抜け時間を追加できること
 ・最終的に100人程度が同時に使えるようにすること
 ▼簡易設計
下準備:SlackAccessTokenを設定しておく
基礎
 投稿を受け取り、スプレッドシートに記載してSlackに返す
 投稿したユーザー名をSlackAPIを用いて取得
 ユーザーにDMを送る
 投稿内容に応じて異なる処理を実行する
---(今日はココまで)---
各種ユーティリティ
 日付のフォーマットを変更する
 スプレッドシートの範囲から連想配列を作成する
 連想配列を二次元配列に変換する
 祝日一覧を取得してシートに転記
 GoogleWorkspaceのユーザー一覧を取得してシートに転記

機能実装予定のもの
1. 打刻機能
 a.  出勤、退勤、中抜け に反応する仕組みを作る
 b. 出勤打刻を受け取ってスプレッドシートに今の時刻を記載する
  例外:出勤済ならば「出勤してます」と返す
 c. 中抜けを受け取ってスプレッドシートに記載する
  例外:未出勤ならば「出勤してません」と返す
  例外:時刻データが不正の場合は「形式が正しくありません」と返す
 d. 退勤打刻を受け取ってスプレッドシートに記載する
  例外:未出勤ならば「出勤してません」と返す
  例外:退勤済ならば「退勤済ですが上書きしますか?」と返す
2.ユーザー管理機能
 a. 初めて投稿したユーザーは勤怠ユーザーリストに追加する
 b. Gsuiteからメールアドレスを取得し、Slackユーザーリストと紐付ける
3. リマインド機能
 a. 今日が平日ならば→指定時間帯に未打刻のユーザーにDMを送る
 b. 退勤をしていないユーザーにDMを送る
4. 勤怠修正機能
 a. Googleフォームで修正申請を受付
 b. 指定の承認者のみが承認できるようにする
 b. 別の勤怠修正フォームから承認された勤怠データとデータを合流させる
5. アラート機能
 a. 月末残業予測値を算出し、しきい値を超えた者はアラートを出す 

Barusu
「多くね…………??死ねるわこれ」

Barusu
「まぁ前に進むしかないか。馬だしな」

下準備:SlackAccessTokenを設定しておく

Barusu
「Slackのデータを取得したり、ユーザーにDM送るときに必要になるのよね。このTokenは外部に漏らしちゃだめよー」

Barusu
「前回作ったSlackBotの管理画面からAccessTokenをコピーする」

画像2

Barusu
「[ファイル]→[プロジェクトのプロパティ]をクリック」

画像3

Barusu
「[スクリプトのプロパティ]タブをクリックしてプロパティを追加。
こうしておくことで、このGASの編集権限持った人以外はTokenを知ることができなくなるって寸法。スプレッドシートまるごとコピーされても大丈夫」

画像4

Barusu
「以後、プロパティに保持したTokenを取得したい場合は以下の記述をすれば取得できる」

PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');

基本機能編

Barusu
「では次に、汎用的な部分をささっと実装していく」

投稿を受け取り、スプレッドシートに記載してSlackに返す

Barusu
「はいとりあえずソースはこんな感じ。
はじめのtry~catch文は前回追加したやつ。それに追記する形で実装していくのである」

function doPost(e) {
 //SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue("ok");
 // Event API Verification 時のコード
 try {
   var json = JSON.parse(e.postData.getDataAsString());
   if (json.type == "url_verification") {
     return ContentService.createTextOutput(json.challenge);        
   }
 } catch (ex) {
 }    
 
 try {
   var json = JSON.parse(e.postData.getDataAsString());
   
   if (json.type == "event_callback" && json.event.type == "message") {
     
     var userId = json.event.user;
     var channel = json.event.channel;
     var text = json.event.text;

     SpreadsheetApp.getActiveSheet().getRange(1, 1).setValue("発言したユーザ名:" + userId);
     SpreadsheetApp.getActiveSheet().getRange(1, 2).setValue("発言した内容:" + text);
     SpreadsheetApp.getActiveSheet().getRange(1, 3).setValue("チャンネルID:"+ channel);
     
}     

Barusu
「一応解説しておく」

Barusu
「SlackからPostされたデータの中身については↓のリファレンスを見ればOK」

Barusu
SpreadsheetApp.getActiveSheet() はGASを外部から呼び出したとき、最も左に位置するシートを指定する。
これ忘れてると意外なバグの元になったりするので要注意(2回ほどあり)

投稿したユーザー名をSlackAPIを用いて取得

Barusu
「メールアドレスと関連付けるときに必要なのよね。
ソースはこんな感じ。const token の部分は下準備で設定した、プロジェクトのプロパティの値。
URLを指定してるのは、Slackがもともと用意してくれてるAPIを使っているから」

function getUserNameFromId(slackId){
 var slack_team  = 'slack-dev';
 const token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');  
 
 var listurl = 'https://' + slack_team + '.slack.com/api/users.info?token=' + token +'&user='+slackId;
 var listres = UrlFetchApp.fetch(listurl);
 var listjson = JSON.parse(listres.getContentText());
 return listjson.user.name;
}

ユーザーにDMを送る

Barusu
「親の顔より見たソースコード。まじでよく使うよねこのコード。
channnelの引数にUserIDを渡すとDMで送れるんだけどこれ意外と知らない人いるんじゃないかな?」

function postMessage(channel, text) {
 
 const url = "https://slack.com/api/chat.postMessage";
 const token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');  
 const payload = {
   "token" : token,
   "channel" : channel,
   "text" : text
 };
 
 const params = {
   "method" : "post",
   "payload" : payload
 };
 
 // Slackに投稿する
 UrlFetchApp.fetch(url, params);
 
}

投稿内容に応じて異なる処理を実行する

Barusu
「まぁ、なんてことのないただの正規表現ですわ」

var json = JSON.parse(e.postData.getDataAsString());
var text = json.event.text;
~中略~
     // 出勤
     var commandSignInSent =/(モ[ー〜]+ニン|も[ー〜]+にん|おっは|おは|へろ|はろ|ヘロ|ハロ|hi|hello|morning|出勤)/.test(json.event.text);
     
     // 退勤
     var commandSignOutSent = /(バ[ー〜ァ]*イ|ば[ー〜ぁ]*い|おやすみ|お[つっ]ー|おつ|さらば|お先|お疲|帰|乙|bye|night|(c|see)\s*(u|you)|退勤|ごきげんよ|グ[ッ]?バイ)/.test(json.event.text);
     
     // 中抜け時間指定 想定: 中抜け 2:00
     var commandIntervalSent = /(中抜|(ぬ|なか|休憩))/.test(json.event.text);
     
     var userCheck = userId == "BotUserIdを入力する";
          
     if(userCheck == false ){

       if(commandSignInSent == true){
         var messagePost = "";
         var command = "SignIn";
         var intervalTime = "";
       }
       else if(commandSignOutSent == true){
         var messagePost = "";
         var command = "SignOut";
         var intervalTime = "";
       }  
       else if(commandIntervalSent == true){
         var messagePost = "";
         var intervalTime = text.match(/.[0-9]:[0-9]{2}/g);
         var command = "Interval";
         SpreadsheetApp.getActiveSheet().getRange(9, 6).setValue(intervalTime);
       }
       else {
         var messagePost = "すみません。よくわかりません。\n私ができることは以下となります。\n\n「おはよう」→ 今の時刻で出勤打刻\n「おつかれ」→今の時刻で退勤打刻\n「中抜け hh:mm」→ 今日の打刻にhh:mm の中抜け時間を追加";// 投稿するメッセージ
       }
       

Barusu
「ある程度、Botへのコメントは幅を持たせられるようにした。
正規表現に一致するかの評価ルールを作成して、Botへのコメント内容と比較した結果を変数に格納、その下のIf文で各分岐とした感じ。
Swith文じゃなくてIf文にしたのはなんでだっけ...忘れたw」

Barusu「一旦今日はココまでかな...」

Barusu
「っとまあわかっちゃいたけど、一人で1ヶ月32hくらいで作る量じゃないよなーこれ」

次回予告

ソースコードつけたら5000文字も書いちゃった!
案の定、全5回では終わらないと悟ったぞ!
もう5回ほど延長して全10回にしたほうが良いぞ!
ということでもうちょっとだけ続くんじゃ。

次回、「1万字を超えてゆけ」。デュエルスタンバイ!

画像1


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