はじめに
わたしの所属するグループでは、細かいチームに分かれてはいるものの、フロントエンド、デザイナー合わせて総勢約40名ほどのメンバーが、同じslackのチャンネルで毎朝勤怠連絡を行います。
リモートワークが中心となり、かつ一人暮らしの社員も多い弊社では、毎日の勤怠チェックはリーダーの必須お仕事になっていますが、40人の中から自分のチームのメンバー(13人)がちゃんと出社しているのかを目視で確認するのは、毎朝それなりに重労働になっていました。
そのため、今年の夏休みに「勤怠連絡を自動でチェックする仕組み(=勤怠お知らせくん)」を作成しました。
今回は、その「勤怠お知らせくん」を3ヶ月運用してみての気づきや、いただいた意見をもとに、アップデートした最新のソースコードなどを共有し、より快適な勤怠チェック生活をご提供したいと思います。
複数人がslackで勤怠連絡をするチームの皆様は、ぜひ導入を検討してみてください。
※本記事は、以前書いたこちらの記事をアップデートしたものになります。
今回作った(アップデートした)もの
勤怠連絡用のslackのチャンネルに所属しているメンバーを取得し、指定の時間までにそのチャンネルで発言していないメンバーをピックアップして、slackで通知してくれる仕組み。
詳細
- スクリプト実行日の朝5:00から10:05までの投稿を取得
- 投稿済みのメンバーのIDを取得し、チャンネルに所属しているメンバーのIDと比較
- チャンネルに所属しているが、本日まだ勤怠連絡を投稿していないメンバーを抽出
- 投稿していない人たちの氏名とテキストをメンション付きでslackチャンネルに投稿する
- 祝日や休日には起動しない判定をする
- 除外リストに設定しているメンバーは比較対象から外す
※当日勤怠チャンネルでお休みの連絡をしたメンバーは拾えませんが、上司にメンション連絡しているはずなのでよしとします
運用の様子
私たちのグループでは業務開始時に、勤怠連絡用のチャンネルに開始連絡を投稿します。
朝10時5分になると、各チームのリーダー宛に、「勤怠お知らせくん」から勤怠連絡状況が投稿されます。リーダーは自分のメンバーの名前があるか、ある場合は報告がなくて問題ないのかを確認し、👍スタンプを押します。
出社するはずのメンバーが勤怠連絡をしていなければ、スレッドなどで報告し、本人に直接連絡するなどをしています。
私のグループは10時に業務開始が通常なのですが、今回は10時05分に動くようにしています。
勤怠お知らせくんが10時00分にチェックしてしまうと、10時00分59秒に勤怠連絡したメンバーも「出勤報告してないよ認定」をされてしまうからです。細かい所ですが、小さな不和をうまないためにも、余裕を持たせてゆるく報告するようにしました。
運用して良かったこと
これはもちろんですが、勤怠確認が圧倒的に楽になりました!!もう、あんなチマチマと確認する作業とはおさらばです。
また、今まで自分のチーム以外のメンバーのお休みはあまり把握できていなかったのですが、デザイナーさんのお休みなども把握でき、不必要なコミュニケーションが減ったと感じています。
(デザイナーさんからも嬉しいお言葉もいただきました)
運用してみての改善点
嬉しいことに、毎月のようにグループへの新規参画があり、チェック用として作成していたメンバーリスト更新の頻度が予想よりも多くなってきました。また3ヶ月のお試し期間を終えリーダー陣にヒアリングした上で、前回なかった以下の機能を追加することにしました。
- メンバーリストを保持するのではなく、チャンネルに所属しているメンバーを取得する
- チャンネルに投稿時に、各チームのリーダーにメンションをつける
導入手順
それでは、slackにアプリを追加する方法から、Google Apps Scriptに記載するコードまで、すべてご紹介します。
大まかな手順としては以下です。
① slackでbotアプリを追加しTokenを取得
② Google Apps Scriptでコーディング
③ Google Apps Scriptを実行(テスト)
④ トリガーを設定
① slackでbotアプリを追加しTokenを取得
-
slack apiの公式サイトの「Create an app」を押下します
-
「Bot Token Scopes(投稿するbotの権限)」と「User Token Scopes(メンバー情報を取得する権限)」をそれぞれ追加していきます
スコープは以下のように選択することができます
Bot Token Scopes には以下2つの権限を付与します。* chat:write * chat:write.public
User Token Scopes には以下6つの権限を付与します。
* channels:history * channels:read * chat:write * groups:history * groups:read * users:read
-
許可後に表示される「User OAuth Token」および「Bot User OAuth Token」を保持しておいてください(後々使用します)
-
「Basic Information」メニューの「Display Information」からアプリの名前やアイコン画像などを変更することができます
② Google Apps Scriptでコーディング
Google Apps Scriptを作成します。
必要なソースコードは後述しますので、基本的には全てコピー&ペーストで使用いただけます。
-
Google Apps Scriptを作成
Googleアカウントでログインし、Google Apps Scriptのホームから「新しいプロジェクト」を作成し、コーディングしていきます。
-
コード.gsの中身を全て削除し、以下のソースコードをそのままコピー&ペーストしてください。
やっていることはざっくり以下です。
- 投稿されたメッセージから、勤怠連絡済みのメンバーを抽出
- slackチャネルに所属しているメンバーを取得
- チャンネル所属メンバーと比較して、投稿していないメンバーを取得
- 除外リストにいるメンバーを報告対象から外す
- 投稿用のテキストの作成
- slackに投稿する
// slack投稿情報 const slackInfo = { token: '保存しておいたUser OAuth Token', bot_token: '保存しておいたBot User OAuth Token', targetChannel: '勤怠チャンネルのチャンネルID', }; // 除外リスト(勤怠連絡が不要だがチャンネルに所属しているメンバー) const exclusionUsers = [ 'U0XXXXXXX', // 勤怠お知らせくん(bot本体のメンバーID) 'XXXXXXXX', // 例)部長 ]; // 通知リスト(slackへ投稿する際メンションをつけてお知らせするメンバー) const notifyUsersList = [ 'XXXXXXXXX', // DESマネジャーのメンバーID 'XXXXXXXXX', // DESリーダーのメンバーID 'XXXXXXXXX', // DESリーダーのメンバーID 'XXXXXXXXX', // FEリーダーのメンバーID 'XXXXXXXXX', // FEマネージャーのメンバーID ]; // Unix時間を取得する function getUnixTime(dateTime) { const date = new Date(dateTime); const millisec = date.getTime(); const sec = millisec / 1000; const time = sec.toString(); return time; } // 本日の日付をY/M/Dの形式で返却する function getToday() { const d = new Date(); const y = d.getFullYear(); const mon = d.getMonth() + 1; const d2 = d.getDate(); const today = y + '/' + mon + '/' + d2; return today; } // 指定された日が営業日か判定する function isWorkday(today) { // 本日の曜日を取得し土日であれば休日(false) const week = today.getDay(); if (week === 0 || week === 6) return false; // 祝日カレンダーを確認する const calJpHolidayUrl = 'ja.japanese#holiday@group.v.calendar.google.com'; const calJpHoliday = CalendarApp.getCalendarById(calJpHolidayUrl); // 祝日カレンダーに本日の日付があれば祝祭日(false) if (calJpHoliday.getEventsForDay(today).length !== 0) return false; // 全て当てはまらなければ営業日(true) return true; } // HTTPリクエスト実行処理 function fetchslackApi(url, options) { try { const res = UrlFetchApp.fetch(url, options); const jsonObj = JSON.parse(res); return jsonObj; } catch (e) { console.error(error); } } // slackからのメッセージを取得する function getPostedMessage(slackInfo) { // 投稿を取得する時間の範囲をタイムスタンプで取得する const oldest = getUnixTime(`${getToday()} 05:00:00`); // 朝5時から const latest = getUnixTime(`${getToday()} 10:05:00`); // 朝10時5分まで // 取得する情報の詳細を設定 const url = 'https://slack.com/api/conversations.history'; const options = { method: 'get', payload: { token: slackInfo.token, channel: slackInfo.targetChannel, oldest: oldest, //この日時から latest: latest, //この日時まで inclusive: true, //oldestとlatestを含めるか true, false }, }; const data = fetchslackApi(url, options); return data.messages; } // slackチャネルに所属しているメンバーを取得 function getChannelUser(slackInfo) { //チャンネルのメンバー取得 const url = 'https://slack.com/api/conversations.members'; const options = { method: 'get', payload: { token: slackInfo.token, channel: slackInfo.targetChannel, }, }; const data = fetchslackApi(url, options); return data.members; } // 引数で取得したメンバーIDからスクリーンネームを取得し配列で返却する function getUserName(idList, slackInfo) { const url = 'https://slack.com/api/users.info'; let array = []; idList.forEach(async (userId) => { const options = { payload: { token: slackInfo.token, user: userId, }, }; const data = fetchslackApi(url, options); array.push(data.user.real_name); }); return array; } // slackにメッセージを投稿する function postChannel(text, slackInfo) { const url = 'https://slack.com/api/chat.postMessage'; const options = { method: 'post', payload: { token: slackInfo.bot_token, channel: slackInfo.targetChannel, text: text, }, }; try { const result = UrlFetchApp.fetch(url, options); } catch (e) { console.error(error); } } // メンションを作成する function createMentionText() { if (notifyUsersList.length === 0) return ''; let str = '<@'; return str.concat(notifyUsersList.join('> <@'), '>'); } // 送信するメッセージを作成する function createSendMessage(noPostedUsers) { // 通知するメンバーのメンションを文字列に変換する const mentions = createMentionText(); // 除外リストをチェックし存在するメンバーは対象から除外する const targetUsers = noPostedUsers.filter((value) => { return !exclusionUsers.includes(value); }); // 送信するテキストの作成 let text = `${mentions}\n【${getToday()}】\n:sun_with_face: おはようございます!:sun_with_face:\n`; // 対象者の有無によって文言を変更する if (targetUsers.length > 0) { const targetUserNameList = getUserName(targetUsers, slackInfo); const users = targetUserNameList.join('さん、'); text += '本日おやすみ、もしくはまだ朝の出勤報告をしていないメンバーは、' + '\n\n' + users + 'さんです!' + '\n\n' + 'ご確認お願いします!'; } else { text += '本日おやすみ、もしくはまだ朝の出勤報告がないメンバーは、いません!' + '\n' + '今日も1日みんなで元気にがんばりましょう〜!!'; } return text; } // 所属メンバーの中から、本日未投稿のメンバーを抽出する function getNotPostedUser(postedUsers, channelUsers) { const notPostedUsers = channelUsers.filter((value) => !postedUsers.includes(value)); return notPostedUsers; } // 取得した投稿済みメッセージからメンバーIDを抽出する function extractPostedUserId(messages) { const userList = []; for (const [key, value] of Object.entries(messages)) { if (typeof value !== 'undefined') { userList.push(value.user); } } return userList; } // 処理実行関数 function run() { // 投稿されたメッセージから、勤怠連絡済みのメンバーを抽出 const messages = getPostedMessage(slackInfo); const postedUsers = extractPostedUserId(messages); // slackチャネルに所属しているメンバーを取得 const channelUsers = getChannelUser(slackInfo); // チャンネル所属メンバーと比較して、投稿していないメンバーを取得 const noPostedUsers = getNotPostedUser(postedUsers, channelUsers); // 投稿用のテキスト作成 const sendText = createSendMessage(noPostedUsers); // slackに投稿する postChannel(sendText, slackInfo); } // 平日に勤怠チェックを動かすトリガー関数(GASのトリガーに設定する) const setTrigger = () => { const today = new Date(); // 営業日でなければ処理終了 if (!isWorkday(today)) return; // 起動時間を10:05:00に時刻を設定 today.setHours(10); today.setMinutes(5); today.setSeconds(0); ScriptApp.newTrigger('run').timeBased().at(today).create(); };
※コピペしやすいように1ファイルにまとめましたが、GitHubではファイルを分割しています
-
コピペした後に、以下の箇所を修正(カスタマイズ)してください。
⑴slack投稿情報
slackの情報を設定します。// slack投稿情報 const slackInfo = { token: '保存しておいたUser OAuth Token', bot_token: '保存しておいたBot User OAuth Token', targetChannel: '勤怠チャンネルのチャンネルID', };
修正箇所 修正内容 token ①の11にて取得した「User OAuth Token」 bot_token ①の11にて取得した「Bot User OAuth Token」 targetChannel slackの勤怠チャンネルのチャンネルID※ ※slackのチャンネルIDの取得方法
・ 対象のチャンネルで右クリック
・ 表示されたモーダルの一番下からコピー
⑵除外リスト
// 除外リスト(勤怠連絡が不要だがチャンネルに所属しているメンバー) const exclusionUsers = [ 'U0XXXXXXX', // 勤怠お知らせくん(bot本体のメンバーID) 'XXXXXXXX', // 部長 ];
チャンネルに所属しているが、勤怠管理が不要なメンバーのメンバーIDを設定します。
また、bot自身のメンバーIDも設定する必要があるため、メニューの「App」から今回作成したアプリを選択し
同じく右クリックで「アプリの詳細を表示する」から取得してください。
⑶通知リスト
// 通知リスト(slackへ投稿する際メンションをつけてお知らせするメンバー) const notifyUsersList = [ 'XXXXXXXXX', // DESマネジャーのメンバーID 'XXXXXXXXX', // DESリーダーのメンバーID 'XXXXXXXXX', // DESリーダーのメンバーID 'XXXXXXXXX', // FEリーダーのメンバーID 'XXXXXXXXX', // FEマネージャーのメンバーID ];
勤怠をお知らせするときに、メンションしたいメンバーのメンバーIDを設定します。
通知が不要な場合は以下のように修正してください。// 通知リスト(slackへ投稿する際メンションをつけてお知らせするメンバー) const notifyUsersList = [];
⑷チェッカーの通知時間
// 起動時間を10:05:00に時刻を設定 today.setHours(10); today.setMinutes(5); today.setSeconds(0);
上記を起動したい時間に変更してください。
9時ちょうどに通知したい場合は以下のようになります。// 9時ちょうどに勤怠お知らせしてほしい場合の設定 today.setHours(9); today.setMinutes(0); today.setSeconds(0);
また、上記を変更した場合は、投稿を取得する範囲の時間も一緒に変更してください。
const oldest = getUnixTime(`${getToday()} 05:00:00`); // 朝5時から const latest = getUnixTime(`${getToday()} 10:05:00`); // 朝10時5分まで
③Google Apps Scriptを実行(テスト)
ここまで設定が終わったら、実際に動くかテストしてみます。Google Apps Scriptページにて、「run」関数を指定して隣の「デバッグ」をクリックします。(実際に投稿されてしまうのでご注意ください!)
指定したslackチャンネルに投稿されれば成功です!おめでとうございます!
うまく投稿されなかった場合は、「⑴slack投稿情報」に設定した情報が間違っていないか再度確認してみてください。
④ トリガーを設定
ここまできたら後一息です!
毎日自動で動くように、Google Apps Scriptページで「トリガー」を設定します。
②の「⑷チェッカーの通知時間」にて自動投稿される時間は指定しているので、その時間より前の時間で動くようにしておくだけでOKです。
項目 | 設定内容 |
---|---|
実行する関数を選択 | setTrigger |
イベントのソースを選択 | 時間主導型 |
時間ベースのトリガーのタイプを選択 | 日付ベースのタイマー |
時刻を選択 | 通知時間より前ならOK。 午前5時〜6時などにしておくと安心です。 |
以上で設定は完了です!
明日の朝、自動で勤怠がチェックされて通知される喜びを味わっていただければと思います!
使用したslack api
今回使用したslack apiをまとめています。
slack api | 内容 |
---|---|
チャンネルに投稿されたメッセージを取得する | https://api.slack.com/methods/conversations.history |
チャンネルに所属するメンバーを取得する | https://slack.com/api/conversations.members |
メンバーIDからメンバー情報を取得する | https://slack.com/api/users.info |
指定のチャンネルにメッセージを投稿する | https://slack.com/api/chat.postMessage |
おわりに
この仕組み自体は、今年の夏に作成し運用を開始できていたのですが、「とりあえず一旦完成させて、後からアップデートすればいいか」根性で作っていたので、ソースコードも大変お粗末なものになっておりました。
今回のアドベントカレンダーをきっかけに、ソースコードの見直しと、ちょっとだけ煩わしかった「毎月のメンバーリストの更新」が解消できてとても良かったです。
また、「困っている人がいたら、絶対導入してほしい!」と思っていたので、手順も含めてかなり細かく丁寧に書いたつもりです。「車輪の再発明」を行ってしまった感は否めないですが、どうかこの記事を見て救われる人がいますように🙏
めんどうだな、なんとかしたいなと思ったものを自分の力で解決できるのは、この仕事の醍醐味だなあと改めて感じました。
おまけ
Google Apps ScriptのコードをGitHubで管理しようと思ったときに、Chrome拡張の「Google Apps Script Github アシスタント」というものがあることを知りました。
Chrome拡張を追加し、ログインするだけでGoogle Apps Script上からリポジトリを作成したりPushすることができます。
Google Apps Script GitHub アシスタント
とても便利だったので、機会があれば使ってみてください!
参考サイト