見出し画像

[GAS][slack]営業日カウントダウン通知botを作っている途中 その1


その2はこちら


注意書き

多分、完成したらまた改めてnoteにまとめます。
作成中の混乱のままに大放流。
自分の整理のためって感じ。
一応動くけど、もうちっとどうにかしたいぞ。


前書き

ノンプロ研にて、初心者向けプログラミング講座【GASコース特別編】が実施されました。講座内容の改廃に伴って、現在は初級コースの最後にAPIも盛り込まれるようになりました。以前のコースではAPI回が無かった講座を受講した人もいたため、その部分だけ受講したいという要望に応えて開催されたのが【GASコース特別編】でした。多分。

2022年4月6日講義、2022年4月25日卒業LTというなかなかのスケジュールです。みなさんそれぞれ初級や中級の受講履歴があるとはいえ、1回受講して、2週間ちょっとしたらもう卒業LT発表です。

今回、私はこの特別編のホスト(Zoom進行、運営スタッフ的な役回り)を拝命いたしました。ホストも卒業LTでなんかしら発表しないとなので、どうしよっかな~。SlackAPIともうちっと仲良くなりたいな~、Slackでどうにかする感じのものを作りたいと思います。

やりたいこと

今月の残り営業日数と年内の残り営業日数を、営業日のみslackで通知する。

土日祝日除いて、年内or月内にあと何営業日あるか、slackの特定のチャンネルに通知してくれるbotを作ろうと思いまーす。

完成形のイメージ

なお、すでにこういうのもあります。


営業日関係なく毎日カウントダウンするなら、こういうのもいいと思います。

また、Slackのリマインダーで平日のみというのもあるので、簡単なものはそれでもいいかもですねー。

用意するもの(例)

・Slackワークスペース(自分がオーナーや管理者であるもの)
・スプレッドシート(祝日リストと営業日の計算する用)
・GAS(スプレッドシートのコンテナバインドスクリプト)

こんな感じだろか。

現在のコード

混乱の極みに陥っている。
これをブラッシュアップしてどうこうしたいぞう。
クラス化したいぞう。

スプレッドシートでどうこうするかGASするかカレンダ整えるか、方針がフラフラしてる。

global

new Date の引数は、テスト用に任意の日付入れてます。本番は空で。

const holidaySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('祝日リスト');
const dateMasterSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('日付マスタ');

const today = new Date('2022/04/16');

main

/**
 * トリガーアカウント: XXXX@gmail.com
 * 実行スクリプト:main
 * トリガーイベント:例: 時間主導型 / 日付ベースのタイマー / 1日 / 午前 7 時~8 時 
 */
function main() {
  //平日のみslackに投稿する用の日付判定用の別関数呼び出し、土日祝ならここで処理終了
  if (isWorkday(today) == false) { return; };

  //平日のみslackに投稿する用の日付判定用の別関数呼び出し、営業日ならslackに投稿
  if (isWorkday(today) == true) { postSlackbot() };
}

postSlackbot

/**
 * 参考
 * https://github.com/soundTricker/SlackApp
 * https://blog.guchimina.com/?p=370
 * https://auto-worker.com/blog/?p=2904#
 * 
 * 参考 プロパティストア
 * https://tonari-it.com/gas-property-store/
 * 
 * スプレッドシートで計算した残営業日を取得してslackで通知する。
 */

function postSlackbot() {

  //SlackAPIで登録したボットのトークンを設定する
  const token = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN');

  //ライブラリから導入したSlackAppを定義し、トークンを設定する
  const slackApp = SlackApp.create(token);

  //Slackボットがメッセージを投稿するチャンネルを定義する
  const channelId = "#general";

  //メッセージ内で使用する日付、残営業日数を定義する
  const todayFromSheet = dateMasterSheet.getRange('D2').getDisplayValue();
  const businessDaysRemainingThisMonth = dateMasterSheet.getRange('D28').getValue();
  const businessDaysRemainingInThisYear = dateMasterSheet.getRange('D29').getValue();
  const businessDaysRemainingInThisFiscalYear = dateMasterSheet.getRange('D30').getValue();

  //Slackボットが投稿するメッセージを定義する
  const message = `
今日は ${todayFromSheet} です。
今月の残り営業日数は  あと ${businessDaysRemainingThisMonth} 日です。
今年の残り営業日数は  あと ${businessDaysRemainingInThisYear} 日です。
今年度の残り営業日数は あと ${businessDaysRemainingInThisFiscalYear} 日です。
`

  //SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
  slackApp.postMessage(channelId, message);
}

isWorkday

この関数単体でテストするときは、

targetDate = new Date('2022/12/30')

のように、引数に直接指定しちゃってる。
デフォルト引数、でいいんだっけ、こういうの、言い方。


