GASで毎日の勤怠連絡（Slack）をチェックする仕組みを作ってみた話

　
本記事をアップデートした最新版の記事を投稿しました。
本記事で諦めていることもちゃんとできるようになっているので、ぜひこちらをご確認ください！！

https://qiita.com/tmryr/items/45dafc71d951553523fb


---
## 作ったもの

**「おはようチェッカー（雑）」**

30名近くいるメンバーが、全員スラックで出勤報告（おはようございます）をしているか毎朝チェックするのが大変だったので、（朝5時以降、）朝10時までの時点で勤怠報告をしていないメンバーを抽出してスラックで教えてくれるやつ。


## やりたきこと
* スクリプト実行日の朝5:00から10:05までの投稿を取得
* 投稿したユーザーのユーザーIDを取得し、既存のリストと比較
* 既存リストに存在するのに、本日投稿していないユーザーを抽出
* 指定のスラックチャネルに、投稿していない人たちの氏名＆テキストを投稿する
* 祝日や休日は送らない判定をする

## やらなかったこと
* 投稿テキスト内の「おはようございます」の文言チェック
  * 理由
       * メンバーに文言を強制するのも微妙だなあと思ったから
       * 「体調不良で休みます」とか、いつもとちょっと違う文章の場合もあるなあと思ったから
       * とりあえず一旦完成させて、後からアップデートすればいいやと思ったから
* チャネルに所属しているユーザーをAPIで取得
    * 理由
        * 所属しているけど投稿しない（部長など）がいる場合、結局チェックしないリストみたいなものを作らなければいけないと思ったから
        * とりあえず一旦完成させて、後からアップデートすればいいやと思ったから
        * 本当はチャンネルに所属するユーザーはAPIで取得したかった。
            * 雑リストを作ってしまったから、新規参画者、もしくは離任者がいる場合はメンテしないといけない。イケてない。（課題）



## 手順
ざっくりこんな感じです

  ① SlackでOAuth Token取得
  ② Google Apps Scriptでコーディング
  ③ Google Apps Scriptを実行
  ④ トリガーを設定


#### ① SlackでOAuth Token取得
これはいろんな人がいろんなところで解説しているので、そちらを見ていただければと思います！！
私が参考にした記事を貼っておきます。

https://best-cloud.jp/slack-message-log-auto-save-gas/#Google_Apps_Script

上の記事には書いてありませんが「Auth Scope」で「chat:write」も設定してあげてください。
これがないと最後にスラックへの投稿ができません。
![スクリーンショット 2022-08-12 0.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982800/76ecf44a-fa22-be7b-3add-adcd6be42276.png)

#### ② Google Apps Scriptでコーディング
Google Apps Scriptを作成します。
下記のURLから「新しいプロジェクト」を作成し、コーディングしていきます。

https://script.google.com/home

実際のソースコードをを貼っておきます。

コピペする場合気をつける点は以下です。
（変更した方がいい箇所はコード上に【TODO】のコメント入れておきます）
* トークンやチャンネルIDなどは仮のものを設定しているので設定し直してください。
* 上部で宣言している`userList`の`userName`と`userId`を自身で設定してください。（ごめんて）
    * `userId`は個々のプロフィールから取得するか、管理者であれば以下の方法で一括取得できます。（ごめんて）
    * https://auto-worker.com/blog/?p=27&#toc_id_4
* 投稿したいtextを変更したい場合は`createText`関数の中の`text`変数の中を適当に変更してください。
* 朝10:05に動かすようにしているので、時間を変えたい場合はslackTTTriggerSet()の時間設定部分を変更してください
```JavaScript	
// 10:05:00に時刻を設定
next.setHours(10);
next.setMinutes(5);
next.setSeconds(0);
```
* 朝5:00から10:00までの間に投稿されたものという設定をしているので、時間変えたい場合は赤枠の時間を修正してください
```JavaScript
// 投稿を取得する時間の範囲をタイムスタンプで取得する
const oldest = getUnixTime(`${getToday()} 05:00:00`);
const latest = getUnixTime(`${getToday()} 10:00:00`);
```
以上、それではソース！

