おはこんばんちは、ソフトウエアエンジニアの橋本 (@af12066) です。コロナウイルスによる自粛期間もあり4ヶ月ほど散髪していないので、そろそろ行きつけの美容室に行きたい気持ちです。
これまでにfreee Developers Blogでは、人事労務freee APIを使用したさまざまな勤怠打刻方法がいくつか提案されてきました。
2018年にはIFTTTおよびGoogleアシスタントを利用した音声操作による打刻やUnity+Oculusを活用したVR打刻、2019年にはAWS IoTエンタープライズボタンを利用した打刻が紹介されました。
developers.freee.co.jp developers.freee.co.jp developers.freee.co.jp
本記事では打刻シリーズ第4弾として、私が開発した「打刻を意識しない打刻方法」を紹介したいと思います。
なお、この記事は「勤怠を自動化する技術」LT Nightで紹介した内容の文字起こし*1、および紹介しきれなかった内容の補足説明となります。
モチベーション
打刻は勤怠実績を残す重要な動作であり、時刻の正確性が要求されます。打刻の正確さと手軽さを両立させるために、たとえば人事労務freeeのトップ画面に打刻ボタンが設置されていたり、iOSアプリでは通知センターからワンタップで打刻することもできます。 一方、私のケースでは打刻ボタンを押す習慣がつく前に忘れてしまう、打刻リマインダを設定してもあとまわしにした結果打刻しない、といったことがたびたび発生していました...。
そこで、いつもどおり仕事をするだけで勝手に打刻される方法はないか?と考えた結果、本記事の手法が生まれました。
前提および事前準備
動作環境
この記事ではmacOSの機能を利用した方法を説明するため、それ以外の環境は対象外となります(すみません)。同様のサービス管理方法があれば実現可能であると思われます。
また、事業所にてfreeeアプリストアからアプリケーションを作成し、Client ID, Client Secret, 認可コードを取得しておいてください。手順の詳細は、freee APIチュートリアルをご参照ください。
勤怠の定義
今回は自動打刻の都合上、勤怠は会社支給のラップトップ上で作業した時間のみを対象とします。つまり、ラップトップを起動しログインしている間は勤務しているとみなすことにします。
自動打刻の概観
打刻に関するAPIドキュメント/create)によると、打刻には次の4種類が存在します。
- 出勤
- 休憩開始
- 休憩終了
- 退勤
一日あたり8時間労働とすると、上記4つすべての打刻を行なう必要があります*2。
出勤は勤怠の定義そのままで、ログインに成功した段階で打刻を行ないます。休憩は昼ごろに1時間確保しますが*3、具体的な時間の規定はないため、適当な時間に確保することにします。退勤もまた、ログアウト完了までに打刻をする必要があります。
ざっくり状態遷移は次のようになります。ただしAPIの具体的な仕様の説明ではないため、正確でない場合があります。
休憩開始の直後に退勤を打刻したり、退勤のあとに休憩終了しようとするとAPI側でエラーとなることから、上記の状態遷移となることが推定されます。
自動打刻の実装
トークンの取得および機密情報の格納
Client ID, Client Secret, 認可コードが取得済みであるとして、これらを用いてリフレッシュトークンを取得する必要があります。また、アクセストークンの取得・更新のために、Client ID、Client Secret、リフレッシュトークンも合わせて保存する必要があります。
トークンは任意のファイルに書き込んでから暗号化して保存したり環境変数に保存してもよいですが、Macにはキーチェーンアクセスという機密情報を保存するためのアプリケーションがあるため、これを活用してみます。
シェルからキーチェーンアクセスを操作するために、security
コマンドがあります。使い方の詳細はman 1 security
を参照してください。
リフレッシュトークンの取得からキーチェーンアクセスへの登録までをまとめると、以下のようなスクリプトになります。リフレッシュトークンの更新は別途行なうため、以下のスクリプトを実行するのは初回のみとなります。
# 初回のリフレッシュトークンを取得するために必要な設定をおこなう export FREEE_CLIENT_ID="<freee APIのClient ID>" export FREEE_CLIENT_SECRET="<freee APIのClient Secret>" export AUTHORIZATION_CODE="<freee APIの認可コード>"
#!/bin/bash readonly REFRESH_TOKEN=$(curl -X POST \ -H "Content-Type:application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=${FREEE_CLIENT_ID}" \ -d "client_secret=${FREEE_CLIENT_SECRET}" \ -d "code=${AUTHORIZATION_CODE}" \ -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob" 'https://accounts.secure.freee.co.jp/public_api/token' \ | jq -r '.refresh_token') readonly KEYCHAIN_SERVICE_NAME='freee-kintai' security add-generic-password -a client-id -w "${FREEE_CLIENT_ID}" -s "${KEYCHAIN_SERVICE_NAME}" security add-generic-password -a client-secret -w "${FREEE_CLIENT_SECRET}" -s "${KEYCHAIN_SERVICE_NAME}" security add-generic-password -a refresh-token -w "${REFRESH_TOKEN}" -s "${KEYCHAIN_SERVICE_NAME}"
キーチェーンアクセスを開き、パスワードと同様の扱いでClient ID, Client Secret, リフレッシュトークンが保存されていることを確認します。
打刻の汎用スクリプト
人事労務freee APIを操作するための、打刻用のスクリプト (Bash) を書いていきます。出勤・休憩・退勤はすべて同じエンドポイントであるため、単一のスクリプトにまとめて引数で打刻種別を変えるようにしてみます。一回の打刻におけるAPIリクエストの流れは次のようになります。
- さきほど登録したClient ID、Client Secret、リフレッシュトークンを使用して認証をおこなう。このとき、アクセストークンおよびリフレッシュトークンが払い出され、リフレッシュトークンはキーチェーンに上書き保存し、アクセストークンは以降のAPIリクエストに使用する
- アクセストークンを使用して、事業所IDおよび従業員IDを取得する
- アクセストークン、および2.で得られた事業所ID、従業員IDに加えて打刻種別を指定し、打刻APIにPOSTする
Client ID、Client Secret、リフレッシュトークンを使用してアクセストークンを取得する
さきほどキーチェーンに登録したClient ID、Client Secret、リフレッシュトークンを取得し、アクセストークンを取得するエンドポイントにPOSTします。
security find-generic-password
コマンドでキーチェーンの内容を取得できます。-w
オプションを与えることで、標準出力にその登録内容だけを流すことができます。
#!/bin/bash readonly CLIENT_ID=$(security find-generic-password -a client-id -w -s freee-kintai) readonly CLIENT_SECRET=$(security find-generic-password -a client-secret -w -s freee-kintai) readonly TOKEN=$(security find-generic-password -a refresh-token -w -s freee-kintai) readonly AUTH_RESP=$(curl -X POST \ -H "Content-Type:application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=${CLIENT_ID}" \ -d "client_secret=${CLIENT_SECRET}" \ -d "refresh_token=${TOKEN}" \ 'https://accounts.secure.freee.co.jp/public_api/token' 2>/dev/null) readonly ACCESS_TOKEN=$(echo ${AUTH_RESP} | jq -r '.access_token') readonly REFRESH_TOKEN=$(echo ${AUTH_RESP} | jq -r '.refresh_token') # add-generic-password -Uでキーチェーンに上書き保存する security add-generic-password -a refresh-token -w "${REFRESH_TOKEN}" -s freee-kintai -U
事業所IDおよび従業員IDの取得
2020年現在、/api/v1/users/me
で取得できます。
人事労務APIリファレンス | freee Developers Community
スクリプトは次のようになります。事業所名
は自身の事業所名に置換してください。
#!/bin/bash readonly COMPANY_RESPONSE=$(curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \ https://api.freee.co.jp/hr/api/v1/users/me 2>/dev/null) readonly COMPANY_ID=$(echo ${COMPANY_RESPONSE} | jq -r '.companies[] | select(.name | test("事業所名")) | .id') readonly EMPROYEE_ID=$(echo ${COMPANY_RESPONSE} | jq -r '.companies[] | select(.name | test("事業所名")) | .employee_id')
打刻APIにPOSTする
2020年現在、/api/v1/employees/${EMPROYEE_ID}/time_clocks
にPOSTすることで打刻できます。
人事労務APIリファレンス | freee Developers Community
#!/bin/bash readonly time_clocks_type=$1 readonly ISO8601DATE=$(date "+%Y-%m-%d") curl -X POST -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type:application/json" \ -d "{\"company_id\":${COMPANY_ID},\"type\":\"${time_clocks_type}\",\"base_date\":\"${ISO8601DATE}\"}" \ https://api.freee.co.jp/hr/api/v1/employees/${EMPROYEE_ID}/time_clocks 2>/dev/null)
time_clocks_type=$1
で引数を得ることで、アクセストークンの取得から打刻までを単一のスクリプトをまとめてdakoku.sh
として実行権限つきで保存したとき、/path/to/dakoku.sh clock_in
のように実行することで出勤打刻を行なうことができます。
打刻スクリプトの実行
作成したスクリプトdakoku.sh
を、勤怠種別ごとに実行させていきます。
出勤
システム環境設定の「ユーザとグループ」内、「ログイン項目」にて、ログイン直後に開きたいアプリケーションを指定します。ただし、実行権限を与えたシェルスクリプトを直接指定することはできないようです。
そこで、シェルスクリプトをラップしたAutomatorアプリケーションを作成し、それをログイン項目に指定することにします。
# Automatorアプリケーションのシェルに以下を記述して保存する export PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin /path/to/dakoku.sh clock_in launchctl unload -w ~/Library/LaunchAgents/freee-kintai-taikin.plist # 後述
初期状態では環境変数PATH
には/usr/bin:/bin:/usr/sbin:/sbin
しか含まれないため、スクリプトで使用するコマンドに応じてPATH
を追加してください。
また、初回実行時にはキーチェーンアクセスへのアクセス許可を求められます。
休憩開始および終了
厳密な休憩時刻は定められていないため、かりに12時から13時まで休憩することにします。
macOSでは、cronに相当する機能としてlaunchdが提供されています*4。launchdの概観を知るには、"A launchd Tutorial" がわかりやすいので、時間があれば眺めてみてください。
12時に休憩開始を打刻するplistは次のようになります。~/Library/LaunchAgents/freee-kintai-put-break-time.plist
といった名称で保存しておきます(~/Library/LaunchAgents/
以下であればファイル名はなんでもよい)。
StartCalendarInterval
を使用して平日の12時に打刻する設定となっており、Weekday
は日曜日が0、月曜日が1、...という指定となります。休憩終了も同様に作成します。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>freee-kintai-put-break-time</string> <key>ProgramArguments</key> <array> <!-- 絶対パスでスクリプトを指定する --> <string>/Users/your-login-name/path/to/dakoku.sh</string> <string>break_begin</string> </array> <!-- スクリプトの内容に合わせてPATHを更新する --> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> </dict> <key>StartCalendarInterval</key> <array> <dict> <key>Weekday</key> <integer>1</integer> <key>Hour</key> <integer>12</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Weekday</key> <integer>2</integer> <key>Hour</key> <integer>12</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Weekday</key> <integer>3</integer> <key>Hour</key> <integer>12</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Weekday</key> <integer>4</integer> <key>Hour</key> <integer>12</integer> <key>Minute</key> <integer>0</integer> </dict> <dict> <key>Weekday</key> <integer>5</integer> <key>Hour</key> <integer>12</integer> <key>Minute</key> <integer>0</integer> </dict> </array> <key>ExitTimeout</key> <integer>30</integer> <key>RunAtLoad</key> <false/> </dict> </plist>
launchctl load
コマンドにより、plistをlaunchdに登録します。
$ launchctl load -w ~/Library/LaunchAgents/freee-kintai-put-break-time.plist # 休憩開始 $ launchctl load -w ~/Library/LaunchAgents/freee-kintai-put-break-time-end.plist # 休憩終了 $ launchctl list | grep freee # launchdに登録したplistを確認。grepにヒットすればOK - 0 freee-kintai-put-break-time - 0 freee-kintai-put-break-time-end
退勤
出勤は「ログイン項目」にて指定できたものの、退勤に相当する「ログアウト項目」は存在しません。
ここで、freeeの退勤打刻を上書きできる仕様*5を活用します。具体的には、
- 19:00に退勤を打刻(WebまたはAPI経由で)
- 19:01に退勤をAPIで打刻
- 19:02に退勤をAPIで打刻
といった操作を順番に行ったとき、結果はすべて成功となり、最終的な退勤時刻は19:02となります(手順1.および2.は上書きされる)。つまり、休憩終了後からログアウトするまで退勤打刻をくりかえすことで、ログアウト時刻を退勤時刻とみなすことができます。
退勤をくりかえすplist (~/Library/LaunchAgents/freee-kintai-taikin.plist
) は次のようになります。以下の例ではStartInterval
を使用して2分ごとに退勤操作をくりかえしています。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>freee-kintai-taikin</string> <key>ProgramArguments</key> <array> <string>/Users/your-login-name/path/to/dakoku.sh</string> <string>clock_out</string> </array> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> </dict> <!-- 120秒ごとに退勤打刻をおこなう --> <key>StartInterval</key> <integer>120</integer> <!-- ねんのためstdout/stderrを残しておく --> <key>StandardOutPath</key> <string>/Users/your-login-name/path/to/taikin.out</string> <key>StandardErrorPath</key> <string>/Users/your-login-name/path/to/taikin.err</string> <key>ExitTimeout</key> <integer>30</integer> <!-- launchctl loadを実行した際にも退勤打刻をする --> <key>RunAtLoad</key> <true/> </dict> </plist>
ここで、出勤時に~/Library/LaunchAgents/freee-kintai-taikin.plist
をunload(launchdから登録解除)していましたが、これは休憩開始前に退勤打刻されるのを防ぐためです。
休憩終了後に再度loadする必要があるため、そのためのplistを休憩打刻と同様に以下のように作成します。休憩終了は13:00としたので、ひとまず14:30から退勤打刻を開始すれば問題ないでしょう。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>freee-kintai-trigger-taikin</string> <key>ProgramArguments</key> <array> <string>launchctl</string> <string>load</string> <string>-w</string> <string>/Users/your-login-name/Library/LaunchAgents/freee-kintai-taikin.plist</string> </array> <key>EnvironmentVariables</key> <dict> <key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> </dict> <key>StartCalendarInterval</key> <array> <dict> <key>Weekday</key> <integer>1</integer> <key>Hour</key> <integer>14</integer> <key>Minute</key> <integer>30</integer> </dict> <!-- ...省略 --> </array> <key>ExitTimeout</key> <integer>30</integer> <key>RunAtLoad</key> <false/> </dict> </plist>
再度launchctl list
を確認します。退勤をunload|loadしているため、時間帯によって結果が異なる場合があります。
$ launchctl list | grep freee - 0 freee-kintai-trigger-taikin - 0 freee-kintai-put-break-time - 0 freee-kintai-put-break-time-end
あとは数日間運用しつつ人事労務freeeのタイムレコーダーを確認し、正常に打刻されていることが確認できればOKです。
打刻の信頼性を向上させる
これまで紹介した実装に加えて、以下の実装を追加で行っています。
- APIサーバへの簡単な疎通確認を行ない、サーバに到達できないケース、認証に失敗したケース、状態遷移が正しくないケースのハンドリングやリトライ処理を行なう
- stdout/stderrを即座に確認しづらいため、たとえば
osascript -e 'display notification "人事労務freeeで打刻しました" with title "出勤"'
を実行することで打刻の成功・失敗に関するフィードバックを通知させる- Mac標準の通知機能をAppleScript経由で利用
打刻漏れをしてしまうことがまずいため、それを早期に検知できること、対処方法がわかること(スクリプトやネットワークがまずいのか、打刻種別がよくないのか、あきらめて手動打刻しなければならないのか、など)が重要であると考えます。
まとめ
この記事では、能動的に打刻操作をしない自動打刻方法を紹介しました。別の環境や打刻以外の場面で活用できるかもしれないので、機会があれば参考にしてみてください。
Enjoy!