/**
 * 最終更新日 2022/4/15
 * @param {Date} targetDate - 今日の日付
 * @return {Boolean} 土日祝+指定の休日かどうかを判定 true=営業日
 * 参考
 * https://dev.classmethod.jp/articles/202001-workday-only-gas/
 */

function isWorkday(targetDate) {
// function isWorkday(targetDate = new Date('2022/12/30') {

  const getTimeOfTtargetDate = targetDate.getTime();

  // targetDate の曜日を確認、週末は休む (false)
  const rest_or_work = ["REST", "mon", "tue", "wed", "thu", "fri", "REST"]; // 日〜土
  if (rest_or_work[targetDate.getDay()] == "REST") {
    console.log(`今日は 土日ですよ`);
    return false;
  };

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

  //会社指定の休日(上記の祝日カレンダには入っていない休日、休暇)スプレッドシートから取得
  const winterHoliday1 = dateMasterSheet.getRange('D31').getValue().getTime();
  const winterHoliday2 = dateMasterSheet.getRange('D32').getValue().getTime();
  const winterHoliday3 = dateMasterSheet.getRange('D33').getValue().getTime();

  const winterHolidays = [winterHoliday1, winterHoliday2, winterHoliday3];

  //winterHolidays に targetDate が含まれていたら true
  if (winterHolidays.includes(targetDate.getTime()) == true) {
    //targetDate が冬季休暇日だったらfalse
    console.log(`今日は 冬季休日ですよ`);
    return false;
  }

  // 全て当てはまらなければ営業日 (True)
  console.log(`今日は 営業日ですよ`);
  return true;
}


残営業日計算用のスプレッドシート関数

この計算もGASでやる…?

=TODAY()=NETWORKDAYS(TODAY(),WORKDAY(EOMONTH(TODAY(), 0) + 1, -1, '祝日リスト'!B2:B),'祝日リスト'!B2:B)
=NETWORKDAYS(TODAY(),WORKDAY(EOMONTH(DATE(YEAR(TODAY()),12,31), 0)+1,-1, '祝日リスト'!B2:B),'祝日リスト'!B2:B)
=NETWORKDAYS(TODAY(),WORKDAY(EOMONTH(DATE(YEAR(TODAY())+1,3,31), 0)+1,-1, '祝日リスト'!B2:B),'祝日リスト'!B2:B)
=DATE(YEAR(TODAY()),12,29)
=DATE(YEAR(TODAY()),12,30)
=DATE(YEAR(TODAY()),12,31)



作成手順用のアレコレ(試行錯誤、自分のメモ、何を考えながらやっていたか、ログ)


以下、今回のnoteを書くにあたって、アレコレしたときのメモ。

1.アプリを作成する

アプリを作って、権限(スコープ、パーミッション)つけて、ワークスペースにアプリをインストールして、トークンを得る。

スクショ付の説明は下記のnoteに書いた。だいたい同じ流れです。
https://note.com/0375/n/n3f1def9a6c27


今回の流れを簡単に書くと下記の通り。

CreateNewApp
https://api.slack.com/apps→CreateNewApp→From scratch→AppName入れて、ワークスペース選択

OAuth & Permissions
OAuth & Permissions→Scopes→Bot Token Scopes, Add an OAuth Scope

chat:write
chat:write.customize
の2つをAdd

writeって打てばサジェストされる

Install App
Install App to Your Team
作ったアプリをワークスペースにインストールする。
インストールしたらInstall App の画面からBot User OAuth Token をコピペできる。

******

うーん、今回はbotとして営業日を通知する=書き込みをするので、writeの権限が必要だと思うんだけど、どれがいいのかな?

write で打ち込んでみると、
chat:write
chat:write.customize
chat:write.public

あたりが適していそうです。

まずは chat:write をAddしてみます。

すると、chat:write の部分が青文字になってリンク先に飛べますので、説明を読んでみます。
https://api.slack.com/scopes/chat:write

で、このへんでパーミッションちょっと調べないとな、と思って
パーミッションまわりのことはこちらの記事を参考にしました。

で、今回は営業日を通知するbotなので、
chat:write →メッセージ書くための基本パーミッション
chat:write.customize  →投稿者(bot)の画像や名前カスタム用
の二つのパーミッションで行くことにします。

chat:write.public は、参加してないチャンネルにもメッセージを送れてしまうので、下手に事故ったときに嫌なので、必要が無い限りは無くていいかな、というのが現時点の考えです。今回は営業日通知botを特定のチャンネルだけで通知させる、というのが目的なので、今後、botを流用したいときなどには chat:write.public のスコープも加えて使ってみようかな。

※chat:write.public 使わない場合には、そのBotが通知するチャンネルに参加させておく必要がある。