```javascript:ohayo.gs

// 【TODO】 取得したトークンと、チェックしたいユーザーの名前とID要修正
const token = 'xoxp-XXXXXXX';
const userList = [
	{ userName: '山田 太郎', userId: 'UXL8XXXX' },
	{ userName: '田中 花子', userId: 'UXXXXCXXXX' },
	{ userName: '高橋 太郎', userId: 'UXSCXXXXX' },
	{ userName: '鈴木 花子', userId: 'UXX9QXXXXXX' }
];

function run() {
	// スラックからメッセージを取得
	const messages = getPost();

	// 取得したメッセージからユーザーを抽出
	const postedUserList = getPostedUser(messages);

	// 定義したuserListと比較して投稿していないユーザーを取得
	const notPostedUserList = getNotPostedUser(postedUserList);

	// スラックに投稿する
	sendSlackToTimeTable(notPostedUserList);
}

// 指定された日が営業日か（営業日 = 「土日でない」「祝日カレンダーに予定がない」）
// 営業日 = true
const isWorkday = (targetDate) => {
	// targetDate の曜日を確認、週末は休む (false)
	var rest_or_work = ['REST', 'mon', 'tue', 'wed', 'thu', 'fri', 'REST']; // 日〜土
	if (rest_or_work[targetDate.getDay()] == 'REST') {
		return false;
	}

	// 祝日カレンダーを確認する
	var calJpHolidayUrl = 'ja.japanese#holiday@group.v.calendar.google.com';
	var calJpHoliday = CalendarApp.getCalendarById(calJpHolidayUrl);
	if (calJpHoliday.getEventsForDay(targetDate).length != 0) {
		// その日に予定がなにか入っている = 祝祭日 = 営業日じゃない (false)
		return false;
	}

	// 全て当てはまらなければ営業日 (True)
	return true;
};
// Unix時間を取得する
function getUnixTime(dateTime) {
	var date = new Date(dateTime);
	var milsec = date.getTime();
	var sec = milsec / 1000;
	var time = sec.toString();
	return time;
}
// 本日の日付をY/M/Dの形式で返却する
function getToday() {
	var d = new Date();
	var y = d.getFullYear();
	var mon = d.getMonth() + 1;
	var d2 = d.getDate();
	var today = y + '/' + mon + '/' + d2;
	return today;
}
// 本日のスラックのメッセージを取得する
function getPost() {
    // 【TODO】 必要であれば取得したい期間の時間帯を修正
	// 投稿を取得する時間の範囲をタイムスタンプで取得する
	const oldest = getUnixTime(`${getToday()} 05:00:00`);
	const latest = getUnixTime(`${getToday()} 10:00:00`);

	// 指定した日のslackの投稿を取得
	const url = 'https://slack.com/api/conversations.history';

    // 【TODO】 要修正
    // 投稿を取得したいチャンネルのIDを設定
	const channel = 'XXXXXXXXX';

	const options = {
		method: 'get',
		payload: {
			token: token,
			channel: channel,
			oldest: oldest, //この日時から
			latest: latest, //この日時まで
			inclusive: true, //oldestとlatestを含めるか true, false
		},
	};

	const res = UrlFetchApp.fetch(url, options);
	const jsonObj = JSON.parse(res);

	return jsonObj.messages;
}

// 本日の投稿済みのユーザーを取得する
function getPostedUser(messages) {
	const postedUser = [];
	for (const [key, value] of Object.entries(messages)) {
		if (typeof value !== 'undefined') {
			// MEMO:「おはよう」のテキスト判定をいれるならこの場所
			postedUser.push(value.user);
		}
	}
	return postedUser;
}

// 本日未投稿のユーザーを取得する
function getNotPostedUser(postedUserList) {
	const notPostedUser = [];
	for (const [key, value] of Object.entries(userList)) {
		if (typeof value !== 'undefined') {
			if (postedUserList.indexOf(value.userId) === -1) {
				notPostedUser.push(value.userName);
			}
		}
	}
	return notPostedUser;
}

// Slackに投稿する
function sendSlackToTimeTable(notPostedUserList) {
	function createText(list) {
        // 【TODO】 好みに合わせて修正 （ここでは、ひとりもいなかった場合／ひとりでもいた場合の共通テキストを設定しています）
		let text =
			'【' + getToday() + '：自動投稿です】' + '\n' + ':sun_with_face: ' + 'おはようございます！:sun_with_face:' + '\n';

		if (list.length > 0) {
            // 一人でもリストにいる場合のテキストを設定
			const notPostedUsers = list.join('さん、');

            // 【TODO】 好みに合わせて修正 （上で設定したテキストに続ける文言を設定してます）
			text +=
				'本日おやすみ、もしくはまだ朝の出勤報告がないメンバーは、' +
				'\n\n' +
				notPostedUsers +
				'さんです！' +
				'\n\n' +
				'ご確認お願いします！';
		} else {
            // 全員勤怠連絡済みの場合のテキストを設定

            // 【TODO】 好みに合わせて修正 （上で設定したテキストに続ける文言を設定してます）
			text +=
				'本日おやすみ、もしくはまだ朝の出勤報告がないメンバーは、いません！' +
				'\n' +
				'本日も元気にがんばりましょう〜！！';
		}
		return text;
	}

	const url = 'https://slack.com/api/chat.postMessage';

    // 【TODO】 投稿したいチャンネルのIDを設定します
	const channel = 'XXXXXXXXXX';
	const text = createText(notPostedUserList);
	const options = {
		method: 'post',
		payload: {
			token: token,
			channel: channel,
			text: text,
		},
	};
	UrlFetchApp.fetch(url, options);
}


// 呼び出し処理
const sendOhayoExec = () => {
  var today = new Date ();
  // debug のために任意の日付を仕込む (year,month-1,day,hour,min,sec)
  /* today = new Date (2022, 6, 16, 10, 0, 0);
  today = new Date (today.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })); */

  // 営業日であれば実行
  if (isWorkday (today) == true) {
    run();
  }
}

// 正確な時間でのトリガー追加
function slackTriggerSet() {
	// 時間登録処理
	// 現在の日付を取得
	const next = new Date();

	// 翌週の日付に変換
	next.setDate(next.getDate());

    // 【TODO】 好みに合わせて修正 （処理を実行したい時間を設定します）
	// 10:05:00に時刻を設定
	next.setHours(10);
	next.setMinutes(5);
	next.setSeconds(0);

	// sendTTExec:00に実行するトリガー作成
	ScriptApp.newTrigger('sendOhayoExec').timeBased().at(next).create();
}


```

