22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NIJIBOXAdvent Calendar 2022

Day 17

30人以上いるグループの勤怠連絡チェックを自動化した話

Last updated at Posted at 2022-12-16

はじめに

わたしの所属するグループでは、細かいチームに分かれてはいるものの、フロントエンド、デザイナー合わせて総勢約40名ほどのメンバーが、同じslackのチャンネルで毎朝勤怠連絡を行います。

リモートワークが中心となり、かつ一人暮らしの社員も多い弊社では、毎日の勤怠チェックはリーダーの必須お仕事になっていますが、40人の中から自分のチームのメンバー(13人)がちゃんと出社しているのかを目視で確認するのは、毎朝それなりに重労働になっていました。

そのため、今年の夏休みに「勤怠連絡を自動でチェックする仕組み(=勤怠お知らせくん)」を作成しました。

今回は、その「勤怠お知らせくん」を3ヶ月運用してみての気づきや、いただいた意見をもとに、アップデートした最新のソースコードなどを共有し、より快適な勤怠チェック生活をご提供したいと思います。

複数人がslackで勤怠連絡をするチームの皆様は、ぜひ導入を検討してみてください。

※本記事は、以前書いたこちらの記事をアップデートしたものになります。

今回作った(アップデートした)もの

勤怠連絡用のslackのチャンネルに所属しているメンバーを取得し、指定の時間までにそのチャンネルで発言していないメンバーをピックアップして、slackで通知してくれる仕組み。
勤怠おしらせんくん.png

詳細

  • スクリプト実行日の朝5:00から10:05までの投稿を取得
  • 投稿済みのメンバーのIDを取得し、チャンネルに所属しているメンバーのIDと比較
  • チャンネルに所属しているが、本日まだ勤怠連絡を投稿していないメンバーを抽出
  • 投稿していない人たちの氏名とテキストをメンション付きでslackチャンネルに投稿する
  • 祝日や休日には起動しない判定をする
  • 除外リストに設定しているメンバーは比較対象から外す

※当日勤怠チャンネルでお休みの連絡をしたメンバーは拾えませんが、上司にメンション連絡しているはずなのでよしとします

運用の様子

私たちのグループでは業務開始時に、勤怠連絡用のチャンネルに開始連絡を投稿します。
スクリーンショット 2022-12-10 15.18.00.png
朝10時5分になると、各チームのリーダー宛に、「勤怠お知らせくん」から勤怠連絡状況が投稿されます。リーダーは自分のメンバーの名前があるか、ある場合は報告がなくて問題ないのかを確認し、👍スタンプを押します。

出社するはずのメンバーが勤怠連絡をしていなければ、スレッドなどで報告し、本人に直接連絡するなどをしています。
スクリーンショット 2022-12-07 10.01.53.png
私のグループは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を取得

  1. slack apiの公式サイトの「Create an app」を押下します
     1.png

  2. 「From scratch」を選択します
    スクリーンショット 2022-12-07 22.50.20.png

  3. App Nameにお好きなアプリ名、その下のプルダウンからアプリを導入したいワークスペース名を選択します
    スクリーンショット 2022-12-07 22.50.42.png

  4. 開いた画面の下部にある「Bots」を押下します
    スクリーンショット 2022-12-07 22.57.48.png

  5. 「Review Scopes to Add」を押下し、スコープを設定します
    スクリーンショット 2022-12-07 22.58.12.png

  6. 「Bot Token Scopes(投稿するbotの権限)」と「User Token Scopes(メンバー情報を取得する権限)」をそれぞれ追加していきます
    2.png
    スコープは以下のように選択することができます
    スクリーンショット 2022-12-07 23.14.55.png
    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
    
  7. 権限追加後、「App Home」メニューからBotの設定を行います
    スクリーンショット 2022-12-07 23.54.22.png

  8. 勤怠をお知らせしてくれるbotの名前と、メンバー名を入力します(なんでもOKです)
    スクリーンショット 2022-12-07 23.56.43.png

  9. botの設定が完了したので、アプリのインストールを行います
    スクリーンショット 2022-12-07 23.59.40.png

  10. ワークスペースにアクセスする「許可する」を押下します
    スクリーンショット 2022-12-07 23.59.55.png

  11. 許可後に表示される「User OAuth Token」および「Bot User OAuth Token」を保持しておいてください(後々使用します)
    スクリーンショット 2022-12-08 0.01.58.png

  12. 「Basic Information」メニューの「Display Information」からアプリの名前やアイコン画像などを変更することができます
    スクリーンショット 2022-12-08 0.16.44.png

② Google Apps Scriptでコーディング

Google Apps Scriptを作成します。
必要なソースコードは後述しますので、基本的には全てコピー&ペーストで使用いただけます。

  1. Google Apps Scriptを作成
    Googleアカウントでログインし、Google Apps Scriptのホームから「新しいプロジェクト」を作成し、コーディングしていきます。
    スクリーンショット 2022-12-09 17.05.24.png

  2. 「新しいプロジェクト」を押下すると以下のようなページが表示されます。
    スクリーンショット 2022-12-09 17.05.24.png

  3. コード.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ではファイルを分割しています

  4. コピペした後に、以下の箇所を修正(カスタマイズ)してください。

    ⑴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の取得方法
    ・ 対象のチャンネルで右クリック
    スクリーンショット 2022-12-09 17.29.32.png
    ・ 表示されたモーダルの一番下からコピー
    スクリーンショット 2022-12-09 17.30.21.png

    ⑵除外リスト

    // 除外リスト(勤怠連絡が不要だがチャンネルに所属しているメンバー)
    const exclusionUsers = [
    	'U0XXXXXXX', // 勤怠お知らせくん(bot本体のメンバーID)
    	'XXXXXXXX', // 部長
    ];
    

    チャンネルに所属しているが、勤怠管理が不要なメンバーのメンバーIDを設定します。

    また、bot自身のメンバーIDも設定する必要があるため、メニューの「App」から今回作成したアプリを選択し
    スクリーンショット 2022-12-09 17.47.18.png
    同じく右クリックで「アプリの詳細を表示する」から取得してください。
    スクリーンショット 2022-12-09 17.50.02.png

    ⑶通知リスト

    // 通知リスト(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」関数を指定して隣の「デバッグ」をクリックします。(実際に投稿されてしまうのでご注意ください!)
スクリーンショット 2022-12-09 18.18.02.png
指定したslackチャンネルに投稿されれば成功です!おめでとうございます!

うまく投稿されなかった場合は、「⑴slack投稿情報」に設定した情報が間違っていないか再度確認してみてください。

全員が勤怠連絡済みの場合は以下のような投稿になります。
スクリーンショット 2022-12-09 18.22.34.png

④ トリガーを設定

ここまできたら後一息です!
毎日自動で動くように、Google Apps Scriptページで「トリガー」を設定します。
スクリーンショット 2022-12-09 18.29.32.png
②の「⑷チェッカーの通知時間」にて自動投稿される時間は指定しているので、その時間より前の時間で動くようにしておくだけでOKです。

スクリーンショット 2022-12-09 18.28.55.png

項目 設定内容
実行する関数を選択 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することができます。
スクリーンショット 2022-12-09 17.05.24.png
Google Apps Script GitHub アシスタント

とても便利だったので、機会があれば使ってみてください!
参考サイト

22
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?