昔は chat:write:user や chat:write:bot があったようですね。
良く知らないけど、2020年にSlackの権限まわりが改訂される前に書かれたqiitaや各種ブログでは chat:write:user や chat:write:bot を使っているものがあるようでした。
chat:write.customize はnew Slack apps で使えるようになったとのこと。
chat:write.customize を使うには、chat:write が必須なんですね。


https://api.slack.com/methods/chat.postMessage#authorship


あれれ、アプリをインストールしようとすると、こうなった。

Basic Information で Botsをどうこうしてみる。
App Displey Name などをEditする
うし、インストールできる
@user-name が無い状態だったからインストールできんかったのか


今回は chat:write.public のスコープつけてないので、Slacknoワークスペースで営業日通知botさんに通知してもらうチャンネルに招待しておいて、下準備できた。


2.GASを書く(営業日はおいといて、まずは簡単なメッセージを投稿するbot)

前回ライブラリ使ってなかなったので、今回はライブラリを使ってみたいと思いまーす。

昔はプロジェクトIDだったが、現在はスクリプトIDで検索するようになった。

さて、まずは営業日やカウントのロジックは置いておいて、Botからメッセージを投稿するというGASを書く。

ライブラリを使いつつ、トークンはスクリプトプロパティにしようっと。

まずはざくっとこんなコード

変数として値が変わっていくようなものでなければ、基本的にconstで定数にしておきたい。

// 参考
// https://github.com/soundTricker/SlackApp
// https://blog.guchimina.com/?p=370
// https://auto-worker.com/blog/?p=2904#

// 参考 プロパティストア
// https://tonari-it.com/gas-property-store/

function postSlackbot() {
//SlackAPIで登録したボットのトークンを設定する
const token = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN');

//ライブラリから導入したSlackAppを定義し、トークンを設定する
const slackApp = SlackApp.create(token);

//Slackボットがメッセージを投稿するチャンネルを定義する
const channelId = "#general";

//Slackボットが投稿するメッセージを定義する
const message = "SlackボットによるGASからの投稿メッセージです。"

//SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
slackApp.postMessage(channelId, message);
}

よしよし、投稿できたぞい。

そんじゃあ、営業日を通知するためのロジックを考えていこう。
それができたら、メッセージに載せればよさそう。

やりたいこと
・今月の残り営業日を通知する。
・年内の残り営業日を通知する。

うーん、まずは営業日をどうすっか。え、これ、計算どうしよ。えー、うーん、まずは営業日、祝日リストが必要だよな~。

3.営業日のデータを整える

営業日かどうかはこのへんが参考になりそう

まずは祝日リストつくって、スプシに営業日判定の関数をやってみる。
フロントワークスさんのコードをそのままお借りしつつ、globalとClass Sheet のとこだけ変えた。


あと、すぐ書き出したかったので、一時的に updateHolidays if (tte.month !== 4) ※操作時の月 にしたり、トリガーでいったん1分おきにして書き込んでから、トリガーのタイミング変えたりした。

2022年の年末以降の祝日が取れた。

うーん、2022年の一覧も欲しいんだけど、コード書き換えるとめんどいので内閣府からデータ持ってくることにする。


カスタム日時形式にして曜日つけたりしてみたり。

うーん、夏季休暇は一斉にはなくて、各自適当な日時とるスタイルの職場だからここはオフにしておくか。

スプレッドシートでこんな感じに計算

あ~う~ん、えーと、月末営業日はこれでわかったから、
月末営業日-今日 で残りの日数は出る。
えーと、そんで、その日数から土日祝日を除きたいんじゃが、どうすれば?

わからん、営業日 計算 スプレッドシート でググる。

NETWORKDAYS関数 というのがあるのね。

フロントワークスさんのブログも参考にしつつ、おおかたスプレッドシートで計算できた~。理解しながら書いてたら時間かかっちった。


4.GAS 営業日数を投稿メッセージに含める

ほいじゃあ、あとはGASに書いていけばいいですね。
global変更すっか

あ~セルの値の取得はこれでいいかな。

日付はフォーマットするかあ。いや、スプシの表示形式変えて、getDisplayValueでいくか。


スプシ

で、テストしてみる

よさそう。
あとはインストーラブルトリガーで1日1回、朝に動くようすればいっかな。

あ~、これ、あれだ、Slackに投稿するときも、営業日のみ投稿、土日祝日は投稿しないようにする必要があるな。

ちょっと後で考えよう。今日はこんなとこで。

ひとまず現時点のGASはこうなった。


/**
 * 最終更新日 2022/4/12
 * 更新者 XXXX
 * 
 * 
 * 参考
 * https://github.com/soundTricker/SlackApp
 * https://blog.guchimina.com/?p=370
 * https://auto-worker.com/blog/?p=2904#
 * 
 * 参考 プロパティストア
 * https://tonari-it.com/gas-property-store/
 */