#### ③ Google Apps Scriptを実行

「デバッグ」の右側が「Run」になっている状態で「デバッグ」してみます。
![スクリーンショット 2022-08-12 23.04.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982800/741cf641-c82f-3d78-9136-1153e6185cc8.png)

指定したスラックチャンネルに投稿されれば成功です〜！

![スクリーンショット 2022-08-15 18.41.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982800/133652d1-5ced-4712-c041-d0dfe863b603.png)

全員報告済みの場合はこんな感じ！
![スクリーンショット 2022-08-15 18.13.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982800/e49271de-cc64-2b00-1195-ca8468fd43fa.png)


#### ④ トリガーを設定

毎日10:00に実行してほしいので、トリガーを設定します。
多分こんな感じで良いはず。（試してダメだったら修正します）
![スクリーンショット 2022-08-12 23.11.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982800/2a628916-ccf3-bd3b-d281-1ea469ba5fee.png)


## おわり
改善余地は（かなり）あるものの、とりあえず毎日の微妙なストレスからは解放される気がします・・・！
「あー、やっぱ不便だわ」もしくは「暇で暇で溶けそう」とおもったら改修します。
ソースはちゃんとリファクタしてないのでおかしなところあると思います。
ただ、一応動いては、いる（エンジニアが一番やっちゃいけないやつ）
コメントいただけたら直しますおてやわらかによろしくお願いします。

おわり

#### 一部改修しました（記事も修正済み）
* 全員勤怠連絡済みの時の文言追加
* 正確な時間で実行されるように機能追加
    * 実行時間を10:05に変更


## 参考URL
色々助けていただいた＆コードも一部真似させてもらった@nanamin_yamakawa さんありがとう！！！

https://api.slack.com/methods

https://best-cloud.jp/slack-message-log-auto-save-gas/#Google_Apps_Script

https://qiita.com/okumurakengo/items/cbf7644a520b066ad3a3

https://dev.classmethod.jp/articles/slackapi-message-history/

└→ 2021年の2月以降 `channels.history`が廃止されたことを説明してくれていてとても助かりましたmm

https://auto-worker.com/blog/?p=27