function postSlackbot() {
//SlackAPIで登録したボットのトークンを設定する
const token = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN');

//ライブラリから導入したSlackAppを定義し、トークンを設定する
const slackApp = SlackApp.create(token);

//Slackボットがメッセージを投稿するチャンネルを定義する
const channelId = "#general";


//メッセージ内で使用する日付、日数を定義する
const today = dateMasterSheet.getRange('D2').getDisplayValue();
const businessDaysRemainingThisMonth =  dateMasterSheet.getRange('D25').getValue();
const businessDaysRemainingInThisYear = dateMasterSheet.getRange('D26').getValue();

//Slackボットが投稿するメッセージを定義する
const message = `
今日は ${today} です。
今月の残り営業日数は あと ${businessDaysRemainingThisMonth} 日です。
年内の残り営業日数は あと ${businessDaysRemainingInThisYear} 日です。
`

//SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
slackApp.postMessage(channelId, message);
}

GAS上で営業日計算してもいいのかもしんないけど、スプシ上で計算して、その値を取得する形にした。
dateMasterSheetは別のスクリプトファイル(Class Sheet.gs)で定義している。

年度も加えてみた


5.土日祝以外の平日にSlackBotの投稿が来るようにする(ここで特に悩んだ)

うーん、どうしよっか。

このへんを読んでみる。
https://ajike.github.io/slack-bot-weekday/
https://dev.classmethod.jp/articles/202001-workday-only-gas/

GAS側で、特定日をkeyにして、それで判定して投稿する/しない 挙動にすればいいかな~。

で、いろいろコード書いてたら、new Date のログがおかしくて、タイムゾーンみたらAmerica/New_York になってた。

Asia/Tokyo に変える。参考https://front-works.co.jp/blog/gas-set-time-zone-to-japan/


あ~~~isWorkday に冬季休暇も加えて判定すればよさそうだけど、どう判定しよう??

  //冬季休暇や会社指定の休日(上記の祝日カレンダには入っていない休日、休暇)
  //12/29
  //12/30
  //12/31

  const winterHolidays = '****';
  if(****){ ****
    return false;
  }

だめだ、頭が働かない。こういう時に自分の頭の弱さに凹む。いったんここまで。

再開。

https://github.com/etau/gas-classes/blob/main/class_datetime.gs

は~、ほんとなんでもあるなetauさん。
ありがたく使わせていただく。う~んクラス…う~ん…いや、クラスほどでもないような…クラス…クラス理解が…

あ~~引数のデフォルト値で日付を指定の日にするにはどうすんだっけ、となって、試したりググったり した https://hirachin.com/post-4161/
インスタンスかーこのへんやっぱ理解が甘いな。

いけてるコードとはいいがたいが動くことは動く



あ~~~GASで日付計算すべき??
https://moripro.net/gas-month-day-later/


日付処理めんどくせ~~~
https://front-works.co.jp/blog/gas-create-datetime-class/


いやこれどうするよ?スプレッドシートで計算して呼び出すほうに寄せる?
GASで全部計算するのに寄せる?
あ~~~~。
https://zenn.dev/shige/articles/e56ca073e9fd15

あ~~~まてまて、日付の判定、比較がやっぱだめだ、これ、見た目同じだけど別インスタンスだから別もの扱いされてる。文字列とかしないとだめだ。


あ〜〜〜となりながら、こういうnote書いてた↓




クラスが〜〜〜と呻いていたら、そーちゃんが書いたものをスッしてくれた


雑感

パーミッションまわりのこと、理解度がちょっと上がった。
一方で、Boltとかなんとか、slackAPIのページもいろいろあるし、Overviewとかもあんまちゃんと読んでないにゃあ

他のAPIも色々試してみたいね。
でもなー、遊ぶ分にはいいんだけど、実際、仕事で使うとなると、そもそもそれ必要?とか、メンテどする?Zapierで良くない?とかいろいろ考えてしまう気もする。個人利用の範囲はどうとでもなるけど、複数人で業務利用となってくると制度設計がむにゃむにゃ。

TODAY関数について
https://support.google.com/docs/answer/3092984?hl=ja
公式に「不安定な関数」って書かれてて不安になる。
動的に更新したときの値を返すから、そらそうか。


スプレッドシートでの計算とGAS上の日付が混在してるので、なんかこう、もちっとスッキリさせたほうが良い気がするが、うーん。


参考URL等

本文中にもURLを示しているので、まとめきれていないが。

Slack関係


パーミッションまわりのことはこちらの記事を参考にしました。


営業日・祝日関係



タイムゾーン

その2はこちら


#GAS
#slack
#ノンプロ研

いただいたサポートで、書籍代や勉強費用にしたり、美味しいもの食べたりします!